From 0989f1ca30c8b7639911286cbf9bb11af379ea6a Mon Sep 17 00:00:00 2001 From: TxFig Date: Mon, 27 Oct 2025 16:54:47 +0000 Subject: [PATCH] Add TOML config parsing --- .gitignore | 2 + package.json | 8 ++-- pnpm-lock.yaml | 47 +++++++++++++++++++----- src/apis/gitea.ts | 14 ++++--- src/apis/github.ts | 40 ++++++++++---------- src/apis/index.ts | 9 +---- src/config.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 29 +++++++++------ src/result.ts | 16 ++++++++ 9 files changed, 198 insertions(+), 58 deletions(-) create mode 100644 src/config.ts create mode 100644 src/result.ts diff --git a/.gitignore b/.gitignore index 8225baa..a4246c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /node_modules /dist + +config.toml diff --git a/package.json b/package.json index e42bf48..3faa2c7 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,6 @@ "description": "Schedule issues in Gitea or GitHub using issue templates", "version": "0.0.0", "main": "dist/index.js", - "author": "Luís Figueiredo (TxFig)", "homepage": "https://gitea.alluna.pt/TxFig/issue-scheduler", "repository": { @@ -12,15 +11,18 @@ }, "license": "ISC", "keywords": [], - "scripts": { "test": "vitest --run", "build": "tsc", "start": "node dist/main.js" }, "devDependencies": { + "@types/node": "^24.9.1", "typescript": "^5.9.3", "vitest": "^3.2.4" }, - "type": "module" + "type": "module", + "dependencies": { + "toml": "^3.0.0" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de5d4aa..b72256a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,13 +7,20 @@ settings: importers: .: + dependencies: + toml: + specifier: ^3.0.0 + version: 3.0.0 devDependencies: + '@types/node': + specifier: ^24.9.1 + version: 24.9.1 typescript: specifier: ^5.9.3 version: 5.9.3 vitest: specifier: ^3.2.4 - version: 3.2.4 + version: 3.2.4(@types/node@24.9.1) packages: @@ -295,6 +302,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node@24.9.1': + resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -460,11 +470,17 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -699,6 +715,10 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node@24.9.1': + dependencies: + undici-types: 7.16.0 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.2 @@ -707,13 +727,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.9)': + '@vitest/mocker@3.2.4(vite@7.1.9(@types/node@24.9.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.19 optionalDependencies: - vite: 7.1.9 + vite: 7.1.9(@types/node@24.9.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -886,15 +906,19 @@ snapshots: tinyspy@4.0.4: {} + toml@3.0.0: {} + typescript@5.9.3: {} - vite-node@3.2.4: + undici-types@7.16.0: {} + + vite-node@3.2.4(@types/node@24.9.1): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.9 + vite: 7.1.9(@types/node@24.9.1) transitivePeerDependencies: - '@types/node' - jiti @@ -909,7 +933,7 @@ snapshots: - tsx - yaml - vite@7.1.9: + vite@7.1.9(@types/node@24.9.1): dependencies: esbuild: 0.25.10 fdir: 6.5.0(picomatch@4.0.3) @@ -918,13 +942,14 @@ snapshots: rollup: 4.52.4 tinyglobby: 0.2.15 optionalDependencies: + '@types/node': 24.9.1 fsevents: 2.3.3 - vitest@3.2.4: + vitest@3.2.4(@types/node@24.9.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.9) + '@vitest/mocker': 3.2.4(vite@7.1.9(@types/node@24.9.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -942,9 +967,11 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.9 - vite-node: 3.2.4 + vite: 7.1.9(@types/node@24.9.1) + vite-node: 3.2.4(@types/node@24.9.1) why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.9.1 transitivePeerDependencies: - jiti - less diff --git a/src/apis/gitea.ts b/src/apis/gitea.ts index 8377091..4d008d4 100644 --- a/src/apis/gitea.ts +++ b/src/apis/gitea.ts @@ -1,6 +1,8 @@ import { URL } from "url" import { BaseApi } from "./index.js" +import type { Result } from "../result.js"; +import * as result from "../result.js" // /api/swagger @@ -30,20 +32,20 @@ class Gitea extends BaseApi { } } - async getIssueTemplates() { + async getIssueTemplates(): Promise> { const url = new URL(IssueTemplatesUrl(this.user, this.repo), this.host) const response = await fetch(url, { headers: this.headers }) switch (response.status) { case 200: break - case 404: return { error: "Issue templates not found" } - default: return { error: "Unknown status code" } + case 404: return result.error("Issue templates not found") + default: return result.error("Unknown status code") } const json: GiteaIssueTemplatesResponse = await response.json() - return { - value: json.map(template => template.file_name) - } + return result.value( + json.map(template => template.file_name) + ) } } diff --git a/src/apis/github.ts b/src/apis/github.ts index 9a5776b..5886045 100644 --- a/src/apis/github.ts +++ b/src/apis/github.ts @@ -1,7 +1,8 @@ import { URL } from "url" -import {BaseApi, type Result} from "./index.js" - +import { BaseApi } from "./index.js" +import type { Result } from "../result.js" +import * as result from "../result.js" // docs.github.com/en/rest /* @@ -69,48 +70,47 @@ class GitHub extends BaseApi { const response = await fetch(url, { headers: this.headers }) switch (response.status) { - case 301: return { error: "Moved permanently" } - case 403: return { error: "Forbidden" } - case 404: return { error: "Resource not found" } + case 301: return result.error("Moved permanently") + case 403: return result.error("Forbidden") + case 404: return result.error("Resource not found") case 200: break - default: return { error: "Unknown status code" } + default: return result.error("Unknown status code") } const json: GitHubRepoInfoResponse = await response.json() - return { value: json.default_branch } + return result.value(json.default_branch) } async getRepoFiles(): Promise> { const branch = await this.getDefaultBranch() - if (!branch.value) return { error: `Default branch error: ${branch.error}` } + if (!branch.success) return result.error(`Default branch error: ${branch.error}`) const url = new URL(TreeUrl(this.user, this.repo, branch.value), this.host) const response = await fetch(url, { headers: this.headers }) switch (response.status) { case 200: break - case 404: return { error: "Resource not found" } - case 409: return { error: "Conflict" } - case 422: return { error: "Validation failed, or the endpoint has been spammed" } - default: return { error: "Unknown status code" } + case 404: return result.error("Resource not found") + case 409: return result.error("Conflict") + case 422: return result.error("Validation failed, or the endpoint has been spammed") + default: return result.error("Unknown status code") } const json: GitHubTreeResponse = await response.json() - return { - value: json.tree - .filter(entry => entry.type == "blob") - .map(entry => entry.path) - } + return result.value(json.tree + .filter(entry => entry.type == "blob") + .map(entry => entry.path) + ) } - async getIssueTemplates() { + async getIssueTemplates(): Promise> { const files = await this.getRepoFiles() - if (!files.value) return { error: `Repo files error: ${files.error}` } + if (!files.success) return result.error(`Repo files error: ${files.error}`) const issue_templates = files.value.filter( file => IssueTemplateFolders.some(folder => file.startsWith(folder)) ) - return { value: issue_templates } + return result.value(issue_templates) } } diff --git a/src/apis/index.ts b/src/apis/index.ts index b534953..2f12030 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,10 +1,5 @@ -export type Result = { - value: Type, - error?: null -} | { - value?: null, - error: Err -} +import type { Result } from "../result.js" + export abstract class BaseApi { constructor( diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8d6f518 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,91 @@ +import fs from "node:fs/promises" +import * as toml from "toml" +import type { Result } from "./result.js" +import * as result from "./result.js" + + +interface Repo { + url: URL + token: string + client?: "gitea" | "github" +} + +function isValidUrl(url: string) { + try { + new URL(url) + return true + } catch { + return false + } +} + +function validateUrl(url: unknown): Result { + if (!url) + return result.error("Missing url") + if (typeof url !== "string") + return result.error("Url must be a string") + if (!isValidUrl(url)) + return result.error("Invalid URL") + return result.value(undefined) +} + +function validateToken(token: unknown): Result { + if (!token) + return result.error("Missing token") + if (typeof token !== "string") + return result.error("Token must be a string") + return result.value(undefined) +} + +function validateClient(client: unknown): Result { + if (!client) + return result.value(undefined) + if (typeof client !== "string") + return result.error("Client must be a string") + if (!["gitea", "github"].includes(client)) { + return result.error("Client must be \"gitea\" or \"github\"") + } + return result.value(undefined) +} + +function validateConfig(config: Record): Result { + if (!("repo" in config) || + !Array.isArray(config["repo"]) + ) return result.error("Missing repo array of tables") + + for (let i = 0; i < config["repo"].length; i++) { + const repo = config["repo"][i] + const url = validateUrl(repo["url"]) + if (!url.success) + return result.error(`Invalid Config (Repo#${i + 1}): ${url.error}`) + repo["url"] = new URL(repo["url"]) + + const token = validateToken(repo["token"]) + if (!token.success) + return result.error(`Invalid Config (Repo#${i + 1}): ${token.error}`) + + const client = validateClient(repo["client"]) + if (!client.success) + return result.error(`Invalid Config (Repo#${i + 1}): ${client.error}`) + } + + return result.value(config["repo"]) +} + +export async function getConfig(config_filepath: string): Promise> { + let configFile: string + try { + configFile = await fs.readFile(config_filepath, "utf8") + } catch (err) { + return result.error(`Failed to open config file: "${config_filepath}"`) + } + + let config: Record + try { + config = toml.parse(configFile) + } catch (err) { + return result.error(`Failed to parse config file. ${err}`) + } + + return validateConfig(config) +} diff --git a/src/index.ts b/src/index.ts index 9299ab2..db2f06b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,22 +1,27 @@ import * as cli from "./cli.js" +import { getConfig } from "./config.js" -const config = cli.parseArgs(process.argv) -// console.log(config) +const cliConfig = cli.parseArgs(process.argv) +// console.log(cliConfig) import type { BaseApi } from "./apis"; import Gitea from "./apis/gitea.js" import GitHub from "./apis/github.js"; -let api: BaseApi -api = new Gitea( - "http://localhost:3000", "...", - "admin", "test_repo" -) -api = new GitHub( - "https://api.github.com", "...", - "TxFig", "test-api") +// let api: BaseApi +// api = new Gitea( +// "http://localhost:3000", "...", +// "admin", "test_repo" +// ) +// api = new GitHub( +// "https://api.github.com", "...", +// "TxFig", "s") +// +// const result = await api.getIssueTemplates() +// console.log(result.value ?? result.error) -const result = await api.getIssueTemplates() -console.log(result.value ?? result.error) + +const config = await getConfig(cliConfig.config_filepath) +console.log(config) diff --git a/src/result.ts b/src/result.ts new file mode 100644 index 0000000..2ee59b6 --- /dev/null +++ b/src/result.ts @@ -0,0 +1,16 @@ +export type Result = { + value: TValue, + success: true +} | { + error: TError, + success: false +} + +export function value(val: TValue): Result { + return { value: val, success: true } +} + +export function error(val: TError): Result { + return { error: val, success: false } +} +