@apistudio/apim-cli
Version:
CLI for API Management Products
207 lines (174 loc) • 6.4 kB
text/typescript
// 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);
}