diff --git a/package.json b/package.json index 955f0b5..29d1d15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "0.1.2", + "version": "0.1.4", "description": "", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/src/OpenApi.ts b/src/OpenApi.ts index 3e4ff51..5708fb1 100644 --- a/src/OpenApi.ts +++ b/src/OpenApi.ts @@ -8,6 +8,33 @@ import { SwaggerV2Converter } from "./internal/SwaggerV2Converter"; /** * Emended OpenAPI v3.1 definition used by `typia` and `nestia`. * + * `OpenApi` is a namespace containing functions and interfaces for emended + * OpenAPI v3.1 specification. The keyword "emended" means that `OpenApi` is + * not a direct OpenAPI v3.1 specification ({@link OpenApiV3_1}), but a little + * bit shrinked to remove ambiguous and duplicated expressions of OpenAPI v3.1 + * for the convenience of `typia` and `nestia`. + * + * For example, when representing nullable type, OpenAPI v3.1 supports three ways. + * In that case, `OpenApi` remains only the third way, so that makes `typia` and + * `nestia` (especially `@nestia/editor`) to be simple and easy to implement. + * + * 1. `type: ["string", "null"]` + * 2. `type: "string", nullable: true` + * 3. `oneOf: [{ type: "string" }, { type: "null" }]` + * + * Here is the entire list of differences between OpenAPI v3.1 and emended `OpenApi`. + * + * - Operation + * - Merged {@link OpenApiV3_1.IPathItem.parameters} to {@link OpenApi.IOperation.parameters} + * - Resolved {@link OpenApi.IJsonSchema.IReference references} of {@link OpenApiV3_1.IOperation} mebers + * - JSON Schema + * - Decomposed mixed type: {@link OpenApiV3_1.IJsonSchema.IMixed} + * - Resolved nullable property: {@link OpenApiV3_1.IJsonSchema.__ISignificant.nullable} + * - Array type utilizes only single {@link OpenAPI.IJsonSchema.IArray.items} + * - Tuple type utilizes only {@link OpenApi.IJsonSchema.ITuple.prefixItems} + * - Merged {@link OpenApiV3_1.IJsonSchema.IAnyOf} to {@link OpenApi.IJsonSchema.IOneOf} + * - Merged {@link OpenApiV3_1.IJsonSchema.IRecursiveReference} to {@link OpenApi.IJsonSchema.IReference} + * * @author Jeongho Nam - https://github.com/samchon */ export namespace OpenApi { @@ -152,6 +179,7 @@ export namespace OpenApi { | IJsonSchema.INumber | IJsonSchema.IString | IJsonSchema.IArray + | IJsonSchema.ITuple | IJsonSchema.IObject | IJsonSchema.IReference | IJsonSchema.IOneOf @@ -220,7 +248,6 @@ export namespace OpenApi { /** @type uint */ maxItems?: number; } export interface ITuple extends __ISignificant<"array"> { - items: never; prefixItems: IJsonSchema[]; additionalItems: boolean | IJsonSchema; /** @type uint */ minItems?: number; diff --git a/src/OpenApiV3_1.ts b/src/OpenApiV3_1.ts index c9ba01d..47396b5 100644 --- a/src/OpenApiV3_1.ts +++ b/src/OpenApiV3_1.ts @@ -278,9 +278,13 @@ export namespace OpenApiV3_1 { export interface IReference extends __IAttribute { $ref: Key; } + export interface IRecursiveReference extends __IAttribute { + $recursiveRef: string; + } export interface __ISignificant extends __IAttribute { type: Type; + nullable?: boolean; } export interface __IAttribute { title?: string; diff --git a/src/internal/OpenApiV3Converter.ts b/src/internal/OpenApiV3Converter.ts index 418ba43..5d8b751 100644 --- a/src/internal/OpenApiV3Converter.ts +++ b/src/internal/OpenApiV3Converter.ts @@ -204,6 +204,7 @@ export namespace OpenApiV3Converter { ), }; const visit = (schema: OpenApiV3.IJsonSchema): void => { + // NULLABLE PROPERTY if ( (schema as OpenApiV3.IJsonSchema.__ISignificant).nullable === true ) diff --git a/src/internal/OpenApiV3_1Converter.ts b/src/internal/OpenApiV3_1Converter.ts index b19f812..d209626 100644 --- a/src/internal/OpenApiV3_1Converter.ts +++ b/src/internal/OpenApiV3_1Converter.ts @@ -224,7 +224,15 @@ export namespace OpenApiV3_1Converter { ), ), }; + const nullable: { value: boolean } = { value: false }; + const visit = (schema: OpenApiV3_1.IJsonSchema): void => { + // NULLABLE PROPERTY + if ( + (schema as OpenApiV3_1.IJsonSchema.__ISignificant).nullable === + true + ) + nullable.value ||= true; // MIXED TYPE CASE if (TypeChecker.isMixed(schema)) { if (schema.const !== undefined) @@ -407,7 +415,7 @@ export namespace OpenApiV3_1Converter { }); } // OBJECT TYPE CASE - else if (TypeChecker.isObject(schema)) { + else if (TypeChecker.isObject(schema)) union.push({ ...schema, ...{ @@ -428,12 +436,24 @@ export namespace OpenApiV3_1Converter { : undefined, }, }); - } + else if (TypeChecker.isRecursiveReference(schema)) + union.push({ + ...schema, + ...{ + $ref: schema.$recursiveRef, + $recursiveRef: undefined, + }, + }); // THE OTHERS else union.push(schema); }; visit(input); + if ( + nullable.value === true && + !union.some((e) => (e as OpenApi.IJsonSchema.INull).type === "null") + ) + union.push({ type: "null" }); return { ...(union.length === 0 ? { type: undefined } @@ -477,6 +497,11 @@ export namespace OpenApiV3_1Converter { schema: OpenApiV3_1.IJsonSchema, ): schema is OpenApiV3_1.IJsonSchema.IReference => (schema as OpenApiV3_1.IJsonSchema.IReference).$ref !== undefined; + export const isRecursiveReference = ( + schema: OpenApiV3_1.IJsonSchema, + ): schema is OpenApiV3_1.IJsonSchema.IRecursiveReference => + (schema as OpenApiV3_1.IJsonSchema.IRecursiveReference).$recursiveRef !== + undefined; export const isOneOf = ( schema: OpenApiV3_1.IJsonSchema, ): schema is OpenApiV3_1.IJsonSchema.IOneOf =>