@samchon/openapi
Version:
OpenAPI definitions and converters for 'typia' and 'nestia'.
566 lines (552 loc) • 19.5 kB
text/typescript
import { OpenApi } from "../OpenApi";
import { SwaggerV2 } from "../SwaggerV2";
import { OpenApiExclusiveEmender } from "../utils/OpenApiExclusiveEmender";
import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker";
export namespace SwaggerV2Upgrader {
export const convert = (input: SwaggerV2.IDocument): OpenApi.IDocument => ({
openapi: "3.1.0",
info: input.info,
components: convertComponents(input),
paths: input.paths
? Object.fromEntries(
Object.entries(input.paths)
.filter(([_, v]) => v !== undefined)
.map(
([key, value]) => [key, convertPathItem(input)(value)] as const,
),
)
: undefined,
servers: input.host
? [
{
url: input.host,
},
]
: undefined,
security: input.security,
tags: input.tags,
"x-samchon-emended-v4": true,
});
/* -----------------------------------------------------------
OPERATORS
----------------------------------------------------------- */
const convertPathItem =
(doc: SwaggerV2.IDocument) =>
(pathItem: SwaggerV2.IPath): OpenApi.IPath => ({
...(pathItem as any),
...(pathItem.get
? { get: convertOperation(doc)(pathItem)(pathItem.get) }
: undefined),
...(pathItem.put
? { put: convertOperation(doc)(pathItem)(pathItem.put) }
: undefined),
...(pathItem.post
? { post: convertOperation(doc)(pathItem)(pathItem.post) }
: undefined),
...(pathItem.delete
? { delete: convertOperation(doc)(pathItem)(pathItem.delete) }
: undefined),
...(pathItem.options
? { options: convertOperation(doc)(pathItem)(pathItem.options) }
: undefined),
...(pathItem.head
? { head: convertOperation(doc)(pathItem)(pathItem.head) }
: undefined),
...(pathItem.patch
? { patch: convertOperation(doc)(pathItem)(pathItem.patch) }
: undefined),
...(pathItem.trace
? { trace: convertOperation(doc)(pathItem)(pathItem.trace) }
: undefined),
});
const convertOperation =
(doc: SwaggerV2.IDocument) =>
(pathItem: SwaggerV2.IPath) =>
(input: SwaggerV2.IOperation): OpenApi.IOperation => ({
...input,
parameters:
pathItem.parameters !== undefined || input.parameters !== undefined
? (
[...(pathItem.parameters ?? []), ...(input.parameters ?? [])]
.map((p) =>
TypeChecker.isReference(p)
? doc.parameters?.[p.$ref.split("/").pop() ?? ""]!
: p,
)
.filter(
(p) =>
p !== undefined &&
p.in !== "body" &&
(p as SwaggerV2.IOperation.IBodyParameter).schema ===
undefined,
) as SwaggerV2.IOperation.IGeneralParameter[]
).map(convertParameter(doc.definitions ?? {}))
: undefined,
requestBody: (() => {
const found: SwaggerV2.IOperation.IBodyParameter | undefined =
input.parameters?.find((p) => {
if (TypeChecker.isReference(p))
p = doc.parameters?.[p.$ref.split("/").pop() ?? ""]!;
return (
(p as SwaggerV2.IOperation.IBodyParameter)?.schema !== undefined
);
}) as SwaggerV2.IOperation.IBodyParameter | undefined;
return found
? convertRequestBody(doc.definitions ?? {})(found)
: undefined;
})(),
responses: input.responses
? Object.fromEntries(
Object.entries(input.responses)
.filter(([_, v]) => v !== undefined)
.map(
([key, value]) => [key, convertResponse(doc)(value)!] as const,
)
.filter(([_, v]) => v !== undefined),
)
: undefined,
});
const convertParameter =
(definitions: Record<string, SwaggerV2.IJsonSchema>) =>
(
input: SwaggerV2.IOperation.IGeneralParameter,
): OpenApi.IOperation.IParameter => ({
name: input.name,
in: input.in as any,
description: input.description,
schema: convertSchema(definitions)(input),
required: true,
});
const convertRequestBody =
(definitions: Record<string, SwaggerV2.IJsonSchema>) =>
(
input: SwaggerV2.IOperation.IBodyParameter,
): OpenApi.IOperation.IRequestBody => ({
description: input.description,
content: {
"application/json": {
schema: convertSchema(definitions)(input.schema),
},
},
});
const convertResponse =
(doc: SwaggerV2.IDocument) =>
(
input:
| SwaggerV2.IOperation.IResponse
| SwaggerV2.IJsonSchema.IReference<`#/definitions/responses/${string}`>,
): OpenApi.IOperation.IResponse | undefined => {
if (TypeChecker.isReference(input)) {
const found: SwaggerV2.IOperation.IResponse | undefined =
doc.responses?.[input.$ref.split("/").pop() ?? ""]!;
if (found === undefined) return undefined;
input = found;
}
return {
description: input.description,
content: input.schema
? {
"application/json": {
schema: convertSchema(doc.definitions ?? {})(input.schema),
example: input.example,
},
}
: undefined,
headers: input.headers
? Object.fromEntries(
Object.entries(input.headers)
.filter(([_, v]) => v !== undefined)
.map(
([key, value]) =>
[
key,
{
schema: convertSchema(doc.definitions ?? {})(value),
in: "header",
},
] as const,
),
)
: undefined,
};
};
/* -----------------------------------------------------------
DEFINITIONS
----------------------------------------------------------- */
const convertComponents = (
input: SwaggerV2.IDocument,
): OpenApi.IComponents => ({
schemas: Object.fromEntries(
Object.entries(input.definitions ?? {})
.filter(([_, v]) => v !== undefined)
.map(([key, value]) => [
key,
convertSchema(input.definitions ?? {})(value),
]),
),
securitySchemes: input.securityDefinitions
? Object.fromEntries(
Object.entries(input.securityDefinitions)
.filter(([_, v]) => v !== undefined)
.map(([key, value]) => [key, convertSecurityScheme(value)])
.filter(([_, v]) => v !== undefined),
)
: undefined,
});
const convertSecurityScheme = (
input: SwaggerV2.ISecurityDefinition,
): OpenApi.ISecurityScheme => {
if (input.type === "apiKey") return input;
else if (input.type === "basic")
return {
type: "http",
scheme: "basic",
description: input.description,
};
else if (input.type === "oauth2")
if (input.flow === "implicit")
return {
type: "oauth2",
description: input.description,
flows: {
implicit: {
authorizationUrl: input.authorizationUrl,
scopes: input.scopes,
},
},
};
else if (input.flow === "accessCode")
return {
type: "oauth2",
description: input.description,
flows: {
authorizationCode: {
authorizationUrl: input.authorizationUrl,
tokenUrl: input.tokenUrl,
scopes: input.scopes,
},
},
};
else if (input.flow === "password")
return {
type: "oauth2",
description: input.description,
flows: {
password: {
tokenUrl: input.tokenUrl,
scopes: input.scopes,
},
},
};
else if (input.flow === "application")
return {
type: "oauth2",
description: input.description,
flows: {
clientCredentials: {
tokenUrl: input.tokenUrl,
scopes: input.scopes,
},
},
};
else return undefined!;
return undefined!;
};
export const convertSchema =
(definitions: Record<string, SwaggerV2.IJsonSchema>) =>
(input: SwaggerV2.IJsonSchema): OpenApi.IJsonSchema => {
const nullable: { value: boolean; default?: null } = {
value: false,
default: undefined,
};
const union: OpenApi.IJsonSchema[] = [];
const attribute: OpenApi.IJsonSchema.__IAttribute = {
title: input.title,
description: input.description,
...Object.fromEntries(
Object.entries(input).filter(
([key, value]) => key.startsWith("x-") && value !== undefined,
),
),
example: input.example,
examples: input.examples
? Object.fromEntries(input.examples.map((v, i) => [`v${i}`, v]))
: undefined,
};
const visit = (schema: SwaggerV2.IJsonSchema): void => {
// NULLABLE PROPERTY
if (
(schema as SwaggerV2.IJsonSchema.__ISignificant<any>)[
"x-nullable"
] === true
) {
nullable.value ||= true;
if ((schema as SwaggerV2.IJsonSchema.INumber).default === null)
nullable.default = null;
}
if (
Array.isArray((schema as SwaggerV2.IJsonSchema.INumber).enum) &&
(schema as SwaggerV2.IJsonSchema.INumber).enum?.length &&
(schema as SwaggerV2.IJsonSchema.INumber).enum?.some(
(e) => e === null,
)
)
nullable.value ||= true;
// UNION TYPE CASE
if (TypeChecker.isAnyOf(schema)) schema["x-anyOf"].forEach(visit);
else if (TypeChecker.isOneOf(schema)) schema["x-oneOf"].forEach(visit);
else if (TypeChecker.isAllOf(schema))
if (schema.allOf.length === 1) visit(schema.allOf[0]);
else union.push(convertAllOfSchema(definitions)(schema));
// ATOMIC TYPE CASE (CONSIDER ENUM VALUES)
else if (
TypeChecker.isBoolean(schema) ||
TypeChecker.isInteger(schema) ||
TypeChecker.isNumber(schema) ||
TypeChecker.isString(schema)
)
if (
schema.enum?.length &&
schema.enum.filter((e) => e !== null).length
)
union.push(
...schema.enum
.filter((v) => v !== null)
.map((value) => ({ const: value })),
);
else if (
TypeChecker.isInteger(schema) ||
TypeChecker.isNumber(schema)
)
union.push(
OpenApiExclusiveEmender.emend({
...schema,
default: (schema.default ?? undefined) satisfies
| boolean
| number
| string
| undefined as any,
examples: schema.examples
? Object.fromEntries(
schema.examples.map((v, i) => [`v${i}`, v]),
)
: undefined,
exclusiveMinimum:
typeof schema.exclusiveMinimum === "boolean"
? schema.exclusiveMinimum === true
? schema.minimum
: undefined
: schema.exclusiveMinimum,
exclusiveMaximum:
typeof schema.exclusiveMaximum === "boolean"
? schema.exclusiveMaximum === true
? schema.maximum
: undefined
: schema.exclusiveMaximum,
minimum:
schema.exclusiveMinimum === true ? undefined : schema.minimum,
maximum:
schema.exclusiveMaximum === true ? undefined : schema.maximum,
...{ enum: undefined },
}),
);
else
union.push({
...schema,
default: (schema.default ?? undefined) satisfies
| boolean
| number
| string
| undefined as any,
examples: schema.examples
? Object.fromEntries(
schema.examples.map((v, i) => [`v${i}`, v]),
)
: undefined,
...{ enum: undefined },
});
// INSTANCE TYPE CASE
else if (TypeChecker.isArray(schema))
union.push({
...schema,
items: convertSchema(definitions)(schema.items),
examples: schema.examples
? Object.fromEntries(schema.examples.map((v, i) => [`v${i}`, v]))
: undefined,
});
else if (TypeChecker.isObject(schema))
union.push({
...schema,
...{
properties: schema.properties
? Object.fromEntries(
Object.entries(schema.properties)
.filter(([_, v]) => v !== undefined)
.map(([key, value]) => [
key,
convertSchema(definitions)(value),
]),
)
: {},
additionalProperties: schema.additionalProperties
? typeof schema.additionalProperties === "object" &&
schema.additionalProperties !== null
? convertSchema(definitions)(schema.additionalProperties)
: schema.additionalProperties
: undefined,
},
examples: schema.examples
? Object.fromEntries(schema.examples.map((v, i) => [`v${i}`, v]))
: undefined,
required: schema.required ?? [],
});
else if (TypeChecker.isReference(schema))
union.push({
...schema,
$ref: schema.$ref.replace(
"#/definitions/",
"#/components/schemas/",
),
examples: schema.examples
? Object.fromEntries(schema.examples.map((v, i) => [`v${i}`, v]))
: undefined,
});
else
union.push({
...schema,
examples: schema.examples
? Object.fromEntries(schema.examples.map((v, i) => [`v${i}`, v]))
: undefined,
});
};
visit(input);
if (
nullable.value === true &&
!union.some((e) => (e as OpenApi.IJsonSchema.INull).type === "null")
)
union.push({
type: "null",
default: nullable.default,
});
if (
union.length === 2 &&
union.filter((x) => OpenApiTypeChecker.isNull(x)).length === 1
) {
const type: OpenApi.IJsonSchema = union.filter(
(x) => OpenApiTypeChecker.isNull(x) === false,
)[0];
for (const key of [
"title",
"description",
"deprecated",
"example",
"examples",
] as const)
if (type[key] !== undefined) delete type[key];
}
return {
...(union.length === 0
? { type: undefined }
: union.length === 1
? { ...union[0] }
: { oneOf: union.map((u) => ({ ...u, "x-nullable": undefined })) }),
...attribute,
...{ "x-nullable": undefined },
};
};
const convertAllOfSchema =
(definitions: Record<string, SwaggerV2.IJsonSchema>) =>
(input: SwaggerV2.IJsonSchema.IAllOf): OpenApi.IJsonSchema => {
const objects: Array<SwaggerV2.IJsonSchema.IObject | null> =
input.allOf.map((schema) => retrieveObject(definitions)(schema));
if (objects.some((obj) => obj === null))
return {
type: undefined,
...{
allOf: undefined,
},
};
return {
...input,
type: "object",
properties: Object.fromEntries(
objects
.map((o) => Object.entries(o?.properties ?? {}))
.flat()
.map(
([key, value]) =>
[key, convertSchema(definitions)(value)] as const,
),
),
...{
allOf: undefined,
required: [...new Set(objects.map((o) => o?.required ?? []).flat())],
},
};
};
const retrieveObject =
(definitions: Record<string, SwaggerV2.IJsonSchema>) =>
(
input: SwaggerV2.IJsonSchema,
visited: Set<SwaggerV2.IJsonSchema> = new Set(),
): SwaggerV2.IJsonSchema.IObject | null => {
if (TypeChecker.isObject(input))
return input.properties !== undefined && !input.additionalProperties
? input
: null;
else if (visited.has(input)) return null;
else visited.add(input);
if (TypeChecker.isReference(input))
return retrieveObject(definitions)(
definitions?.[input.$ref.split("/").pop() ?? ""] ?? {},
visited,
);
return null;
};
namespace TypeChecker {
export const isBoolean = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IBoolean =>
(schema as SwaggerV2.IJsonSchema.IBoolean).type === "boolean";
export const isInteger = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IInteger =>
(schema as SwaggerV2.IJsonSchema.IInteger).type === "integer";
export const isNumber = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.INumber =>
(schema as SwaggerV2.IJsonSchema.INumber).type === "number";
export const isString = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IString =>
(schema as SwaggerV2.IJsonSchema.IString).type === "string";
export const isArray = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IArray =>
(schema as SwaggerV2.IJsonSchema.IArray).type === "array";
export const isObject = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IObject =>
(schema as SwaggerV2.IJsonSchema.IObject).type === "object";
export const isReference = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IReference =>
(schema as SwaggerV2.IJsonSchema.IReference).$ref !== undefined;
export const isAllOf = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IAllOf =>
(schema as SwaggerV2.IJsonSchema.IAllOf).allOf !== undefined;
export const isOneOf = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IOneOf =>
(schema as SwaggerV2.IJsonSchema.IOneOf)["x-oneOf"] !== undefined;
export const isAnyOf = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.IAnyOf =>
(schema as SwaggerV2.IJsonSchema.IAnyOf)["x-anyOf"] !== undefined;
export const isNullOnly = (
schema: SwaggerV2.IJsonSchema,
): schema is SwaggerV2.IJsonSchema.INullOnly =>
(schema as SwaggerV2.IJsonSchema.INullOnly).type === "null";
}
}