162 lines
6.2 KiB
Plaintext
162 lines
6.2 KiB
Plaintext
import { performance } from "node:perf_hooks";
|
|
import * as changeCase from "change-case";
|
|
import ts from "typescript";
|
|
import { addJSDocComment, NEVER, QUESTION_TOKEN, tsModifiers, tsPropertyIndex } from "../lib/ts.js";
|
|
import { createRef, debug, getEntries } from "../lib/utils.js";
|
|
import type { ComponentsObject, GlobalContext, SchemaObject, TransformNodeOptions } from "../types.js";
|
|
import transformHeaderObject from "./header-object.js";
|
|
import transformParameterObject from "./parameter-object.js";
|
|
import transformPathItemObject from "./path-item-object.js";
|
|
import transformRequestBodyObject from "./request-body-object.js";
|
|
import transformResponseObject from "./response-object.js";
|
|
import transformSchemaObject from "./schema-object.js";
|
|
|
|
/**
|
|
* Determines if a schema object represents an enum type to prevent duplicate exports
|
|
* when using --root-types and --enum flags together.
|
|
*
|
|
* When both flags are enabled:
|
|
* - --enum flag generates TypeScript enums at the bottom of the file
|
|
* - --root-types flag would normally also export these as root type aliases
|
|
* - This results in duplicate exports (both enum and type alias for the same schema)
|
|
*
|
|
* This function identifies enum schemas so they can be excluded from root type generation,
|
|
* allowing only the TypeScript enum to be generated.
|
|
*
|
|
* @param schema The schema object to check
|
|
* @returns true if the schema represents an enum type
|
|
*/
|
|
export function isEnumSchema(schema: unknown): boolean {
|
|
return (
|
|
typeof schema === "object" &&
|
|
schema !== null &&
|
|
!Array.isArray(schema) &&
|
|
"enum" in schema &&
|
|
Array.isArray((schema as any).enum) &&
|
|
(!("type" in schema) || (schema as any).type !== "object") &&
|
|
!("properties" in schema) &&
|
|
!("additionalProperties" in schema)
|
|
);
|
|
}
|
|
|
|
type ComponentTransforms = keyof Omit<ComponentsObject, "examples" | "securitySchemes" | "links" | "callbacks">;
|
|
|
|
const transformers: Record<ComponentTransforms, (node: any, options: TransformNodeOptions) => ts.TypeNode> = {
|
|
schemas: transformSchemaObject,
|
|
responses: transformResponseObject,
|
|
parameters: transformParameterObject,
|
|
requestBodies: transformRequestBodyObject,
|
|
headers: transformHeaderObject,
|
|
pathItems: transformPathItemObject,
|
|
};
|
|
|
|
/**
|
|
* Transform the ComponentsObject (4.8.7)
|
|
* @see https://spec.openapis.org/oas/latest.html#components-object
|
|
*/
|
|
export default function transformComponentsObject(componentsObject: ComponentsObject, ctx: GlobalContext): ts.Node[] {
|
|
const type: ts.TypeElement[] = [];
|
|
const rootTypeAliases: { [key: string]: ts.TypeAliasDeclaration } = {};
|
|
for (const key of Object.keys(transformers) as ComponentTransforms[]) {
|
|
const componentT = performance.now();
|
|
|
|
const items: ts.TypeElement[] = [];
|
|
if (componentsObject[key]) {
|
|
for (const [name, item] of getEntries<SchemaObject>(componentsObject[key], ctx)) {
|
|
let subType = transformers[key](item, {
|
|
path: createRef(["components", key, name]),
|
|
schema: item,
|
|
ctx,
|
|
});
|
|
|
|
let hasQuestionToken = false;
|
|
if (ctx.transform) {
|
|
const result = ctx.transform(item, {
|
|
path: createRef(["components", key, name]),
|
|
schema: item,
|
|
ctx,
|
|
});
|
|
if (result) {
|
|
if ("schema" in result) {
|
|
subType = result.schema;
|
|
hasQuestionToken = result.questionToken;
|
|
} else {
|
|
subType = result;
|
|
}
|
|
}
|
|
}
|
|
|
|
const property = ts.factory.createPropertySignature(
|
|
/* modifiers */ tsModifiers({ readonly: ctx.immutable }),
|
|
/* name */ tsPropertyIndex(name),
|
|
/* questionToken */ hasQuestionToken ? QUESTION_TOKEN : undefined,
|
|
/* type */ subType,
|
|
);
|
|
addJSDocComment(item as unknown as any, property);
|
|
items.push(property);
|
|
|
|
if (ctx.rootTypes) {
|
|
// Skip enum schemas when generating root types to prevent duplication (only when --enum flag is enabled)
|
|
const shouldSkipEnumSchema = ctx.enum && key === "schemas" && isEnumSchema(item);
|
|
|
|
if (!shouldSkipEnumSchema) {
|
|
const componentKey = changeCase.pascalCase(singularizeComponentKey(key));
|
|
const componentName = ctx.rootTypesKeepCasing && key === "schemas" ? name : changeCase.pascalCase(name);
|
|
let aliasName = `${componentKey}${componentName}`;
|
|
|
|
// Add counter suffix (e.g. "_2") if conflict in name
|
|
let conflictCounter = 1;
|
|
|
|
while (rootTypeAliases[aliasName] !== undefined) {
|
|
conflictCounter++;
|
|
aliasName = `${componentKey}${componentName}_${conflictCounter}`;
|
|
}
|
|
const ref = ts.factory.createTypeReferenceNode(`components['${key}']['${name}']`);
|
|
if (ctx.rootTypesNoSchemaPrefix && key === "schemas") {
|
|
aliasName = aliasName.replace(componentKey, "");
|
|
}
|
|
const typeAlias = ts.factory.createTypeAliasDeclaration(
|
|
/* modifiers */ tsModifiers({ export: true }),
|
|
/* name */ aliasName,
|
|
/* typeParameters */ undefined,
|
|
/* type */ ref,
|
|
);
|
|
rootTypeAliases[aliasName] = typeAlias;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
type.push(
|
|
ts.factory.createPropertySignature(
|
|
/* modifiers */ undefined,
|
|
/* name */ tsPropertyIndex(key),
|
|
/* questionToken */ undefined,
|
|
/* type */ items.length ? ts.factory.createTypeLiteralNode(items) : NEVER,
|
|
),
|
|
);
|
|
|
|
debug(`Transformed components → ${key}`, "ts", performance.now() - componentT);
|
|
}
|
|
|
|
// Extract root types
|
|
let rootTypes: ts.TypeAliasDeclaration[] = [];
|
|
if (ctx.rootTypes) {
|
|
rootTypes = Object.keys(rootTypeAliases).map((k) => rootTypeAliases[k]);
|
|
}
|
|
|
|
return [ts.factory.createTypeLiteralNode(type), ...rootTypes];
|
|
}
|
|
|
|
export function singularizeComponentKey(
|
|
key: `x-${string}` | "schemas" | "responses" | "parameters" | "requestBodies" | "headers" | "pathItems",
|
|
): string {
|
|
switch (key) {
|
|
// Handle special singular case
|
|
case "requestBodies":
|
|
return "requestBody";
|
|
// Default to removing the "s"
|
|
default:
|
|
return key.slice(0, -1);
|
|
}
|
|
}
|