UNPKG

@apistudio/apim-cli

Version:

CLI for API Management Products

207 lines (174 loc) 6.4 kB
// import fs from "fs"; // import path from "path"; // import yaml from "js-yaml"; // type JSONSchema = { // type?: string; // properties?: Record<string, JSONSchema>; // items?: JSONSchema; // required?: string[]; // enum?: string[]; // description?: string; // [key: string]: any; // }; // function schemaToSpectralRules(schema: JSONSchema, basePath = "$"): Record<string, any> { // const rules: Record<string, any> = {}; // if (schema.type === "object" && schema.properties) { // for (const [prop, propSchema] of Object.entries<JSONSchema>(schema.properties)) { // const ruleName = `valid-${prop.replace(/[^a-zA-Z0-9]/g, "-")}`; // rules[ruleName] = { // description: propSchema.description || `Validate ${prop} at ${basePath}.${prop}`, // given: `${basePath}.${prop}`, // severity: "error", // then: { // function: "schema", // functionOptions: { // schema: propSchema, // }, // }, // }; // if (propSchema.type === "object" || propSchema.type === "array") { // Object.assign(rules, schemaToSpectralRules(propSchema, `${basePath}.${prop}`)); // } // } // } // if (schema.type === "array" && schema.items) { // Object.assign(rules, schemaToSpectralRules(schema.items, `${basePath}[*]`)); // } // return rules; // } // function generateRuleset(schemaPath: string) { // const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8")) as JSONSchema; // const rules = schemaToSpectralRules(schema); // const ruleset = { // extends: ["./global-assets-rule.spectral.yaml"], // rules, // }; // const outFile = path.join( // path.dirname(schemaPath), // path.basename(schemaPath, path.extname(schemaPath)) + "-spectral.ruleset.yaml" // ); // fs.writeFileSync(outFile, yaml.dump(ruleset, { noRefs: true })); // console.log(`Spectral ruleset generated: ${outFile}`); // } // if (process.argv.length < 3) { // console.error("Usage: ts-node generate-ruleset.ts <schema-file>"); // process.exit(1); // } // generateRuleset(process.argv[2]); // generator.ts import fs from "fs"; import path from "path"; import yaml from "js-yaml"; export interface GenerationOptions { specPath: string; outputRoot: string; } type ValidationSpec = Record<string, any>; /** * Recursively extract x-validation rules from schema */ function extractValidations(schema: any, parentPath: string[] = []): Record<string, ValidationSpec> { let validations: Record<string, ValidationSpec> = {}; if (schema && typeof schema === "object") { // If node has x-validation if (schema["x-validation"]) { validations[parentPath.join(".") || "root"] = schema["x-validation"]; } // Check properties recursively if (schema.properties) { for (const [prop, propSchema] of Object.entries<any>(schema.properties)) { const childPath = [...parentPath, prop]; validations = { ...validations, ...extractValidations(propSchema, childPath), }; } } } return validations; } /** * Generate spectral ruleset YAML */ function generateRuleset(validations: Record<string, ValidationSpec>, functionsDir: string) { const rules: Record<string, any> = {}; const functions: string[] = []; for (const [pathKey, validationObj] of Object.entries(validations)) { for (const [fnName, fnConfig] of Object.entries<any>(validationObj)) { const ruleName = `${fnName}-${pathKey.replace(/\./g, "_")}`; // Reference the function file const fnFile = `./functions/${fnName}.ts`; if (!functions.includes(fnFile)) { functions.push(fnFile); } rules[ruleName] = { description: `Validation for ${pathKey} using ${fnName}`, message: `Field ${pathKey} failed validation: ${fnName}`, given: `$..${pathKey.split(".").join(".properties.")}`, then: { function: fnName, functionOptions: fnConfig, }, }; } } return { extends: ["spectral:oas", "spectral:asyncapi", "spectral:json-schema-draft7"], functions: functions, rules: rules, }; } /** * Generate a stub function file for each validation */ function generateFunctionFiles(validations: Record<string, ValidationSpec>, functionsDir: string) { if (!fs.existsSync(functionsDir)) { fs.mkdirSync(functionsDir, { recursive: true }); } for (const validationObj of Object.values(validations)) { for (const [fnName] of Object.entries<any>(validationObj)) { const fnPath = path.join(functionsDir, `${fnName}.ts`); if (!fs.existsSync(fnPath)) { const fnContent = ` // ${fnName}.ts import { IFunction, IFunctionResult } from "@stoplight/spectral-core"; export const ${fnName}: IFunction = (targetVal, opts, ctx) => { const results: IFunctionResult[] = []; // TODO: Implement validation logic // Example: regex pattern check if (opts?.pattern) { const regex = new RegExp(opts.pattern); if (typeof targetVal === "string" && !regex.test(targetVal)) { results.push({ message: \`Value "\${targetVal}" does not match pattern \${opts.pattern}\`, }); } } return results; }; `.trimStart(); fs.writeFileSync(fnPath, fnContent, "utf-8"); console.log(`Created function file: ${fnPath}`); } } } } /** * Main generator */ export async function generateTypes({ specPath, outputRoot }: GenerationOptions) { const content = fs.readFileSync(specPath, "utf-8"); const schema = specPath.endsWith(".yaml") || specPath.endsWith(".yml") ? yaml.load(content) : JSON.parse(content); const validations = extractValidations(schema); const functionsDir = path.join(outputRoot, "functions"); // Generate ruleset const ruleset = generateRuleset(validations, functionsDir); const rulesetFile = path.join( outputRoot, `${path.basename(specPath, path.extname(specPath))}-spectral.ruleset.yaml` ); fs.writeFileSync(rulesetFile, yaml.dump(ruleset), "utf-8"); console.log(`Generated ruleset: ${rulesetFile}`); // Generate functions generateFunctionFiles(validations, functionsDir); }