From 2b81ba279c46a35d3adb8e899f0d29fecd3a557f Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 14:58:11 -0500 Subject: [PATCH 01/10] ci: Adds CI check to dry run publishing CLI to npm --- .github/workflows/ci-cli-npm.yml | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/ci-cli-npm.yml diff --git a/.github/workflows/ci-cli-npm.yml b/.github/workflows/ci-cli-npm.yml new file mode 100644 index 0000000..7d93e13 --- /dev/null +++ b/.github/workflows/ci-cli-npm.yml @@ -0,0 +1,35 @@ +name: CI - Publish @common-grants/cli (dry run) + +on: + pull_request: + branches: + - main + paths: + - "cli/**" # Trigger only if files in the `cli/` directory are changed + - ".github/workflows/ci-cli-npm.yml" + +defaults: + run: + working-directory: ./cli + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm ci + + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v4 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + + - name: Dry run publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --tag alpha --access public --dry-run From 357bba6c68201d0e5e8935f167089fa6ff1f8db8 Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 16:15:19 -0500 Subject: [PATCH 02/10] ci: Adds CD to publish the `@common-grants/cli` to npm --- .github/workflows/cd-cli-npm.yml | 38 ++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .github/workflows/cd-cli-npm.yml diff --git a/.github/workflows/cd-cli-npm.yml b/.github/workflows/cd-cli-npm.yml new file mode 100644 index 0000000..d897bff --- /dev/null +++ b/.github/workflows/cd-cli-npm.yml @@ -0,0 +1,38 @@ +name: CD - Publish @common-grants/cli + +on: + push: + branches: + - main + paths: + - "cli/**" # Trigger only if files in the `cli/` directory are changed + - ".github/workflows/cd-cli-npm.yml" + +defaults: + run: + working-directory: ./cli + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm ci + + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v4 + with: + node-version: "20.x" + registry-url: "https://registry.npmjs.org" + + - name: Run checks + run: npm run checks + + - name: Publish to npm + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --tag alpha --access public From 306faae61d78f8e849267ed895c346c38a249e1f Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 16:50:23 -0500 Subject: [PATCH 03/10] build: Update package.json for npm publishing --- cli/package-lock.json | 1 + cli/package.json | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/cli/package-lock.json b/cli/package-lock.json index ba92d1a..746b870 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "@common-grants/cli", "version": "0.1.0-alpha.1", + "license": "CC0-1.0", "dependencies": { "chalk": "^4.1.2", "commander": "^11.1.0", diff --git a/cli/package.json b/cli/package.json index 099009d..0cee1a6 100644 --- a/cli/package.json +++ b/cli/package.json @@ -6,10 +6,29 @@ "bin": { "cg": "./dist/index.js" }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/HHS/simpler-grants-protocol.git", + "directory": "cli" + }, + "bugs": { + "url": "https://github.com/HHS/simpler-grants-protocol/issues" + }, + "homepage": "https://github.com/HHS/simpler-grants-protocol/tree/main/cli#readme", + "keywords": [ + "cli", + "protocol", + "grants", + "opportunities" + ], "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "ts-node src/index.ts", + "prepare": "npm run build", "test": "jest", "test:watch": "jest --watch", "lint": "eslint . --ext .ts --fix", From 07dbe2c1951e8b8680a0f401a6f8bc2540760d48 Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 20:08:54 -0500 Subject: [PATCH 04/10] test: Simplifies and speeds up command tests --- cli/src/__tests__/commands/add-field.test.ts | 46 ++++++++--------- cli/src/__tests__/commands/check.test.ts | 52 ++++++++++---------- cli/src/__tests__/commands/generate.test.ts | 52 ++++++++++---------- cli/src/__tests__/commands/preview.test.ts | 47 +++++++++--------- 4 files changed, 93 insertions(+), 104 deletions(-) diff --git a/cli/src/__tests__/commands/add-field.test.ts b/cli/src/__tests__/commands/add-field.test.ts index fee763a..88e22a9 100644 --- a/cli/src/__tests__/commands/add-field.test.ts +++ b/cli/src/__tests__/commands/add-field.test.ts @@ -1,49 +1,45 @@ -import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { describe, it, expect, beforeEach, beforeAll, jest } from "@jest/globals"; import { Command } from "commander"; import { addFieldCommand } from "../../commands/add-field"; -import { DefaultFieldService } from "../../services/field.service"; -jest.mock("../../services/field.service"); +// Create mock function outside +const mockAddField = jest.fn(); + +// Mock the service with consistent implementation +jest.mock("../../services/field.service", () => ({ + DefaultFieldService: jest.fn(() => ({ + addField: mockAddField, + })), +})); describe("addFieldCommand", () => { let program: Command; + let addCmd: Command; - beforeEach(() => { + beforeAll(() => { program = new Command(); - (DefaultFieldService as jest.Mock).mockClear(); + addFieldCommand(program); + addCmd = program.commands.find(cmd => cmd.name() === "add")!; + }); + + beforeEach(() => { + mockAddField.mockClear(); }); it("should register add field command", () => { - addFieldCommand(program); - const addCmd = program.commands.find(cmd => cmd.name() === "add"); - expect(addCmd).toBeDefined(); - const fieldCmd = addCmd?.commands.find(cmd => cmd.name() === "field"); + const fieldCmd = addCmd.commands.find(cmd => cmd.name() === "field"); expect(fieldCmd).toBeDefined(); expect(fieldCmd?.description()).toBe("Add a custom field to the schema"); }); it("should handle field addition with basic options", async () => { - const mockAddField = jest.fn(); - (DefaultFieldService as jest.Mock).mockImplementation(() => ({ - addField: mockAddField, - })); - - addFieldCommand(program); - const addCmd = program.commands.find(cmd => cmd.name() === "add"); - await addCmd?.parseAsync(["node", "test", "field", "testField", "string"]); + await addCmd.parseAsync(["node", "test", "field", "testField", "string"]); expect(mockAddField).toHaveBeenCalledWith("testField", "string", {}); }); it("should handle field addition with all options", async () => { - const mockAddField = jest.fn(); - (DefaultFieldService as jest.Mock).mockImplementation(() => ({ - addField: mockAddField, - })); - - addFieldCommand(program); - const addCmd = program.commands.find(cmd => cmd.name() === "add"); - await addCmd?.parseAsync([ + await addCmd.parseAsync([ "node", "test", "field", diff --git a/cli/src/__tests__/commands/check.test.ts b/cli/src/__tests__/commands/check.test.ts index 0eb375c..448eb90 100644 --- a/cli/src/__tests__/commands/check.test.ts +++ b/cli/src/__tests__/commands/check.test.ts @@ -1,23 +1,37 @@ -import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { describe, it, expect, beforeEach, beforeAll, jest } from "@jest/globals"; import { Command } from "commander"; import { checkCommand } from "../../commands/check"; -import { DefaultValidationService } from "../../services/validation.service"; -jest.mock("../../services/validation.service"); +// Create mock functions outside +const mockCheckApi = jest.fn(); +const mockCheckSpec = jest.fn(); + +// Mock the service with consistent implementation +jest.mock("../../services/validation.service", () => ({ + DefaultValidationService: jest.fn(() => ({ + checkApi: mockCheckApi, + checkSpec: mockCheckSpec, + })), +})); describe("checkCommand", () => { let program: Command; + let checkCmd: Command; - beforeEach(() => { + beforeAll(() => { program = new Command(); - (DefaultValidationService as jest.Mock).mockClear(); + checkCommand(program); + checkCmd = program.commands.find(cmd => cmd.name() === "check")!; + }); + + beforeEach(() => { + mockCheckApi.mockClear(); + mockCheckSpec.mockClear(); }); describe("check api", () => { it("should register check api command", () => { - checkCommand(program); - const checkCmd = program.commands.find(cmd => cmd.name() === "check"); - const apiCmd = checkCmd?.commands.find(cmd => cmd.name() === "api"); + const apiCmd = checkCmd.commands.find(cmd => cmd.name() === "api"); expect(apiCmd).toBeDefined(); expect(apiCmd?.description()).toBe( "Validate an API implementation against its specification" @@ -25,14 +39,7 @@ describe("checkCommand", () => { }); it("should handle API validation with options", async () => { - const mockCheckApi = jest.fn(); - (DefaultValidationService as jest.Mock).mockImplementation(() => ({ - checkApi: mockCheckApi, - })); - - checkCommand(program); - const checkCmd = program.commands.find(cmd => cmd.name() === "check"); - await checkCmd?.parseAsync([ + await checkCmd.parseAsync([ "node", "test", "api", @@ -53,9 +60,7 @@ describe("checkCommand", () => { describe("check spec", () => { it("should register check spec command", () => { - checkCommand(program); - const checkCmd = program.commands.find(cmd => cmd.name() === "check"); - const specCmd = checkCmd?.commands.find(cmd => cmd.name() === "spec"); + const specCmd = checkCmd.commands.find(cmd => cmd.name() === "spec"); expect(specCmd).toBeDefined(); expect(specCmd?.description()).toBe( "Validate a specification against the CommonGrants base spec" @@ -63,14 +68,7 @@ describe("checkCommand", () => { }); it("should handle spec validation with version", async () => { - const mockCheckSpec = jest.fn(); - (DefaultValidationService as jest.Mock).mockImplementation(() => ({ - checkSpec: mockCheckSpec, - })); - - checkCommand(program); - const checkCmd = program.commands.find(cmd => cmd.name() === "check"); - await checkCmd?.parseAsync(["node", "test", "spec", "spec.yaml", "--spec-version", "v2.0.1"]); + await checkCmd.parseAsync(["node", "test", "spec", "spec.yaml", "--spec-version", "v2.0.1"]); expect(mockCheckSpec).toHaveBeenCalledWith("spec.yaml", { specVersion: "v2.0.1", diff --git a/cli/src/__tests__/commands/generate.test.ts b/cli/src/__tests__/commands/generate.test.ts index caf064f..aee055a 100644 --- a/cli/src/__tests__/commands/generate.test.ts +++ b/cli/src/__tests__/commands/generate.test.ts @@ -1,23 +1,37 @@ -import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { describe, it, expect, beforeEach, beforeAll, jest } from "@jest/globals"; import { Command } from "commander"; import { generateCommand } from "../../commands/generate"; -import { DefaultCodeGenerationService } from "../../services/code-generation.service"; -jest.mock("../../services/code-generation.service"); +// Create mock functions outside +const mockGenerateServer = jest.fn(); +const mockGenerateClient = jest.fn(); + +// Mock the service with consistent implementation +jest.mock("../../services/code-generation.service", () => ({ + DefaultCodeGenerationService: jest.fn(() => ({ + generateServer: mockGenerateServer, + generateClient: mockGenerateClient, + })), +})); describe("generateCommand", () => { let program: Command; + let generateCmd: Command; - beforeEach(() => { + beforeAll(() => { program = new Command(); - (DefaultCodeGenerationService as jest.Mock).mockClear(); + generateCommand(program); + generateCmd = program.commands.find(cmd => cmd.name() === "generate")!; + }); + + beforeEach(() => { + mockGenerateServer.mockClear(); + mockGenerateClient.mockClear(); }); describe("generate server", () => { it("should register generate server command", () => { - generateCommand(program); - const generateCmd = program.commands.find(cmd => cmd.name() === "generate"); - const serverCmd = generateCmd?.commands.find(cmd => cmd.name() === "server"); + const serverCmd = generateCmd.commands.find(cmd => cmd.name() === "server"); expect(serverCmd).toBeDefined(); expect(serverCmd?.description()).toBe( "Generate API server code from a TypeSpec specification" @@ -25,14 +39,7 @@ describe("generateCommand", () => { }); it("should handle server generation with options", async () => { - const mockGenerateServer = jest.fn(); - (DefaultCodeGenerationService as jest.Mock).mockImplementation(() => ({ - generateServer: mockGenerateServer, - })); - - generateCommand(program); - const generateCmd = program.commands.find(cmd => cmd.name() === "generate"); - await generateCmd?.parseAsync([ + await generateCmd.parseAsync([ "node", "test", "server", @@ -52,22 +59,13 @@ describe("generateCommand", () => { describe("generate client", () => { it("should register generate client command", () => { - generateCommand(program); - const generateCmd = program.commands.find(cmd => cmd.name() === "generate"); - const clientCmd = generateCmd?.commands.find(cmd => cmd.name() === "client"); + const clientCmd = generateCmd.commands.find(cmd => cmd.name() === "client"); expect(clientCmd).toBeDefined(); expect(clientCmd?.description()).toBe("Generate client code from a TypeSpec specification"); }); it("should handle client generation with options", async () => { - const mockGenerateClient = jest.fn(); - (DefaultCodeGenerationService as jest.Mock).mockImplementation(() => ({ - generateClient: mockGenerateClient, - })); - - generateCommand(program); - const generateCmd = program.commands.find(cmd => cmd.name() === "generate"); - await generateCmd?.parseAsync([ + await generateCmd.parseAsync([ "node", "test", "client", diff --git a/cli/src/__tests__/commands/preview.test.ts b/cli/src/__tests__/commands/preview.test.ts index ed09d6e..fc6ec5e 100644 --- a/cli/src/__tests__/commands/preview.test.ts +++ b/cli/src/__tests__/commands/preview.test.ts @@ -1,34 +1,38 @@ -import { describe, it, expect, beforeEach, jest } from "@jest/globals"; +import { describe, it, expect, beforeEach, beforeAll, jest } from "@jest/globals"; import { Command } from "commander"; import { previewCommand } from "../../commands/preview"; -import { DefaultPreviewService } from "../../services/preview.service"; -jest.mock("../../services/preview.service"); +// Create mock function outside +const mockPreviewSpec = jest.fn(); + +// Mock the service with consistent implementation +jest.mock("../../services/preview.service", () => ({ + DefaultPreviewService: jest.fn(() => ({ + previewSpec: mockPreviewSpec, + })), +})); describe("previewCommand", () => { let program: Command; + let previewCmd: Command; - beforeEach(() => { + beforeAll(() => { program = new Command(); - (DefaultPreviewService as jest.Mock).mockClear(); + previewCommand(program); + previewCmd = program.commands.find(cmd => cmd.name() === "preview")!; + }); + + beforeEach(() => { + mockPreviewSpec.mockClear(); }); it("should register preview command", () => { - previewCommand(program); - const cmd = program.commands.find(cmd => cmd.name() === "preview"); - expect(cmd).toBeDefined(); - expect(cmd?.description()).toBe("Preview an OpenAPI specification"); + expect(previewCmd).toBeDefined(); + expect(previewCmd.description()).toBe("Preview an OpenAPI specification"); }); it("should handle preview with default UI", async () => { - const mockPreviewSpec = jest.fn(); - (DefaultPreviewService as jest.Mock).mockImplementation(() => ({ - previewSpec: mockPreviewSpec, - })); - - previewCommand(program); - const cmd = program.commands.find(cmd => cmd.name() === "preview"); - await cmd?.parseAsync(["node", "test", "spec.yaml"]); + await previewCmd.parseAsync(["node", "test", "spec.yaml"]); expect(mockPreviewSpec).toHaveBeenCalledWith("spec.yaml", { ui: "swagger", @@ -36,14 +40,7 @@ describe("previewCommand", () => { }); it("should handle preview with custom UI", async () => { - const mockPreviewSpec = jest.fn(); - (DefaultPreviewService as jest.Mock).mockImplementation(() => ({ - previewSpec: mockPreviewSpec, - })); - - previewCommand(program); - const cmd = program.commands.find(cmd => cmd.name() === "preview"); - await cmd?.parseAsync(["node", "test", "spec.yaml", "--ui", "redocly"]); + await previewCmd.parseAsync(["node", "test", "spec.yaml", "--ui", "redocly"]); expect(mockPreviewSpec).toHaveBeenCalledWith("spec.yaml", { ui: "redocly", From 3fdf330b7f22834ce41efa32950e0e5eb10f45b5 Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 20:54:12 -0500 Subject: [PATCH 05/10] build: Adds zod for improved arg parsing --- cli/package-lock.json | 12 +++++++++++- cli/package.json | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 746b870..c111fc6 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "chalk": "^4.1.2", "commander": "^11.1.0", - "inquirer": "^8.2.6" + "inquirer": "^8.2.6", + "zod": "^3.24.1" }, "bin": { "cg": "dist/index.js" @@ -6050,6 +6051,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/cli/package.json b/cli/package.json index 0cee1a6..fb87a8f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,7 @@ { "name": "@common-grants/cli", "version": "0.1.0-alpha.1", + "license": "CC0-1.0", "description": "The CommonGrants protocol CLI tool", "main": "dist/index.js", "bin": { @@ -39,7 +40,8 @@ "dependencies": { "chalk": "^4.1.2", "commander": "^11.1.0", - "inquirer": "^8.2.6" + "inquirer": "^8.2.6", + "zod": "^3.24.1" }, "devDependencies": { "@types/inquirer": "^8.2.10", @@ -51,8 +53,8 @@ "eslint-config-prettier": "^9.1.0", "eslint-plugin-jest": "^27.9.0", "eslint-plugin-prettier": "^5.1.3", - "prettier": "^3.2.5", "jest": "^29.7.0", + "prettier": "^3.2.5", "ts-jest": "^29.1.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" From a20c0030d34b5342d455c357e052b07d68601607 Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 20:55:27 -0500 Subject: [PATCH 06/10] refactor: Updates arg parsing in commands --- .../__tests__/types/command-options.test.ts | 65 ------------ cli/src/commands/add-field.ts | 10 +- cli/src/commands/check.ts | 21 +++- cli/src/commands/generate.ts | 28 ++--- cli/src/commands/init.ts | 11 +- cli/src/commands/preview.ts | 9 +- cli/src/types/command-args.ts | 100 ++++++++++++++++++ cli/src/types/command-options.ts | 88 --------------- 8 files changed, 146 insertions(+), 186 deletions(-) delete mode 100644 cli/src/__tests__/types/command-options.test.ts create mode 100644 cli/src/types/command-args.ts delete mode 100644 cli/src/types/command-options.ts diff --git a/cli/src/__tests__/types/command-options.test.ts b/cli/src/__tests__/types/command-options.test.ts deleted file mode 100644 index 953f65e..0000000 --- a/cli/src/__tests__/types/command-options.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, it, expect } from "@jest/globals"; -import { - validatePreviewOptions, - validateCheckApiOptions, - validateGenerateServerOptions, -} from "../../types/command-options"; - -describe("Command Options Validation", () => { - describe("validatePreviewOptions", () => { - it("should accept valid UI options", () => { - expect(validatePreviewOptions({ ui: "swagger" })).toEqual({ - ui: "swagger", - }); - expect(validatePreviewOptions({ ui: "redocly" })).toEqual({ - ui: "redocly", - }); - }); - - it("should use swagger as default UI", () => { - expect(validatePreviewOptions({})).toEqual({ ui: "swagger" }); - }); - - it("should reject invalid UI options", () => { - expect(() => validatePreviewOptions({ ui: "invalid" })).toThrow( - 'UI option must be either "swagger" or "redocly"' - ); - }); - }); - - describe("validateCheckApiOptions", () => { - it("should accept valid report formats", () => { - expect(validateCheckApiOptions({ report: "json" })).toEqual({ - client: undefined, - report: "json", - auth: undefined, - }); - expect(validateCheckApiOptions({ report: "html" })).toEqual({ - client: undefined, - report: "html", - auth: undefined, - }); - }); - - it("should reject invalid report formats", () => { - expect(() => validateCheckApiOptions({ report: "invalid" })).toThrow( - 'Report format must be either "json" or "html"' - ); - }); - }); - - describe("validateGenerateServerOptions", () => { - it("should accept valid component options", () => { - expect(validateGenerateServerOptions({ only: "controllers,models" })).toEqual({ - lang: undefined, - only: "controllers,models", - }); - }); - - it("should reject invalid components", () => { - expect(() => validateGenerateServerOptions({ only: "invalid,components" })).toThrow( - /Invalid components: invalid, components/ - ); - }); - }); -}); diff --git a/cli/src/commands/add-field.ts b/cli/src/commands/add-field.ts index d42d182..4051190 100644 --- a/cli/src/commands/add-field.ts +++ b/cli/src/commands/add-field.ts @@ -1,5 +1,6 @@ import { Command } from "commander"; import { DefaultFieldService } from "../services/field.service"; +import { AddFieldArgsSchema, AddFieldCommandSchema } from "../types/command-args"; export function addFieldCommand(program: Command) { const fieldService = new DefaultFieldService(); @@ -9,15 +10,14 @@ export function addFieldCommand(program: Command) { .command("field") .description("Add a custom field to the schema") .argument("", "Name of the field") - .argument("", "Type of the field") + .argument("", "Type of the field (string|number|boolean|date|object|array)") .option("--example ", "Example value for the field") .option("--description ", "Description of the field") .action(async (name, type, options) => { try { - await fieldService.addField(name, type, { - example: options.example, - description: options.description, - }); + const validatedArgs = AddFieldArgsSchema.parse({ name, type }); + const validatedOptions = AddFieldCommandSchema.parse(options); + await fieldService.addField(validatedArgs.name, validatedArgs.type, validatedOptions); } catch (error) { console.error("Error adding field:", error); process.exit(1); diff --git a/cli/src/commands/check.ts b/cli/src/commands/check.ts index 37d7355..afbc667 100644 --- a/cli/src/commands/check.ts +++ b/cli/src/commands/check.ts @@ -1,6 +1,11 @@ import { Command } from "commander"; import { DefaultValidationService } from "../services/validation.service"; -import { validateCheckApiOptions, validateCheckSpecOptions } from "../types/command-options"; +import { + CheckApiArgsSchema, + CheckApiCommandSchema, + CheckSpecArgsSchema, + CheckSpecCommandSchema, +} from "../types/command-args"; export function checkCommand(program: Command) { const validationService = new DefaultValidationService(); @@ -17,8 +22,13 @@ export function checkCommand(program: Command) { .option("--auth ", "Authentication token or credentials") .action(async (apiUrl, specPath, options) => { try { - const validatedOptions = validateCheckApiOptions(options); - await validationService.checkApi(apiUrl, specPath, validatedOptions); + const validatedArgs = CheckApiArgsSchema.parse({ apiUrl, specPath }); + const validatedOptions = CheckApiCommandSchema.parse(options); + await validationService.checkApi( + validatedArgs.apiUrl, + validatedArgs.specPath, + validatedOptions + ); } catch (error) { if (error instanceof Error) { console.error("Validation error:", error.message); @@ -37,8 +47,9 @@ export function checkCommand(program: Command) { .option("--base ", "Path to base spec for validation") .action(async (specPath, options) => { try { - const validatedOptions = validateCheckSpecOptions(options); - await validationService.checkSpec(specPath, validatedOptions); + const validatedArgs = CheckSpecArgsSchema.parse({ specPath }); + const validatedOptions = CheckSpecCommandSchema.parse(options); + await validationService.checkSpec(validatedArgs.specPath, validatedOptions); } catch (error) { if (error instanceof Error) { console.error("Validation error:", error.message); diff --git a/cli/src/commands/generate.ts b/cli/src/commands/generate.ts index 2f23bbb..0a12bd5 100644 --- a/cli/src/commands/generate.ts +++ b/cli/src/commands/generate.ts @@ -1,6 +1,10 @@ import { Command } from "commander"; import { DefaultCodeGenerationService } from "../services/code-generation.service"; -import { validateGenerateServerOptions } from "../types/command-options"; +import { + GenerateArgsSchema, + GenerateServerCommandSchema, + GenerateClientCommandSchema, +} from "../types/command-args"; export function generateCommand(program: Command) { const generationService = new DefaultCodeGenerationService(); @@ -15,11 +19,9 @@ export function generateCommand(program: Command) { .option("--only ", "Generate only specific components") .action(async (specPath, options) => { try { - const validatedOptions = validateGenerateServerOptions(options); - await generationService.generateServer(specPath, { - lang: validatedOptions.lang, - only: validatedOptions.only?.split(","), - }); + const validatedArgs = GenerateArgsSchema.parse({ specPath }); + const validatedOptions = GenerateServerCommandSchema.parse(options); + await generationService.generateServer(validatedArgs.specPath, validatedOptions); } catch (error) { if (error instanceof Error) { console.error("Validation error:", error.message); @@ -39,13 +41,15 @@ export function generateCommand(program: Command) { .option("--docs", "Include API documentation") .action(async (specPath, options) => { try { - await generationService.generateClient(specPath, { - lang: options.lang, - output: options.output, - docs: options.docs, - }); + const validatedArgs = GenerateArgsSchema.parse({ specPath }); + const validatedOptions = GenerateClientCommandSchema.parse(options); + await generationService.generateClient(validatedArgs.specPath, validatedOptions); } catch (error) { - console.error("Error generating client:", error); + if (error instanceof Error) { + console.error("Validation error:", error.message); + } else { + console.error("Error generating client:", error); + } process.exit(1); } }); diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts index 0ef007e..73a8cdb 100644 --- a/cli/src/commands/init.ts +++ b/cli/src/commands/init.ts @@ -1,6 +1,7 @@ import { Command } from "commander"; -import { InitService, InitOptions } from "../services/interfaces"; +import { InitService } from "../services/interfaces"; import { DefaultInitService } from "../services/init.service"; +import { InitCommandSchema } from "../types/command-args"; export function initCommand(program: Command) { const initService: InitService = new DefaultInitService(); @@ -20,12 +21,8 @@ export function initCommand(program: Command) { return; } - const initOptions: InitOptions = { - template: options.template, - output: options.output, - }; - - await initService.init(initOptions); + const validatedOptions = InitCommandSchema.parse(options); + await initService.init(validatedOptions); } catch (error) { console.error("Error initializing project:", error); process.exit(1); diff --git a/cli/src/commands/preview.ts b/cli/src/commands/preview.ts index 5784dff..4dfae80 100644 --- a/cli/src/commands/preview.ts +++ b/cli/src/commands/preview.ts @@ -1,6 +1,6 @@ import { Command } from "commander"; import { DefaultPreviewService } from "../services/preview.service"; -import { validatePreviewOptions } from "../types/command-options"; +import { PreviewArgsSchema, PreviewCommandSchema } from "../types/command-args"; export function previewCommand(program: Command) { const previewService = new DefaultPreviewService(); @@ -8,12 +8,13 @@ export function previewCommand(program: Command) { program .command("preview") .description("Preview an OpenAPI specification") - .argument("", "Path to TypeSpec file") + .argument("", "Path to TypeSpec or OpenAPI spec (.tsp or .yaml)") .option("--ui ", "Preview tool to use (swagger or redocly)", "swagger") .action(async (specPath, options) => { try { - const validatedOptions = validatePreviewOptions(options); - await previewService.previewSpec(specPath, validatedOptions); + const validatedArgs = PreviewArgsSchema.parse({ specPath }); + const validatedOptions = PreviewCommandSchema.parse(options); + await previewService.previewSpec(validatedArgs.specPath, validatedOptions); } catch (error) { if (error instanceof Error) { console.error("Validation error:", error.message); diff --git a/cli/src/types/command-args.ts b/cli/src/types/command-args.ts new file mode 100644 index 0000000..eb39eee --- /dev/null +++ b/cli/src/types/command-args.ts @@ -0,0 +1,100 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { z } from "zod"; + +// ############################################################ +// Zod Schemas - Arguments +// ############################################################ + +export const AddFieldArgsSchema = z.object({ + name: z.string(), + type: z.enum(["string", "number", "boolean", "date", "object", "array"]), +}); + +export const PreviewArgsSchema = z.object({ + specPath: z.string().endsWith(".tsp").or(z.string().endsWith(".yaml")), +}); + +export const CheckApiArgsSchema = z.object({ + apiUrl: z.string().url(), + specPath: z.string().endsWith(".tsp").or(z.string().endsWith(".yaml")), +}); + +export const CheckSpecArgsSchema = z.object({ + specPath: z.string().endsWith(".tsp").or(z.string().endsWith(".yaml")), +}); + +export const GenerateArgsSchema = z.object({ + specPath: z.string().endsWith(".tsp").or(z.string().endsWith(".yaml")), +}); + +// ############################################################ +// Zod Schemas - Options +// ############################################################ + +export const InitCommandSchema = z.object({ + template: z.string().optional(), + dir: z.string().optional(), + list: z.boolean().optional(), +}); + +export const AddFieldCommandSchema = z.object({ + example: z.string().optional(), + description: z.string().optional(), +}); + +export const PreviewCommandSchema = z.object({ + ui: z.enum(["swagger", "redocly"]).default("swagger"), +}); + +export const CheckApiCommandSchema = z.object({ + client: z.string().optional(), + report: z.enum(["json", "html"]).optional(), + auth: z.string().optional(), +}); + +export const GenerateServerCommandSchema = z.object({ + lang: z.string().optional(), + only: z + .string() + .optional() + .transform(val => val?.split(",")) + .refine(val => !val || val.every(c => ["controllers", "models", "routes"].includes(c)), { + message: "Only valid components are: controllers, models, routes", + }), +}); + +export const CheckSpecCommandSchema = z.object({ + specVersion: z + .string() + .regex(/^v[0-9]+\.[0-9]+\.[0-9]+$/, "Version must be in format vX.Y.Z (e.g., v2.0.1)") + .optional(), + base: z.string().optional(), +}); + +export const GenerateClientCommandSchema = z.object({ + lang: z.string().optional(), + output: z.string().optional(), + docs: z.boolean().optional(), +}); + +// ############################################################ +// Types +// ############################################################ + +export type AddFieldArgs = z.infer; +export type PreviewArgs = z.infer; +export type CheckApiArgs = z.infer; +export type CheckSpecArgs = z.infer; +export type GenerateArgs = z.infer; + +// ############################################################ +// Types - Options +// ############################################################ + +export type InitCommandOptions = z.infer; +export type AddFieldCommandOptions = z.infer; +export type PreviewCommandOptions = z.infer; +export type CheckApiCommandOptions = z.infer; +export type GenerateServerCommandOptions = z.infer; +export type CheckSpecCommandOptions = z.infer; +export type GenerateClientCommandOptions = z.infer; diff --git a/cli/src/types/command-options.ts b/cli/src/types/command-options.ts deleted file mode 100644 index bfb78dc..0000000 --- a/cli/src/types/command-options.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// Define strict types for command options -export interface InitCommandOptions { - template?: string; - dir?: string; - list?: boolean; -} - -export interface PreviewCommandOptions { - ui: "swagger" | "redocly"; -} - -export interface AddFieldCommandOptions { - example?: string; - description?: string; -} - -export interface CheckApiCommandOptions { - client?: string; - report?: "json" | "html"; - auth?: string; -} - -export interface GenerateServerCommandOptions { - lang?: string; - only?: string; -} - -export interface GenerateClientCommandOptions { - lang?: string; - output?: string; - docs?: boolean; -} - -export interface CheckSpecCommandOptions { - specVersion?: string; - base?: string; -} - -// Validation functions -export function validatePreviewOptions(options: any): PreviewCommandOptions { - if (options.ui && !["swagger", "redocly"].includes(options.ui)) { - throw new Error('UI option must be either "swagger" or "redocly"'); - } - return { - ui: options.ui || "swagger", - }; -} - -export function validateCheckApiOptions(options: any): CheckApiCommandOptions { - if (options.report && !["json", "html"].includes(options.report)) { - throw new Error('Report format must be either "json" or "html"'); - } - return { - client: options.client, - report: options.report, - auth: options.auth, - }; -} - -export function validateGenerateServerOptions(options: any): GenerateServerCommandOptions { - if (options.only) { - const validComponents = ["controllers", "models", "routes"]; - const components = options.only.split(","); - const invalidComponents = components.filter((c: string) => !validComponents.includes(c)); - if (invalidComponents.length > 0) { - throw new Error( - `Invalid components: ${invalidComponents.join( - ", " - )}. Valid components are: ${validComponents.join(", ")}` - ); - } - } - return { - lang: options.lang, - only: options.only, - }; -} - -export function validateCheckSpecOptions(options: any): CheckSpecCommandOptions { - if (options.specVersion && !/^v[0-9]+\.[0-9]+\.[0-9]+$/.test(options.specVersion)) { - throw new Error("Version must be in format vX.Y.Z (e.g., v2.0.1)"); - } - return { - specVersion: options.specVersion, - base: options.base, - }; -} From af4dded17b7332a88f849bb48663cb55c9ec71ad Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 21:44:03 -0500 Subject: [PATCH 07/10] refactor: Standardizes error handling --- .../__tests__/commands/error-handling.test.ts | 135 ++++++++++++++++++ cli/src/commands/init.ts | 4 +- cli/src/types/command-args.ts | 4 +- cli/src/utils/error.ts | 16 +++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 cli/src/__tests__/commands/error-handling.test.ts create mode 100644 cli/src/utils/error.ts diff --git a/cli/src/__tests__/commands/error-handling.test.ts b/cli/src/__tests__/commands/error-handling.test.ts new file mode 100644 index 0000000..07a7653 --- /dev/null +++ b/cli/src/__tests__/commands/error-handling.test.ts @@ -0,0 +1,135 @@ +import { describe, it, expect, jest, beforeEach } from "@jest/globals"; +import { Command } from "commander"; +import { initCommand } from "../../commands/init"; +import { previewCommand } from "../../commands/preview"; +import { addFieldCommand } from "../../commands/add-field"; +import { checkCommand } from "../../commands/check"; +import { generateCommand } from "../../commands/generate"; + +// Mock console.error and process.exit +const mockConsoleError = jest.spyOn(console, "error").mockImplementation(() => {}); +const mockProcessExit = jest.spyOn(process, "exit").mockImplementation(() => undefined as never); + +describe("Command Error Handling", () => { + let program: Command; + + beforeEach(() => { + program = new Command(); + mockConsoleError.mockClear(); + mockProcessExit.mockClear(); + }); + + describe("init command", () => { + beforeEach(() => { + initCommand(program); + }); + + it("should handle validation errors", async () => { + const initCmd = program.commands.find(cmd => cmd.name() === "init")!; + await initCmd.parseAsync(["node", "test", "--template", ""]); + + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + }); + + describe("preview command", () => { + beforeEach(() => { + previewCommand(program); + }); + + it("should handle invalid UI option", async () => { + const previewCmd = program.commands.find(cmd => cmd.name() === "preview")!; + await previewCmd.parseAsync(["node", "test", "spec.tsp", "--ui", "invalid"]); + + expect(mockConsoleError).toHaveBeenCalledWith( + "Validation error:", + expect.stringContaining("invalid_enum_value") + ); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + }); + + describe("add field command", () => { + beforeEach(() => { + addFieldCommand(program); + }); + + it("should handle invalid field type", async () => { + const addCmd = program.commands.find(cmd => cmd.name() === "add")!; + await addCmd.parseAsync(["node", "test", "field", "testField", "invalid-type"]); + + expect(mockConsoleError).toHaveBeenCalledWith("Error adding field:", expect.any(Error)); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + }); + + describe("check command", () => { + beforeEach(() => { + checkCommand(program); + }); + + it("should handle invalid API URL", async () => { + const checkCmd = program.commands.find(cmd => cmd.name() === "check")!; + await checkCmd.parseAsync(["node", "test", "api", "not-a-url", "spec.tsp"]); + + expect(mockConsoleError).toHaveBeenCalledWith( + "Validation error:", + expect.stringContaining("Invalid url") + ); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + + it("should handle invalid spec version", async () => { + const checkCmd = program.commands.find(cmd => cmd.name() === "check")!; + await checkCmd.parseAsync([ + "node", + "test", + "spec", + "spec.tsp", + "--spec-version", + "invalid-version", + ]); + + expect(mockConsoleError).toHaveBeenCalledWith( + "Validation error:", + expect.stringContaining("Version must be in format") + ); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + }); + + describe("generate command", () => { + beforeEach(() => { + generateCommand(program); + }); + + it("should handle invalid file extension", async () => { + const generateCmd = program.commands.find(cmd => cmd.name() === "generate")!; + await generateCmd.parseAsync(["node", "test", "client", "spec.invalid"]); + + expect(mockConsoleError).toHaveBeenCalledWith( + "Validation error:", + expect.stringContaining("invalid_string") + ); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + + it("should handle invalid server components", async () => { + const generateCmd = program.commands.find(cmd => cmd.name() === "generate")!; + await generateCmd.parseAsync([ + "node", + "test", + "server", + "spec.tsp", + "--only", + "invalid-component", + ]); + + expect(mockConsoleError).toHaveBeenCalledWith( + "Validation error:", + expect.stringContaining("Only valid components are") + ); + expect(mockProcessExit).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/cli/src/commands/init.ts b/cli/src/commands/init.ts index 73a8cdb..a9abbaa 100644 --- a/cli/src/commands/init.ts +++ b/cli/src/commands/init.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; import { InitService } from "../services/interfaces"; import { DefaultInitService } from "../services/init.service"; import { InitCommandSchema } from "../types/command-args"; +import { handleCommandError } from "../utils/error"; export function initCommand(program: Command) { const initService: InitService = new DefaultInitService(); @@ -24,8 +25,7 @@ export function initCommand(program: Command) { const validatedOptions = InitCommandSchema.parse(options); await initService.init(validatedOptions); } catch (error) { - console.error("Error initializing project:", error); - process.exit(1); + handleCommandError(error); } }); } diff --git a/cli/src/types/command-args.ts b/cli/src/types/command-args.ts index eb39eee..8cf85df 100644 --- a/cli/src/types/command-args.ts +++ b/cli/src/types/command-args.ts @@ -32,8 +32,8 @@ export const GenerateArgsSchema = z.object({ // ############################################################ export const InitCommandSchema = z.object({ - template: z.string().optional(), - dir: z.string().optional(), + template: z.string().min(1).optional(), + dir: z.string().min(1).optional(), list: z.boolean().optional(), }); diff --git a/cli/src/utils/error.ts b/cli/src/utils/error.ts new file mode 100644 index 0000000..0a9402d --- /dev/null +++ b/cli/src/utils/error.ts @@ -0,0 +1,16 @@ +import { ZodError } from "zod"; +import chalk from "chalk"; + +export function handleCommandError(error: unknown): never { + if (error instanceof ZodError) { + console.error(chalk.red("Validation error:")); + error.errors.forEach(err => { + console.error(chalk.red(`- ${err.path.join(".")}: ${err.message}`)); + }); + } else if (error instanceof Error) { + console.error(chalk.red("Error:"), error.message); + } else { + console.error(chalk.red("An unexpected error occurred:"), error); + } + process.exit(1); +} From 0f5b045976fd5b20c49698ab5fd5c146c80b6226 Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 21:44:22 -0500 Subject: [PATCH 08/10] test: Ignores test files from test coverage --- cli/jest.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/jest.config.ts b/cli/jest.config.ts index ff0eca7..e554b2e 100644 --- a/cli/jest.config.ts +++ b/cli/jest.config.ts @@ -5,7 +5,7 @@ const config: Config = { testEnvironment: "node", testMatch: ["**/__tests__/**/*.test.ts"], collectCoverage: true, - collectCoverageFrom: ["src/**/*.ts", "!src/index.ts", "!src/**/*.d.ts"], + collectCoverageFrom: ["src/**/*.ts", "!src/index.ts", "!src/**/*.d.ts", "!src/__tests__/**/*.ts"], testPathIgnorePatterns: ["/node_modules/", "/__tests__/integration/"], }; From 3355f314926002d2297a8375839e75862c3d0df7 Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 21:56:49 -0500 Subject: [PATCH 09/10] docs: Updates the README --- cli/README.md | 248 ++++++++++++++++++-------------------------------- 1 file changed, 88 insertions(+), 160 deletions(-) diff --git a/cli/README.md b/cli/README.md index 0ee34a3..dd65fe0 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,224 +1,152 @@ # CommonGrants CLI -The CommonGrants CLI is a tool for working with the CommonGrants protocol. It's designed to simplify the process of defining, implementing, and validating CommonGrants APIs. +The CommonGrants CLI (`cg`) is a tool for working with the CommonGrants protocol. It simplifies the process of defining, implementing, and validating CommonGrants APIs. -## Initializing a project +> **Note**: This package is currently in alpha. The commands described below are mocked implementations that will be replaced with full functionality in future releases. -### User story - -As a developer implementing the CommonGrants protocol from scratch, I want to run a command that quickly sets up a new API, custom fields library, or other CommonGrants package, so that I don't have to spend a lot of time creating boilerplate code. - -### Developer experience - -Simplest use case: Developer runs `cg init` and is prompted to select from a list of templates. +## Installation ```bash -cg init -``` - -#### Additional Features -- Pass a `--template` flag to create a project using a predefined CommonGrants template. -- Use `--dir` to specify a target directory for the generated project. -- Run `cg init --list` to display available templates without starting initialization. - -#### Example Usage -```bash -# Initialize a new project interactively -cg init - -# Initialize a new project using a specific template -cg init --template grants-api - -# Initialize a new project in a custom directory -cg init --template grants-api --dir ./my-grants-project +# Install globally +npm install -g @common-grants/cli -# List available templates before choosing one -cg init --list +# Or use with npx +npx @common-grants/cli ``` -### Technical details -- If the user doesn't pass a `--template` flag to the command, it should prompt users with a list of optional templates. -- This command should be a thin wrapper for the `tsp init` function so that users can also pass paths or URLs to valid TypeSpec templates to create their own templates. - ---- - -## Previewing an OpenAPI spec - -### User story +## Usage -As a developer working on a CommonGrants API, I want to preview my OpenAPI specification using Swagger or Redocly, so that I can quickly inspect my API documentation. - -### Developer experience - -Simplest use case: Preview an OpenAPI spec in Swagger UI. +View available commands and options: ```bash -cg preview spec +cg --help ``` -#### Additional Features -- Allow choosing a preview tool (`--ui swagger` or `--ui redocly`). -- Open a local preview server for interactive exploration. +Output: -#### Example Usage -```bash -# Preview an OpenAPI spec with Swagger UI -cg preview spec grants-api.tsp --ui swagger - -# Preview an OpenAPI spec with Redocly -cg preview spec grants-api.tsp --ui redocly ``` +Usage: cg [options] [command] -### Technical details -- The command should generate an OpenAPI spec from the TypeSpec project and serve it using Swagger UI or Redocly. -- Defaults to Swagger UI if no `--ui` option is specified. - ---- - -## Adding a custom field - -### User story - -As a developer defining a CommonGrants API, I want to add a new custom field with configurable options, so that I can extend the API schema easily. - -### Developer experience +CommonGrants CLI tools -Simplest use case: Add a custom field by specifying `name` and `type`. +Options: + -V, --version output the version number + -h, --help display help for command -```bash -cg add field +Commands: + init [options] Initialize a new CommonGrants project + preview Preview an OpenAPI specification + add field Add a custom field to the schema + check Validate APIs and specifications + generate Generate server or client code + help [command] display help for command ``` -#### Additional Features -- Provide an example value with `--example`. -- Add a description using `--description`. +## Development status -#### Example Usage -```bash -# Add a simple custom field -cg add field fundingAmount number +This CLI is currently in alpha stage with the following limitations: -# Add a custom field with an example value -cg add field fundingAmount number --example 100000 +- All commands are mocked and return simulated responses -# Add a custom field with a description -cg add field fundingAmount number --description "The total amount of funding available" -``` +The first round of releases will implement the following core: -### Technical details -- The command should append the new field to the appropriate schema definition in the TypeSpec project. -- If `--example` or `--description` is provided, they should be included as metadata in the schema definition. +- Basic project initialization +- Previewing an OpenAPI spec using Swagger UI or Redocly +- Validating an API specification against the CommonGrants standard ---- +Subsequent releases will add: -## Validating a CommonGrants API implementation +- An expanded set of templates +- Validating an API implementation against its specification +- Generating server and client code -### User story +## Anticipated features -As a developer implementing a CommonGrants API, I want to run a command that checks whether a given API matches an OpenAPI spec, so that I can catch inconsistencies between the spec and the implementation. +The following examples describe the anticipated features of the CLI, but these are not yet implemented and are subject to change. -### Developer experience - -```bash -cg check api -``` +### Initialize a Project -#### Additional Features -- Allow selecting the HTTP client for validation (e.g., `curl`, `httpx`). -- Provide an option to generate a report (`--report json` or `--report html`). -- Support authentication with `--auth` flag for APIs requiring credentials. +Create a new CommonGrants project from a template: -#### Example Usage ```bash -# Validate a running API against a spec -cg check api https://api.example.com grants-api.yaml +# Initialize interactively +cg init -# Validate using a different HTTP client -cg check api https://api.example.com grants-api.yaml --client httpx +# Use a specific template +cg init --template grants-api -# Validate an authenticated API -cg check api https://api.example.com grants-api.yaml --auth bearer:mytoken +# Initialize in a custom directory +cg init --template grants-api --dir ./my-grants-project -# Generate a validation report in JSON format -cg check api https://api.example.com grants-api.yaml --report json +# List available templates +cg init --list ``` -### Technical details -- This command should leverage existing tools that validate OpenAPI spec implementations, where possible. +### Preview OpenAPI Specification ---- +Preview an API specification using Swagger UI or Redocly: -## Generating server code +```bash +# Preview with Swagger UI (default) +cg preview spec.tsp -### User story +# Preview with Redocly +cg preview spec.tsp --ui redocly +``` -As a developer implementing a CommonGrants API, I want to run a command that auto-generates an API server interface from a specification, so that I can follow a pattern of specification-driven development and quickly build APIs from scratch using the CommonGrants library. +### Add Custom Fields -### Developer experience +Extend the API schema with custom fields: ```bash -cg generate server +# Add a basic field +cg add field fundingAmount number + +# Include example and description +cg add field fundingAmount number --example 100000 --description "Total funding available" ``` -#### Additional Features -- Allow specifying a language/framework with `--lang` (e.g., Python, Node.js). -- Enable plugin support for custom server code generation. -- Generate only specific components with `--only `. +### Validate API Implementation + +Check if an API implementation matches its specification: -#### Example Usage ```bash -# Generate a server using the default framework -cg generate server grants-api.tsp +# Basic validation +cg check api https://api.example.com spec.yaml -# Generate a server for a specific language or framework -cg generate server grants-api.tsp --lang python +# Generate validation report +cg check api https://api.example.com spec.yaml --report json -# Generate only controllers and routes -cg generate server grants-api.tsp --only controllers,routes +# Validate with authentication +cg check api https://api.example.com spec.yaml --auth bearer:token ``` -### Technical details -- This may require a combination of TypeSpec emitters and OpenAPI codegen. -- If an API framework isn't specified by the user via a flag, the CLI should prompt a user to choose one. -- Ideally, this entry point would be designed to support plugins for custom server code generators. +### Generate Server Code ---- +Generate API server code from a specification: -## Generating client code - -### User story - -As a developer consuming a CommonGrants API, I want to run a command that generates client code from an API spec, so that I don't have to manually set up the code to work with that API. +```bash +# Generate with default settings +cg generate server spec.tsp -### Developer experience +# Specify language/framework +cg generate server spec.tsp --lang python -```bash -cg generate client +# Generate specific components +cg generate server spec.tsp --only controllers,routes ``` -#### Additional Features -- Support multiple output formats (`--output `). -- Allow targeting specific programming languages (`--lang`). -- Optionally include API documentation with `--docs`. +### Generate Client Code -#### Example Usage -```bash -# Generate a client SDK from a spec -cg generate client grants-api.tsp +Generate client SDKs from an API specification: -# Generate a client SDK for TypeScript -cg generate client grants-api.tsp --lang typescript +```bash +# Generate default client +cg generate client spec.tsp -# Save generated client SDK in a custom directory -cg generate client grants-api.tsp --output ./sdk +# Generate for specific language +cg generate client spec.tsp --lang typescript -# Include API documentation -cg generate client grants-api.tsp --docs +# Include documentation +cg generate client spec.tsp --docs ``` - -### Technical details -- This may require a combination of TypeSpec emitters and OpenAPI codegen. -- If a client framework isn't specified by the user via a flag, the CLI should prompt a user to choose one. -- Ideally, this entry point would be designed to support plugins for custom client code generators. - - From 1b3bd50e092c733fe501e86e6f67c7479e71e9ac Mon Sep 17 00:00:00 2001 From: widal001 Date: Wed, 5 Feb 2025 22:07:48 -0500 Subject: [PATCH 10/10] build: Updates files included in npm package --- cli/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cli/package.json b/cli/package.json index fb87a8f..a2656c5 100644 --- a/cli/package.json +++ b/cli/package.json @@ -4,6 +4,11 @@ "license": "CC0-1.0", "description": "The CommonGrants protocol CLI tool", "main": "dist/index.js", + "files": [ + "dist", + "lib", + "!dist/__tests__" + ], "bin": { "cg": "./dist/index.js" },