From ea7633ef90376d48301281bb920ba0f4ff1b302c Mon Sep 17 00:00:00 2001 From: tareq Date: Sun, 3 Aug 2025 21:16:21 +0200 Subject: [PATCH 1/5] feat: add YAML support for OpenAPI specifications and enhance input validation --- package-lock.json | 28 ++++++++-- package.json | 1 + packages/ng-openapi/package.json | 3 +- packages/ng-openapi/src/lib/cli.ts | 37 +++++++++++--- .../ng-openapi/src/lib/core/swagger-parser.ts | 51 ++++++++++++++++++- .../generators/service/service.generator.ts | 36 ++++++++++--- .../ng-openapi/src/lib/types/swagger.types.ts | 1 + 7 files changed, 134 insertions(+), 23 deletions(-) diff --git a/package-lock.json b/package-lock.json index cbd567a..3211153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@swc/cli": "~0.6.0", "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", + "@types/js-yaml": "^4.0.9", "@types/node": "18.16.9", "eslint": "^9.8.0", "eslint-config-prettier": "^10.0.0", @@ -4609,6 +4610,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -6408,7 +6416,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-flatten": { @@ -15012,11 +15019,12 @@ } }, "packages/ng-openapi": { - "version": "0.0.8", + "version": "0.0.35", "license": "MIT", "dependencies": { "@types/swagger-schema-official": "^2.0.25", "commander": "^14.0.0", + "js-yaml": "^4.1.0", "ts-morph": "^26.0.0", "ts-node": "^10.9.2", "typescript": "^5.8.3" @@ -15033,8 +15041,8 @@ "url": "https://github.com/sponsors/ng-openapi" }, "peerDependencies": { - "@angular/common": ">=20", - "@angular/core": ">=20" + "@angular/common": ">=15", + "@angular/core": ">=15" }, "peerDependenciesMeta": { "@angular/common": { @@ -15053,6 +15061,18 @@ "engines": { "node": ">=20" } + }, + "packages/ng-openapi/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } } } } diff --git a/package.json b/package.json index 23676d2..bf540d7 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@swc/cli": "~0.6.0", "@swc/core": "~1.5.7", "@swc/helpers": "~0.5.11", + "@types/js-yaml": "^4.0.9", "@types/node": "18.16.9", "eslint": "^9.8.0", "eslint-config-prettier": "^10.0.0", diff --git a/packages/ng-openapi/package.json b/packages/ng-openapi/package.json index 46b9d93..0699f50 100644 --- a/packages/ng-openapi/package.json +++ b/packages/ng-openapi/package.json @@ -61,7 +61,8 @@ "ts-morph": "^26.0.0", "ts-node": "^10.9.2", "typescript": "^5.8.3", - "@types/swagger-schema-official": "^2.0.25" + "@types/swagger-schema-official": "^2.0.25", + "js-yaml": "^4.1.0" }, "peerDependencies": { "@angular/core": ">=15", diff --git a/packages/ng-openapi/src/lib/cli.ts b/packages/ng-openapi/src/lib/cli.ts index b0bb0a2..5c59c6f 100644 --- a/packages/ng-openapi/src/lib/cli.ts +++ b/packages/ng-openapi/src/lib/cli.ts @@ -41,20 +41,39 @@ async function loadConfigFile(configPath: string): Promise { } } +function validateInputFile(inputPath: string): void { + if (!fs.existsSync(inputPath)) { + throw new Error(`Input file not found: ${inputPath}`); + } + + const extension = path.extname(inputPath).toLowerCase(); + const supportedExtensions = [".json", ".yaml", ".yml"]; + + if (!supportedExtensions.includes(extension)) { + console.warn( + `Warning: File extension '${extension}' is not explicitly supported. Supported extensions: ${supportedExtensions.join( + ", " + )}` + ); + console.warn("The parser will attempt to auto-detect the format."); + } +} + async function generateFromOptions(options: any): Promise { try { if (options.config) { // Load configuration from file const config = await loadConfigFile(options.config); + + // Validate the input file from config + validateInputFile(path.resolve(config.input)); + await generateFromConfig(config); } else if (options.input) { // Use command line options const inputPath = path.resolve(options.input); - if (!fs.existsSync(inputPath)) { - console.error(`Error: Input file not found: ${inputPath}`); - process.exit(1); - } + validateInputFile(inputPath); const config: GeneratorConfig = { input: inputPath, @@ -84,10 +103,10 @@ async function generateFromOptions(options: any): Promise { // Main command with options (allows: ng-openapi -c config.ts) program .name("ng-openapi") - .description("Generate Angular services and types from Swagger/OpenAPI spec") + .description("Generate Angular services and types from OpenAPI/Swagger specifications (JSON, YAML, YML)") .version(packageJson.version) .option("-c, --config ", "Path to configuration file") - .option("-i, --input ", "Path to Swagger/OpenAPI specification file") + .option("-i, --input ", "Path to OpenAPI/Swagger specification file (.json, .yaml, .yml)") .option("-o, --output ", "Output directory", "./src/generated") .option("--types-only", "Generate only TypeScript interfaces") .option("--date-type ", "Date type to use (string | Date)", "Date") @@ -99,9 +118,9 @@ program program .command("generate") .alias("gen") - .description("Generate code from Swagger specification") + .description("Generate code from OpenAPI/Swagger specification") .option("-c, --config ", "Path to configuration file") - .option("-i, --input ", "Path to Swagger/OpenAPI specification file") + .option("-i, --input ", "Path to OpenAPI/Swagger specification file (.json, .yaml, .yml)") .option("-o, --output ", "Output directory", "./src/generated") .option("--types-only", "Generate only TypeScript interfaces") .option("--date-type ", "Date type to use (string | Date)", "Date") @@ -115,6 +134,8 @@ program.on("--help", () => { console.log("Examples:"); console.log(" $ ng-openapi -c ./openapi.config.ts"); console.log(" $ ng-openapi -i ./swagger.json -o ./src/api"); + console.log(" $ ng-openapi -i ./openapi.yaml -o ./src/api"); + console.log(" $ ng-openapi -i ./api-spec.yml -o ./src/api"); console.log(" $ ng-openapi generate -c ./openapi.config.ts"); console.log(" $ ng-openapi generate -i ./api.yaml --types-only"); }); diff --git a/packages/ng-openapi/src/lib/core/swagger-parser.ts b/packages/ng-openapi/src/lib/core/swagger-parser.ts index 161a811..09c30af 100644 --- a/packages/ng-openapi/src/lib/core/swagger-parser.ts +++ b/packages/ng-openapi/src/lib/core/swagger-parser.ts @@ -1,12 +1,14 @@ import * as fs from "fs"; +import * as path from "path"; +import * as yaml from "js-yaml"; import { SwaggerDefinition, SwaggerSpec } from "../types"; export class SwaggerParser { - private spec: SwaggerSpec; + private readonly spec: SwaggerSpec; constructor(swaggerPath: string) { const swaggerContent = fs.readFileSync(swaggerPath, "utf8"); - this.spec = JSON.parse(swaggerContent); + this.spec = this.parseSpecFile(swaggerContent, swaggerPath); } getDefinitions(): Record { @@ -29,4 +31,49 @@ export class SwaggerParser { getAllDefinitionNames(): string[] { return Object.keys(this.getDefinitions()); } + + getSpec(): SwaggerSpec { + return this.spec; + } + + getPaths(): Record { + return this.spec.paths || {}; + } + + isValidSpec(): boolean { + return !!( + (this.spec.swagger && this.spec.swagger.startsWith("2.")) || + (this.spec.openapi && this.spec.openapi.startsWith("3.")) + ); + } + + getSpecVersion(): { type: "swagger" | "openapi"; version: string } | null { + if (this.spec.swagger) { + return { type: "swagger", version: this.spec.swagger }; + } + if (this.spec.openapi) { + return { type: "openapi", version: this.spec.openapi }; + } + return null; + } + + private parseSpecFile(content: string, filePath: string): SwaggerSpec { + const extension = path.extname(filePath).toLowerCase(); + + switch (extension) { + case ".json": + return JSON.parse(content); + + case ".yaml": + case ".yml": + return yaml.load(content) as SwaggerSpec; + + default: + throw new Error( + `Failed to parse ${ + extension || "specification" + } file: ${filePath}. Supported formats are .json, .yaml, and .yml.` + ); + } + } } diff --git a/packages/ng-openapi/src/lib/generators/service/service.generator.ts b/packages/ng-openapi/src/lib/generators/service/service.generator.ts index bd9819f..7fb992c 100644 --- a/packages/ng-openapi/src/lib/generators/service/service.generator.ts +++ b/packages/ng-openapi/src/lib/generators/service/service.generator.ts @@ -17,18 +17,38 @@ export class ServiceGenerator { this.config = config; this.project = project; this.parser = new SwaggerParser(swaggerPath); - this.spec = JSON.parse(require("fs").readFileSync(swaggerPath, "utf8")); + + this.spec = this.parser.getSpec(); + + // Validate the spec + if (!this.parser.isValidSpec()) { + const versionInfo = this.parser.getSpecVersion(); + throw new Error( + `Invalid or unsupported specification format. ` + + `Expected OpenAPI 3.x or Swagger 2.x. ` + + `${versionInfo ? `Found: ${versionInfo.type} ${versionInfo.version}` : "No version info found"}` + ); + } + this.methodGenerator = new ServiceMethodGenerator(config); } generate(outputRoot: string): void { const outputDir = path.join(outputRoot, "services"); const paths = this.extractPaths(); + + if (paths.length === 0) { + console.warn("No API paths found in the specification"); + return; + } + const controllerGroups = this.groupPathsByController(paths); Object.entries(controllerGroups).forEach(([controllerName, operations]) => { this.generateServiceFile(controllerName, operations, outputDir); }); + + console.log(`Generated ${Object.keys(controllerGroups).length} service(s) from ${paths.length} path(s)`); } private extractPaths(): PathInfo[] { @@ -273,13 +293,13 @@ export class ServiceGenerator { { name: "existingContext", type: "HttpContext", - hasQuestionToken: true - } + hasQuestionToken: true, + }, ], returnType: "HttpContext", statements: ` const context = existingContext || new HttpContext(); -return context.set(this.clientContextToken, '${this.config.clientName || 'default'}');` +return context.set(this.clientContextToken, '${this.config.clientName || "default"}');`, }); // Generate methods for each operation @@ -295,14 +315,14 @@ return context.set(this.clientContextToken, '${this.config.clientName || 'defaul } private getClientContextTokenName(): string { - const clientName = this.config.clientName || 'default'; - const clientSuffix = clientName.toUpperCase().replace(/[^A-Z0-9]/g, '_'); + const clientName = this.config.clientName || "default"; + const clientSuffix = clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `CLIENT_CONTEXT_TOKEN_${clientSuffix}`; } private getBasePathTokenName(): string { - const clientName = this.config.clientName || 'default'; - const clientSuffix = clientName.toUpperCase().replace(/[^A-Z0-9]/g, '_'); + const clientName = this.config.clientName || "default"; + const clientSuffix = clientName.toUpperCase().replace(/[^A-Z0-9]/g, "_"); return `BASE_PATH_${clientSuffix}`; } diff --git a/packages/ng-openapi/src/lib/types/swagger.types.ts b/packages/ng-openapi/src/lib/types/swagger.types.ts index 28c0d8c..bb5ef34 100644 --- a/packages/ng-openapi/src/lib/types/swagger.types.ts +++ b/packages/ng-openapi/src/lib/types/swagger.types.ts @@ -79,6 +79,7 @@ export interface SwaggerDefinition { } export interface SwaggerSpec { + openapi: string; swagger: string; info: Info; externalDocs?: ExternalDocs | undefined; From 2125b8c02d85d4b28ed82bb1ae010e9f7c24f01e Mon Sep 17 00:00:00 2001 From: tareq Date: Sun, 3 Aug 2025 21:18:08 +0200 Subject: [PATCH 2/5] docs: update quick-start guide to include YAML file support --- docs/getting-started/quick-start.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/getting-started/quick-start.md b/docs/getting-started/quick-start.md index 88f0869..664f67a 100644 --- a/docs/getting-started/quick-start.md +++ b/docs/getting-started/quick-start.md @@ -10,6 +10,7 @@ Generate Angular services and TypeScript types from your OpenAPI specification. You need an OpenAPI/Swagger specification file: - JSON file (`swagger.json`, `openapi.json`) +- Yaml file (`swagger.yml`, `openapi.yaml`) ## Step 2: Generate API Client From c907e6893de22156a33fd367aae71f2410153e1f Mon Sep 17 00:00:00 2001 From: tareq Date: Sun, 3 Aug 2025 22:06:41 +0200 Subject: [PATCH 3/5] feature: remove redundant console log in service file generation --- .../ng-openapi/src/lib/generators/service/service.generator.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ng-openapi/src/lib/generators/service/service.generator.ts b/packages/ng-openapi/src/lib/generators/service/service.generator.ts index 7fb992c..2189eca 100644 --- a/packages/ng-openapi/src/lib/generators/service/service.generator.ts +++ b/packages/ng-openapi/src/lib/generators/service/service.generator.ts @@ -47,8 +47,6 @@ export class ServiceGenerator { Object.entries(controllerGroups).forEach(([controllerName, operations]) => { this.generateServiceFile(controllerName, operations, outputDir); }); - - console.log(`Generated ${Object.keys(controllerGroups).length} service(s) from ${paths.length} path(s)`); } private extractPaths(): PathInfo[] { From 8a94116e01b5f8e0d2547d592015f55afb5ec05c Mon Sep 17 00:00:00 2001 From: tareq Date: Sun, 3 Aug 2025 22:17:24 +0200 Subject: [PATCH 4/5] feature: improve error handling for unsupported file extensions in CLI --- packages/ng-openapi/src/lib/cli.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/ng-openapi/src/lib/cli.ts b/packages/ng-openapi/src/lib/cli.ts index 5c59c6f..45ebb11 100644 --- a/packages/ng-openapi/src/lib/cli.ts +++ b/packages/ng-openapi/src/lib/cli.ts @@ -50,12 +50,9 @@ function validateInputFile(inputPath: string): void { const supportedExtensions = [".json", ".yaml", ".yml"]; if (!supportedExtensions.includes(extension)) { - console.warn( - `Warning: File extension '${extension}' is not explicitly supported. Supported extensions: ${supportedExtensions.join( - ", " - )}` + throw new Error( + `Failed to parse ${extension || "specification"}. Supported formats are .json, .yaml, and .yml.` ); - console.warn("The parser will attempt to auto-detect the format."); } } From 663d76c563d2533f8e51624f81e48f2a3aaab443 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 3 Aug 2025 20:19:46 +0000 Subject: [PATCH 5/5] chore: release v0.0.39 --- packages/ng-openapi/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ng-openapi/package.json b/packages/ng-openapi/package.json index 3f18b76..daae4ae 100644 --- a/packages/ng-openapi/package.json +++ b/packages/ng-openapi/package.json @@ -1,6 +1,6 @@ { "name": "ng-openapi", - "version": "0.0.38", + "version": "0.0.39", "description": "Generate Angular services and TypeScript types from OpenAPI/Swagger specifications", "keywords": [ "angular",