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 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 c9cf645..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", @@ -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..45ebb11 100644 --- a/packages/ng-openapi/src/lib/cli.ts +++ b/packages/ng-openapi/src/lib/cli.ts @@ -41,20 +41,36 @@ 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)) { + throw new Error( + `Failed to parse ${extension || "specification"}. Supported formats are .json, .yaml, and .yml.` + ); + } +} + 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 +100,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 +115,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 +131,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..2189eca 100644 --- a/packages/ng-openapi/src/lib/generators/service/service.generator.ts +++ b/packages/ng-openapi/src/lib/generators/service/service.generator.ts @@ -17,13 +17,31 @@ 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]) => { @@ -273,13 +291,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 +313,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;