@nestia/sdk
Version:
Nestia SDK and Swagger generator
265 lines (245 loc) • 8.28 kB
text/typescript
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;
};