@samchon/openapi
Version:
OpenAPI definitions and converters for 'typia' and 'nestia'.
423 lines (407 loc) • 14.8 kB
text/typescript
import { OpenApi } from "../OpenApi";
import { SwaggerV2 } from "../SwaggerV2";
import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker";
export namespace SwaggerV2Downgrader {
export interface IComponentsCollection {
original: OpenApi.IComponents;
downgraded: Record<string, SwaggerV2.IJsonSchema>;
}
export const downgrade = (input: OpenApi.IDocument): SwaggerV2.IDocument => {
const collection: IComponentsCollection = downgradeComponents(
input.components,
);
return {
swagger: "2.0",
info: input.info,
host: input.servers?.[0]?.url
? input.servers[0].url.split("://").pop()!
: "",
definitions: collection.downgraded,
securityDefinitions: input.components?.securitySchemes
? Object.fromEntries(
Object.entries(input.components.securitySchemes)
.filter(([_, v]) => v !== undefined)
.map(([key, value]) =>
downgradeSecurityScheme(value).map((v) => [key, v]),
)
.flat(),
)
: undefined,
paths: input.paths
? Object.fromEntries(
Object.entries(input.paths)
.filter(([_, v]) => v !== undefined)
.map(
([key, value]) =>
[key, downgradePathItem(collection)(value)] as const,
),
)
: undefined,
security: input.security,
tags: input.tags,
};
};
/* -----------------------------------------------------------
OPERATORS
----------------------------------------------------------- */
const downgradePathItem =
(collection: IComponentsCollection) =>
(pathItem: OpenApi.IPath): SwaggerV2.IPath => ({
...(pathItem as any),
...(pathItem.get
? { get: downgradeOperation(collection)(pathItem.get) }
: undefined),
...(pathItem.put
? { put: downgradeOperation(collection)(pathItem.put) }
: undefined),
...(pathItem.post
? { post: downgradeOperation(collection)(pathItem.post) }
: undefined),
...(pathItem.delete
? { delete: downgradeOperation(collection)(pathItem.delete) }
: undefined),
...(pathItem.options
? { options: downgradeOperation(collection)(pathItem.options) }
: undefined),
...(pathItem.head
? { head: downgradeOperation(collection)(pathItem.head) }
: undefined),
...(pathItem.patch
? { patch: downgradeOperation(collection)(pathItem.patch) }
: undefined),
...(pathItem.trace
? { trace: downgradeOperation(collection)(pathItem.trace) }
: undefined),
});
const downgradeOperation =
(collection: IComponentsCollection) =>
(input: OpenApi.IOperation): SwaggerV2.IOperation => ({
...input,
parameters:
input.parameters !== undefined || input.requestBody !== undefined
? [
...(input.parameters ?? []).map(downgradeParameter(collection)),
...(input.requestBody
? [downgradeRequestBody(collection)(input.requestBody)]
: []),
]
: undefined,
responses: input.responses
? Object.fromEntries(
Object.entries(input.responses)
.filter(([_, v]) => v !== undefined)
.map(([key, value]) => [
key,
downgradeResponse(collection)(value),
]),
)
: undefined,
...{
requestBody: undefined,
servers: undefined,
},
});
const downgradeParameter =
(collection: IComponentsCollection) =>
(
input: OpenApi.IOperation.IParameter,
i: number,
): SwaggerV2.IOperation.IParameter =>
({
...downgradeSchema(collection)(input.schema),
...input,
required: (input.schema as any)?.required,
schema: undefined,
name: input.name ?? `p${i}`,
...{
example: undefined,
examples: undefined,
},
}) as any;
const downgradeRequestBody =
(collection: IComponentsCollection) =>
(
input: OpenApi.IOperation.IRequestBody,
): SwaggerV2.IOperation.IParameter => ({
name: "body",
in: "body",
description: input.description,
required: input.required,
schema: downgradeSchema(collection)(
Object.values(input.content ?? {})[0]?.schema ?? {},
),
});
const downgradeResponse =
(collection: IComponentsCollection) =>
(input: OpenApi.IOperation.IResponse): SwaggerV2.IOperation.IResponse => ({
description: input.description,
schema: downgradeSchema(collection)(
Object.values(input.content ?? {})[0]?.schema ?? {},
),
headers: input.headers
? Object.fromEntries(
Object.entries(input.headers)
.filter(([_, v]) => v !== undefined)
.map(([key, value]) => [
key,
{
...value,
schema: downgradeSchema(collection)(value.schema),
...{
example: undefined,
examples: undefined,
},
},
]),
)
: undefined,
});
/* -----------------------------------------------------------
DEFINITIONS
----------------------------------------------------------- */
export const downgradeComponents = (
input: OpenApi.IComponents,
): IComponentsCollection => {
const collection: IComponentsCollection = {
original: input,
downgraded: {},
};
if (input.schemas) {
collection.downgraded.schemas = {};
for (const [key, value] of Object.entries(input.schemas))
if (value !== undefined)
collection.downgraded[key.split("/").pop()!] =
downgradeSchema(collection)(value);
}
return collection;
};
export const downgradeSchema =
(collection: IComponentsCollection) =>
(input: OpenApi.IJsonSchema): SwaggerV2.IJsonSchema => {
const nullable: boolean = isNullable(new Set())(collection.original)(
input,
);
const union: SwaggerV2.IJsonSchema[] = [];
const attribute: SwaggerV2.IJsonSchema.__IAttribute = {
title: input.title,
description: input.description,
example: input.example,
examples: input.examples ? Object.values(input.examples) : undefined,
...Object.fromEntries(
Object.entries(input).filter(
([key, value]) => key.startsWith("x-") && value !== undefined,
),
),
};
const visit = (schema: OpenApi.IJsonSchema): void => {
if (OpenApiTypeChecker.isBoolean(schema))
union.push({ type: "boolean" });
else if (
OpenApiTypeChecker.isBoolean(schema) ||
OpenApiTypeChecker.isInteger(schema) ||
OpenApiTypeChecker.isNumber(schema) ||
OpenApiTypeChecker.isString(schema)
)
union.push({
...schema,
examples: schema.examples
? Object.values(schema.examples)
: undefined,
});
else if (OpenApiTypeChecker.isReference(schema))
union.push({ $ref: `#/definitions/${schema.$ref.split("/").pop()}` });
else if (OpenApiTypeChecker.isArray(schema))
union.push({
...schema,
items: downgradeSchema(collection)(schema.items),
examples: schema.examples
? Object.values(schema.examples)
: undefined,
});
else if (OpenApiTypeChecker.isTuple(schema))
union.push({
...schema,
items: ((): SwaggerV2.IJsonSchema => {
if (schema.additionalItems === true) return {};
const elements = [
...schema.prefixItems,
...(typeof schema.additionalItems === "object"
? [downgradeSchema(collection)(schema.additionalItems)]
: []),
];
if (elements.length === 0) return {};
return {
"x-oneOf": elements.map(downgradeSchema(collection) as any),
};
})(),
minItems: schema.prefixItems.length,
maxItems:
!!schema.additionalItems === true
? undefined
: schema.prefixItems.length,
...{
prefixItems: undefined,
additionalItems: undefined,
},
examples: schema.examples
? Object.values(schema.examples)
: undefined,
});
else if (OpenApiTypeChecker.isObject(schema))
union.push({
...schema,
properties: schema.properties
? Object.fromEntries(
Object.entries(schema.properties)
.filter(([_, v]) => v !== undefined)
.map(([key, value]) => [
key,
downgradeSchema(collection)(value),
]),
)
: undefined,
additionalProperties:
typeof schema.additionalProperties === "object"
? downgradeSchema(collection)(schema.additionalProperties)
: schema.additionalProperties,
required: schema.required,
examples: schema.examples
? Object.values(schema.examples)
: undefined,
});
else if (OpenApiTypeChecker.isOneOf(schema))
schema.oneOf.forEach(visit);
};
const visitConstant = (schema: OpenApi.IJsonSchema): void => {
const insert = (value: any): void => {
const matched: SwaggerV2.IJsonSchema.INumber | undefined = union.find(
(u) =>
(u as SwaggerV2.IJsonSchema.__ISignificant<any>).type === value,
) as SwaggerV2.IJsonSchema.INumber | undefined;
if (matched !== undefined) {
matched.enum ??= [];
matched.enum.push(value);
} else union.push({ type: typeof value as "number", enum: [value] });
if (OpenApiTypeChecker.isConstant(schema)) insert(schema.const);
else if (OpenApiTypeChecker.isOneOf(schema))
schema.oneOf.forEach(insert);
};
};
visit(input);
visitConstant(input);
if (nullable) {
for (const u of union)
if (OpenApiTypeChecker.isReference(u as any))
downgradeNullableReference(new Set())(collection)(u as any);
else (u as SwaggerV2.IJsonSchema.IArray)["x-nullable"] = true;
}
if (nullable === true && union.length === 0)
return { type: "null", ...attribute };
return {
...(union.length === 0
? { type: undefined }
: union.length === 1
? { ...union[0] }
: { "x-oneOf": union }),
...attribute,
...(union.length > 1 ? { discriminator: undefined } : {}),
};
};
const downgradeNullableReference =
(visited: Set<string>) =>
(collection: IComponentsCollection) =>
(schema: SwaggerV2.IJsonSchema.IReference): void => {
const key: string = schema.$ref.split("/").pop()!;
if (key.endsWith(".Nullable")) return;
const found: OpenApi.IJsonSchema | undefined =
collection.original.schemas?.[key];
if (found === undefined) return;
else if (isNullable(visited)(collection.original)(found) === true) return;
else if (collection.downgraded[`${key}.Nullable`] === undefined) {
collection.downgraded[`${key}.Nullable`] = {};
collection.downgraded[`${key}.Nullable`] = downgradeSchema(collection)(
OpenApiTypeChecker.isOneOf(found)
? {
...found,
oneOf: [...found.oneOf, { type: "null" }],
}
: {
title: found.title,
description: found.description,
example: found.example,
examples: found.examples
? Object.values(found.examples)
: undefined,
...Object.fromEntries(
Object.entries(found).filter(
([key, value]) =>
key.startsWith("x-") && value !== undefined,
),
),
oneOf: [found, { type: "null" }],
},
);
}
schema.$ref += ".Nullable";
};
const downgradeSecurityScheme = (
input: OpenApi.ISecurityScheme,
): SwaggerV2.ISecurityDefinition[] => {
if (input.type === "apiKey") return [input];
else if (input.type === "http")
if (input.scheme === "basic")
return [{ type: "basic", description: input.description }];
else return [];
else if (input.type === "oauth2") {
const output: SwaggerV2.ISecurityDefinition[] = [];
if (input.flows.implicit)
output.push({
type: "oauth2",
flow: "implicit",
authorizationUrl: input.flows.implicit.authorizationUrl,
scopes: input.flows.implicit.scopes,
});
if (input.flows.password)
output.push({
type: "oauth2",
flow: "password",
tokenUrl: input.flows.password.tokenUrl,
scopes: input.flows.password.scopes,
});
if (input.flows.clientCredentials)
output.push({
type: "oauth2",
flow: "application",
tokenUrl: input.flows.clientCredentials.tokenUrl,
scopes: input.flows.clientCredentials.scopes,
});
if (input.flows.authorizationCode)
output.push({
type: "oauth2",
flow: "accessCode",
authorizationUrl: input.flows.authorizationCode.authorizationUrl,
tokenUrl: input.flows.authorizationCode.tokenUrl,
scopes: input.flows.authorizationCode.scopes,
});
return output;
}
return [];
};
const isNullable =
(visited: Set<string>) =>
(components: OpenApi.IComponents) =>
(schema: OpenApi.IJsonSchema): boolean => {
if (OpenApiTypeChecker.isNull(schema)) return true;
else if (OpenApiTypeChecker.isReference(schema)) {
if (visited.has(schema.$ref)) return false;
visited.add(schema.$ref);
const key: string = schema.$ref.split("/").pop()!;
const next: OpenApi.IJsonSchema | undefined = components.schemas?.[key];
return next ? isNullable(visited)(components)(next) : false;
}
return (
OpenApiTypeChecker.isOneOf(schema) &&
schema.oneOf.some(isNullable(visited)(components))
);
};
}