UNPKG

@nestia/sdk

Version:

Nestia SDK and Swagger generator

265 lines (245 loc) 8.28 kB
import { OpenApi } from "@typia/interface"; import { MetadataAliasType, MetadataArrayType, MetadataObjectType, MetadataSchema, MetadataTupleType, } from "../../internal/legacy"; type JsonSchemaObject = OpenApi.IJsonSchema & Record<string, any>; export namespace SwaggerReadonlyArrayEmender { export const emend = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema | undefined; metadata: MetadataSchema; }): void => { const visited: IVisited = { aliases: new WeakSet(), arrays: new WeakSet(), objects: new WeakSet(), schemas: new WeakSet(), tuples: new WeakSet(), }; visitMetadata({ components: props.components, schema: props.schema, metadata: props.metadata, visited, }); }; } interface IVisited { aliases: WeakSet<MetadataAliasType>; arrays: WeakSet<MetadataArrayType>; objects: WeakSet<MetadataObjectType>; schemas: WeakSet<MetadataSchema>; tuples: WeakSet<MetadataTupleType>; } const visitMetadata = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema | undefined; metadata: MetadataSchema; visited: IVisited; }): void => { if (props.schema !== undefined && isReadonlyArrayLike(props.metadata)) setReadonlyArray(props.schema); if (props.visited.schemas.has(props.metadata)) return; props.visited.schemas.add(props.metadata); for (const array of props.metadata.arrays) { const type: MetadataArrayType | undefined = array.type as | MetadataArrayType | undefined; if (type !== undefined) visitArrayType({ components: props.components, schema: arraySchema(props.schema), type, visited: props.visited, }); } for (const tuple of props.metadata.tuples) { const type: MetadataTupleType | undefined = tuple.type as | MetadataTupleType | undefined; if (type !== undefined) visitTupleType({ components: props.components, schema: props.schema, type, visited: props.visited, }); } for (const alias of props.metadata.aliases) { const type: MetadataAliasType | undefined = alias.type as | MetadataAliasType | undefined; if (type !== undefined) visitAliasType({ components: props.components, schema: componentSchema(props.components, type.name), type, visited: props.visited, }); } for (const object of props.metadata.objects) { const type: MetadataObjectType | undefined = object.type as | MetadataObjectType | undefined; if (type !== undefined) visitObjectType({ components: props.components, type, visited: props.visited, }); } }; const visitAliasType = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema | undefined; type: MetadataAliasType; visited: IVisited; }): void => { if (props.visited.aliases.has(props.type)) return; props.visited.aliases.add(props.type); if (props.schema !== undefined && isReadonlyArrayLike(props.type.value)) setReadonlyArray(props.schema); visitMetadata({ components: props.components, schema: props.schema, metadata: props.type.value, visited: props.visited, }); }; const visitArrayType = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema | undefined; type: MetadataArrayType; visited: IVisited; }): void => { if (props.visited.arrays.has(props.type)) return; props.visited.arrays.add(props.type); const component: OpenApi.IJsonSchema | undefined = componentSchema( props.components, props.type.name, ); if (component !== undefined && isReadonlyArrayName(props.type.name)) setReadonlyArray(component); visitMetadata({ components: props.components, schema: arraySchema(props.schema ?? component), metadata: props.type.value, visited: props.visited, }); }; const visitTupleType = (props: { components: OpenApi.IComponents; schema: OpenApi.IJsonSchema | undefined; type: MetadataTupleType; visited: IVisited; }): void => { if (props.visited.tuples.has(props.type)) return; props.visited.tuples.add(props.type); const component: OpenApi.IJsonSchema | undefined = componentSchema( props.components, props.type.name, ); if (component !== undefined && isReadonlyArrayName(props.type.name)) setReadonlyArray(component); const schema: JsonSchemaObject | undefined = objectSchema( props.schema ?? component, ); props.type.elements.forEach((metadata, index) => visitMetadata({ components: props.components, schema: Array.isArray(schema?.prefixItems) ? schema.prefixItems[index] : undefined, metadata, visited: props.visited, }), ); }; const visitObjectType = (props: { components: OpenApi.IComponents; type: MetadataObjectType; visited: IVisited; }): void => { if (props.visited.objects.has(props.type)) return; props.visited.objects.add(props.type); const schema: JsonSchemaObject | undefined = objectSchema( componentSchema(props.components, props.type.name), ); const properties: Record<string, OpenApi.IJsonSchema> | undefined = schema?.properties as Record<string, OpenApi.IJsonSchema> | undefined; for (const property of props.type.properties) { const key: string | undefined = propertyKey(property.key); const child: OpenApi.IJsonSchema | undefined = key === undefined ? undefined : properties?.[key]; visitMetadata({ components: props.components, schema: child, metadata: property.value, visited: props.visited, }); } }; const isReadonlyArrayLike = ( metadata: MetadataSchema, visited: WeakSet<MetadataSchema> = new WeakSet(), ): boolean => { if (visited.has(metadata)) return false; visited.add(metadata); const name: unknown = (metadata as { name?: unknown }).name; return ( (typeof name === "string" && isReadonlyArrayName(name)) || metadata.arrays.some((array) => isReadonlyArrayName(array.name)) || metadata.tuples.some((tuple) => isReadonlyArrayName(tuple.name)) || metadata.aliases.some((alias) => { const type: MetadataAliasType | undefined = alias.type as | MetadataAliasType | undefined; return type !== undefined && isReadonlyArrayLike(type.value, visited); }) ); }; const isReadonlyArrayName = (name: string): boolean => { const compact: string = name.replace(/\s+/g, ""); // Metadata identity names pass through the generator's name replacer, which // strips TypeScript punctuation for schema-key safety — depending on the // compiler shim, `ReadonlyArray<string>` arrives here as "ReadonlyArray", // "ReadonlyArray<string>", or the stripped "ReadonlyArraystring", and // `readonly string[]` as "readonly string[]" or "readonlystring". Match on // the prefixes that survive every form; this predicate only ever sees names // of array/tuple metadata entries, whose spelling the generator controls // ("readonly" is case-sensitive, so PascalCase user types cannot collide). return compact.startsWith("ReadonlyArray") || compact.startsWith("readonly"); }; const componentSchema = ( components: OpenApi.IComponents, name: string, ): OpenApi.IJsonSchema | undefined => (components.schemas ?? {})[name] as OpenApi.IJsonSchema | undefined; const arraySchema = ( schema: OpenApi.IJsonSchema | undefined, ): OpenApi.IJsonSchema | undefined => { const obj: JsonSchemaObject | undefined = objectSchema(schema); return obj?.items as OpenApi.IJsonSchema | undefined; }; const objectSchema = ( schema: OpenApi.IJsonSchema | undefined, ): JsonSchemaObject | undefined => typeof schema === "object" && schema !== null ? (schema as JsonSchemaObject) : undefined; const propertyKey = (metadata: MetadataSchema): string | undefined => { const value: unknown = metadata.constants[0]?.values[0]?.value; return typeof value === "string" ? value : typeof value === "number" ? String(value) : undefined; }; const setReadonlyArray = (schema: OpenApi.IJsonSchema): void => { const obj: JsonSchemaObject | undefined = objectSchema(schema); if (obj !== undefined) obj["x-readonly-array"] = true; };