openapi-generator

安装量: 57
排名: #13086

安装

npx skills add https://github.com/patricio0312rev/skills --skill openapi-generator

OpenAPI Generator

Generate OpenAPI 3.0/3.1 specifications from your API codebase automatically.

Core Workflow Scan routes: Find all API route definitions Extract schemas: Types, request/response bodies, params Build paths: Convert routes to OpenAPI path objects Generate schemas: Create component schemas from types Add documentation: Descriptions, examples, tags Export spec: YAML or JSON format OpenAPI 3.1 Base Template openapi: 3.1.0 info: title: API Title version: 1.0.0 description: API description contact: email: api@example.com license: name: MIT url: https://opensource.org/licenses/MIT

servers: - url: http://localhost:3000/api description: Development - url: https://api.example.com description: Production

tags: - name: Users description: User management endpoints - name: Products description: Product catalog endpoints

paths: {}

components: schemas: {} securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT apiKey: type: apiKey in: header name: X-API-Key

security: - bearerAuth: []

TypeScript to OpenAPI Schema Converter // scripts/type-to-schema.ts import * as ts from "typescript";

interface OpenAPISchema { type?: string; properties?: Record; required?: string[]; items?: OpenAPISchema; $ref?: string; enum?: string[]; format?: string; description?: string; example?: unknown; }

function typeToOpenAPISchema( checker: ts.TypeChecker, type: ts.Type ): OpenAPISchema { // Handle primitives if (type.flags & ts.TypeFlags.String) { return { type: "string" }; } if (type.flags & ts.TypeFlags.Number) { return { type: "number" }; } if (type.flags & ts.TypeFlags.Boolean) { return { type: "boolean" }; }

// Handle arrays if (checker.isArrayType(type)) { const elementType = (type as ts.TypeReference).typeArguments?.[0]; return { type: "array", items: elementType ? typeToOpenAPISchema(checker, elementType) : {}, }; }

// Handle object types if (type.flags & ts.TypeFlags.Object) { const properties: Record = {}; const required: string[] = [];

type.getProperties().forEach((prop) => {
  const propType = checker.getTypeOfSymbolAtLocation(
    prop,
    prop.valueDeclaration!
  );
  properties[prop.name] = typeToOpenAPISchema(checker, propType);

  // Check if required (no ? modifier)
  if (!(prop.flags & ts.SymbolFlags.Optional)) {
    required.push(prop.name);
  }
});

return {
  type: "object",
  properties,
  required: required.length > 0 ? required : undefined,
};

}

// Handle union types (enums) if (type.isUnion()) { const enumValues = type.types .filter((t) => t.isStringLiteral()) .map((t) => (t as ts.StringLiteralType).value);

if (enumValues.length > 0) {
  return { type: "string", enum: enumValues };
}

}

return {}; }

Express Route Scanner with JSDoc // scripts/express-openapi.ts import * as fs from "fs"; import * as path from "path"; import { parse } from "@babel/parser"; import traverse from "@babel/traverse";

interface RouteMetadata { method: string; path: string; summary?: string; description?: string; tags?: string[]; requestBody?: object; responses?: Record; parameters?: object[]; security?: object[]; }

function extractJSDocMetadata(comments: string): Partial { const metadata: Partial = {};

// @summary const summaryMatch = comments.match(/@summary\s+(.+)/); if (summaryMatch) metadata.summary = summaryMatch[1].trim();

// @description const descMatch = comments.match(/@description\s+(.+)/); if (descMatch) metadata.description = descMatch[1].trim();

// @tags const tagsMatch = comments.match(/@tags\s+(.+)/); if (tagsMatch) metadata.tags = tagsMatch[1].split(",").map((t) => t.trim());

return metadata; }

function scanExpressWithOpenAPI(sourceDir: string): RouteMetadata[] { const routes: RouteMetadata[] = [];

// Implementation: traverse files and extract routes with JSDoc comments // Similar to postman generator but with OpenAPI-specific metadata

return routes; }

OpenAPI Path Generator // scripts/generate-openapi.ts import * as yaml from "js-yaml";

interface OpenAPISpec { openapi: string; info: object; servers: object[]; paths: Record; components: { schemas: Record; securitySchemes?: object; }; tags?: object[]; security?: object[]; }

function generateOpenAPISpec( routes: RouteMetadata[], options: { title: string; version: string; description?: string; servers: { url: string; description: string }[]; } ): OpenAPISpec { const spec: OpenAPISpec = { openapi: "3.1.0", info: { title: options.title, version: options.version, description: options.description, }, servers: options.servers, paths: {}, components: { schemas: {}, securitySchemes: { bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT", }, }, }, tags: [], };

// Collect unique tags const tagSet = new Set();

// Generate paths for (const route of routes) { const openAPIPath = route.path.replace(/:(\w+)/g, "{$1}");

if (!spec.paths[openAPIPath]) {
  spec.paths[openAPIPath] = {};
}

spec.paths[openAPIPath][route.method.toLowerCase()] = {
  summary: route.summary || `${route.method} ${route.path}`,
  description: route.description,
  tags: route.tags || [extractResourceTag(route.path)],
  parameters: generateParameters(route),
  requestBody: route.requestBody,
  responses: route.responses || generateDefaultResponses(route.method),
  security: route.security,
};

// Collect tags
(route.tags || [extractResourceTag(route.path)]).forEach((t) =>
  tagSet.add(t)
);

}

// Add tags to spec spec.tags = Array.from(tagSet).map((name) => ({ name }));

return spec; }

function generateParameters(route: RouteMetadata): object[] { const params: object[] = [];

// Extract path parameters const pathParamRegex = /:(\w+)/g; let match;

while ((match = pathParamRegex.exec(route.path)) !== null) { params.push({ name: match[1], in: "path", required: true, schema: { type: "string" }, description: ${match[1]} parameter, }); }

return params; }

function generateDefaultResponses(method: string): object { const responses: Record = { "200": { description: "Successful response", content: { "application/json": { schema: { type: "object" }, }, }, }, "400": { description: "Bad request", content: { "application/json": { schema: { $ref: "#/components/schemas/Error" }, }, }, }, "401": { description: "Unauthorized", }, "404": { description: "Not found", }, "500": { description: "Internal server error", }, };

if (method === "POST") { responses["201"] = { description: "Created successfully", content: { "application/json": { schema: { type: "object" }, }, }, }; }

if (method === "DELETE") { responses["204"] = { description: "Deleted successfully", }; }

return responses; }

function extractResourceTag(path: string): string { const parts = path.split("/").filter(Boolean); return parts[0] || "default"; }

Common Schema Components components: schemas: Error: type: object required: - code - message properties: code: type: string example: "VALIDATION_ERROR" message: type: string example: "Invalid request data" details: type: object additionalProperties: type: array items: type: string

Pagination:
  type: object
  properties:
    page:
      type: integer
      minimum: 1
      example: 1
    limit:
      type: integer
      minimum: 1
      maximum: 100
      example: 10
    total:
      type: integer
      example: 156
    total_pages:
      type: integer
      example: 16

PaginatedResponse:
  type: object
  properties:
    success:
      type: boolean
      example: true
    data:
      type: array
      items: {}
    meta:
      $ref: "#/components/schemas/Pagination"

User:
  type: object
  required:
    - id
    - email
    - name
  properties:
    id:
      type: string
      format: uuid
      example: "123e4567-e89b-12d3-a456-426614174000"
    email:
      type: string
      format: email
      example: "user@example.com"
    name:
      type: string
      example: "John Doe"
    created_at:
      type: string
      format: date-time
      example: "2024-01-15T10:30:00Z"

CreateUserRequest:
  type: object
  required:
    - email
    - name
    - password
  properties:
    email:
      type: string
      format: email
    name:
      type: string
      minLength: 2
      maxLength: 100
    password:
      type: string
      format: password
      minLength: 8

Fastify Integration // Fastify with @fastify/swagger import Fastify from "fastify"; import swagger from "@fastify/swagger"; import swaggerUi from "@fastify/swagger-ui";

const fastify = Fastify({ logger: true });

await fastify.register(swagger, { openapi: { info: { title: "My API", version: "1.0.0", }, servers: [{ url: "http://localhost:3000" }], }, });

await fastify.register(swaggerUi, { routePrefix: "/docs", });

// Routes with schema fastify.get( "/users/:id", { schema: { params: { type: "object", properties: { id: { type: "string", format: "uuid" }, }, required: ["id"], }, response: { 200: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, email: { type: "string" }, }, }, }, }, }, async (request, reply) => { // Handler } );

NestJS Integration // NestJS with @nestjs/swagger import { Controller, Get, Post, Body, Param } from "@nestjs/common"; import { ApiTags, ApiOperation, ApiResponse, ApiBody } from "@nestjs/swagger";

@ApiTags("users") @Controller("users") export class UsersController { @Get() @ApiOperation({ summary: "Get all users" }) @ApiResponse({ status: 200, description: "List of users", type: [UserDto] }) findAll() { // Implementation }

@Get(":id") @ApiOperation({ summary: "Get user by ID" }) @ApiResponse({ status: 200, description: "User found", type: UserDto }) @ApiResponse({ status: 404, description: "User not found" }) findOne(@Param("id") id: string) { // Implementation }

@Post() @ApiOperation({ summary: "Create new user" }) @ApiBody({ type: CreateUserDto }) @ApiResponse({ status: 201, description: "User created", type: UserDto }) create(@Body() createUserDto: CreateUserDto) { // Implementation } }

CLI Script

!/usr/bin/env node

// scripts/openapi-gen.ts import * as fs from "fs"; import * as yaml from "js-yaml"; import { program } from "commander";

program .name("openapi-gen") .description("Generate OpenAPI specification from API routes") .option("-f, --framework ", "Framework (express|nextjs|fastify)", "express") .option("-s, --source ", "Source directory", "./src") .option("-o, --output ", "Output file", "./openapi.yaml") .option("-t, --title ", "API title", "My API") .option("-v, --version ", "API version", "1.0.0") .option("--json", "Output as JSON instead of YAML") .parse();

const options = program.opts();

async function main() { const routes = await scanRoutes(options.framework, options.source);

const spec = generateOpenAPISpec(routes, { title: options.title, version: options.version, servers: [ { url: "http://localhost:3000/api", description: "Development" }, ], });

const output = options.json ? JSON.stringify(spec, null, 2) : yaml.dump(spec, { lineWidth: -1 });

fs.writeFileSync(options.output, output); console.log(Generated ${options.output} with ${routes.length} endpoints); }

main();

Validation Script // scripts/validate-openapi.ts import SwaggerParser from "@apidevtools/swagger-parser";

async function validateSpec(specPath: string): Promise { try { const api = await SwaggerParser.validate(specPath); console.log(API name: ${api.info.title}, Version: ${api.info.version}); console.log("OpenAPI specification is valid!"); } catch (err) { console.error("Validation failed:", err.message); process.exit(1); } }

Best Practices Use $ref: Reference shared schemas to avoid duplication Add examples: Include realistic examples for all schemas Document errors: Define all possible error responses Use tags: Organize endpoints by resource/feature Version control: Commit spec to repository Validate: Run validation before publishing Generate SDKs: Use openapi-generator for client SDKs Serve UI: Host Swagger UI or Redoc for documentation Output Checklist All routes converted to OpenAPI paths Path parameters use {param} syntax Request bodies defined with schemas Response schemas for all status codes Common schemas in components/schemas Security schemes configured Tags applied to all endpoints Examples included for schemas Spec validates without errors YAML/JSON exported successfully

返回排行榜