diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a5f06c7..f8f8e11 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,12 +21,9 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 20.x - - uses: pnpm/action-setup@v2 - with: - version: 8 - name: Install dependencies - run: pnpm install + run: npm install - name: Build run: npm run build - name: Test diff --git a/package.json b/package.json index a04fc97..5b0e53d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "0.3.0", + "version": "0.3.1", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "typings": "./lib/index.d.ts", diff --git a/test/features/test_document_convert_v20.ts b/test/features/test_document_convert_v20.ts new file mode 100644 index 0000000..1002ad7 --- /dev/null +++ b/test/features/test_document_convert_v20.ts @@ -0,0 +1,15 @@ +import { OpenApi, SwaggerV2 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_convert_v20 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v2.0`; + for (const file of await fs.promises.readdir(path)) { + if (file.endsWith(".json") === false) continue; + const swagger: SwaggerV2.IDocument = typia.assert( + JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + typia.assert(openapi); + } +}; diff --git a/test/features/test_document_convert_v30.ts b/test/features/test_document_convert_v30.ts new file mode 100644 index 0000000..a6fd498 --- /dev/null +++ b/test/features/test_document_convert_v30.ts @@ -0,0 +1,15 @@ +import { OpenApi, OpenApiV3 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_convert_v30 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v3.0`; + for (const file of await fs.promises.readdir(path)) { + if (file.endsWith(".json") === false) continue; + const swagger: OpenApiV3.IDocument = typia.assert( + JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + typia.assert(openapi); + } +}; diff --git a/test/features/test_document_convert_v31.ts b/test/features/test_document_convert_v31.ts new file mode 100644 index 0000000..a22419a --- /dev/null +++ b/test/features/test_document_convert_v31.ts @@ -0,0 +1,15 @@ +import { OpenApi, OpenApiV3_1 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_convert_v31 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v3.1`; + for (const file of await fs.promises.readdir(path)) { + if (file.endsWith(".json") === false) continue; + const swagger: OpenApiV3_1.IDocument = typia.assert( + JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + typia.assert(openapi); + } +}; diff --git a/test/features/test_document_downgrade_v20.ts b/test/features/test_document_downgrade_v20.ts new file mode 100644 index 0000000..beef3a4 --- /dev/null +++ b/test/features/test_document_downgrade_v20.ts @@ -0,0 +1,27 @@ +import { OpenApi, OpenApiV3, OpenApiV3_1, SwaggerV2 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_downgrade_v20 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v3.1`; + for (const directory of await fs.promises.readdir(path)) { + const stats: fs.Stats = await fs.promises.lstat(`${path}/${directory}`); + if (stats.isDirectory() === false) continue; + for (const file of await fs.promises.readdir(`${path}/${directory}`)) { + if (file.endsWith(".json") === false) continue; + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = typia.assert< + SwaggerV2.IDocument | OpenApiV3.IDocument | OpenApiV3_1.IDocument + >( + JSON.parse( + await fs.promises.readFile(`${path}/${directory}/${file}`, "utf8"), + ), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + const downgraded: SwaggerV2.IDocument = OpenApi.downgrade(openapi, "2.0"); + typia.assert(downgraded); + } + } +}; diff --git a/test/features/test_document_downgrade_v30.ts b/test/features/test_document_downgrade_v30.ts new file mode 100644 index 0000000..b5d078e --- /dev/null +++ b/test/features/test_document_downgrade_v30.ts @@ -0,0 +1,27 @@ +import { OpenApi, OpenApiV3, OpenApiV3_1, SwaggerV2 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_downgrade_v30 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v3.1`; + for (const directory of await fs.promises.readdir(path)) { + const stats: fs.Stats = await fs.promises.lstat(`${path}/${directory}`); + if (stats.isDirectory() === false) continue; + for (const file of await fs.promises.readdir(`${path}/${directory}`)) { + if (file.endsWith(".json") === false) continue; + const swagger: + | SwaggerV2.IDocument + | OpenApiV3.IDocument + | OpenApiV3_1.IDocument = typia.assert< + SwaggerV2.IDocument | OpenApiV3.IDocument | OpenApiV3_1.IDocument + >( + JSON.parse( + await fs.promises.readFile(`${path}/${directory}/${file}`, "utf8"), + ), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + const downgraded: OpenApiV3.IDocument = OpenApi.downgrade(openapi, "3.0"); + typia.assert(downgraded); + } + } +}; diff --git a/test/features/test_document_migrate_v20.ts b/test/features/test_document_migrate_v20.ts new file mode 100644 index 0000000..8a6b083 --- /dev/null +++ b/test/features/test_document_migrate_v20.ts @@ -0,0 +1,16 @@ +import { IMigrateDocument, OpenApi, SwaggerV2 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_migrate_v20 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v2.0`; + for (const file of await fs.promises.readdir(path)) { + if (file.endsWith(".json") === false) continue; + const swagger: SwaggerV2.IDocument = typia.assert( + JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + const migrate: IMigrateDocument = OpenApi.migrate(openapi); + typia.assert(migrate); + } +}; diff --git a/test/features/test_document_migrate_v30.ts b/test/features/test_document_migrate_v30.ts new file mode 100644 index 0000000..de3a2e6 --- /dev/null +++ b/test/features/test_document_migrate_v30.ts @@ -0,0 +1,16 @@ +import { IMigrateDocument, OpenApi, OpenApiV3 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_migrate_v30 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v3.0`; + for (const file of await fs.promises.readdir(path)) { + if (file.endsWith(".json") === false) continue; + const swagger: OpenApiV3.IDocument = typia.assert( + JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + const migrate: IMigrateDocument = OpenApi.migrate(openapi); + typia.assert(migrate); + } +}; diff --git a/test/features/test_document_migrate_v31.ts b/test/features/test_document_migrate_v31.ts new file mode 100644 index 0000000..fdd9d5c --- /dev/null +++ b/test/features/test_document_migrate_v31.ts @@ -0,0 +1,16 @@ +import { IMigrateDocument, OpenApi, OpenApiV3_1 } from "@samchon/openapi"; +import fs from "fs"; +import typia from "typia"; + +export const test_document_migrate_v31 = async (): Promise => { + const path: string = `${__dirname}/../../../examples/v3.1`; + for (const file of await fs.promises.readdir(path)) { + if (file.endsWith(".json") === false) continue; + const swagger: OpenApiV3_1.IDocument = typia.assert( + JSON.parse(await fs.promises.readFile(`${path}/${file}`, "utf8")), + ); + const openapi: OpenApi.IDocument = OpenApi.convert(swagger); + const migrate: IMigrateDocument = OpenApi.migrate(openapi); + typia.assert(migrate); + } +}; diff --git a/test/features/test_downgrade_v20.ts b/test/features/test_json_schema_downgrade_v20.ts similarity index 92% rename from test/features/test_downgrade_v20.ts rename to test/features/test_json_schema_downgrade_v20.ts index 5bc5f44..da70757 100644 --- a/test/features/test_downgrade_v20.ts +++ b/test/features/test_json_schema_downgrade_v20.ts @@ -2,7 +2,7 @@ import { TestValidator } from "@nestia/e2e"; import { OpenApi, SwaggerV2 } from "@samchon/openapi"; import { SwaggerV2Downgrader } from "@samchon/openapi/lib/internal/SwaggerV2Downgrader"; -export const test_downgrade_v20 = () => { +export const test_json_schema_downgrade_v20 = () => { const schema: OpenApi.IJsonSchema = { oneOf: [{ const: "a" }, { const: "b" }, { const: "c" }], title: "something", diff --git a/test/features/test_downgrade_v30.ts b/test/features/test_json_schema_downgrade_v30.ts similarity index 92% rename from test/features/test_downgrade_v30.ts rename to test/features/test_json_schema_downgrade_v30.ts index e12ed42..51bf90c 100644 --- a/test/features/test_downgrade_v30.ts +++ b/test/features/test_json_schema_downgrade_v30.ts @@ -2,7 +2,7 @@ import { TestValidator } from "@nestia/e2e"; import { OpenApi, OpenApiV3 } from "@samchon/openapi"; import { OpenApiV3Downgrader } from "@samchon/openapi/lib/internal/OpenApiV3Downgrader"; -export const test_downgrade_v30 = () => { +export const test_json_schema_downgrade_v30 = () => { const schema: OpenApi.IJsonSchema = { oneOf: [{ const: "a" }, { const: "b" }, { const: "c" }], title: "something", diff --git a/test/features/test_json_schema_type_checker_cover_any.ts b/test/features/test_json_schema_type_checker_cover_any.ts new file mode 100644 index 0000000..f49d294 --- /dev/null +++ b/test/features/test_json_schema_type_checker_cover_any.ts @@ -0,0 +1,74 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApiTypeChecker } from "@samchon/openapi"; + +export const test_json_schema_type_checker_cover_any = (): void => { + TestValidator.equals("any covers (string | null)")(true)( + OpenApiTypeChecker.covers({})( + { + type: undefined, + }, + { + oneOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + ), + ); + TestValidator.equals("any covers union type")(true)( + OpenApiTypeChecker.covers({})( + { + type: undefined, + }, + { + oneOf: [ + { + type: "string", + }, + { + type: "number", + }, + ], + }, + ), + ); + + TestValidator.equals("(string | null) can't cover any")(false)( + OpenApiTypeChecker.covers({})( + { + oneOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + { + type: undefined, + }, + ), + ); + TestValidator.equals("union can't cover any")(false)( + OpenApiTypeChecker.covers({})( + { + oneOf: [ + { + type: "string", + }, + { + type: "number", + }, + ], + }, + { + type: undefined, + }, + ), + ); +}; diff --git a/test/features/test_json_schema_type_checker_cover_array.ts b/test/features/test_json_schema_type_checker_cover_array.ts new file mode 100644 index 0000000..875d762 --- /dev/null +++ b/test/features/test_json_schema_type_checker_cover_array.ts @@ -0,0 +1,167 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApi, OpenApiTypeChecker } from "@samchon/openapi"; +import typia, { IJsonApplication } from "typia"; + +export const test_json_schema_type_checker_cover_array = (): void => { + const app: IJsonApplication = + typia.json.application<[Plan2D, Plan3D, Box2D, Box3D]>(); + const components: OpenApi.IComponents = app.components as any; + + const plan2D: OpenApi.IJsonSchema = components.schemas!.Plan2D; + const plan3D: OpenApi.IJsonSchema = components.schemas!.Plan3D; + + const box2D: OpenApi.IJsonSchema = components.schemas!.Box2D; + const box3D: OpenApi.IJsonSchema = components.schemas!.Box3D; + + TestValidator.equals("Plan3D[] covers Plan2D[]")(true)( + OpenApiTypeChecker.covers(components)( + { type: "array", items: plan3D }, + { type: "array", items: plan2D }, + ), + ); + TestValidator.equals("Box3D[] covers Box2D[]")(true)( + OpenApiTypeChecker.covers(components)( + { type: "array", items: box3D }, + { type: "array", items: box2D }, + ), + ); + TestValidator.equals("Array covers Array")(true)( + OpenApiTypeChecker.covers(components)( + { + type: "array", + items: { + oneOf: [plan3D, box3D], + }, + }, + { + type: "array", + items: { + oneOf: [plan2D, box2D], + }, + }, + ), + ); + TestValidator.equals("(Plan3D|Box3D)[] covers (Plan2D|Box2D)[]")(true)( + OpenApiTypeChecker.covers(components)( + { + oneOf: [ + { type: "array", items: plan3D }, + { type: "array", items: box3D }, + ], + }, + { + oneOf: [ + { type: "array", items: plan2D }, + { type: "array", items: box2D }, + ], + }, + ), + ); + + TestValidator.equals("Plan2D[] can't cover Plan3D[]")(false)( + OpenApiTypeChecker.covers(components)( + { type: "array", items: plan2D }, + { type: "array", items: plan3D }, + ), + ); + TestValidator.equals("Box2D[] can't cover Box3D[]")(false)( + OpenApiTypeChecker.covers(components)( + { type: "array", items: box2D }, + { type: "array", items: box3D }, + ), + ); + TestValidator.equals("Array can't cover Array")( + false, + )( + OpenApiTypeChecker.covers(components)( + { + type: "array", + items: { + oneOf: [plan2D, box2D], + }, + }, + { + type: "array", + items: { + oneOf: [plan3D, box3D], + }, + }, + ), + ); + TestValidator.equals("(Plan2D[]|Box2D[]) can't cover (Plan3D[]|Box3D[])")( + false, + )( + OpenApiTypeChecker.covers(components)( + { + oneOf: [ + { type: "array", items: plan2D }, + { type: "array", items: box2D }, + ], + }, + { + oneOf: [ + { type: "array", items: plan3D }, + { type: "array", items: box3D }, + ], + }, + ), + ); + TestValidator.equals("Plan3D[] can't cover (Plan2D|Box2D)[]")(false)( + OpenApiTypeChecker.covers(components)( + { type: "array", items: plan3D }, + { + oneOf: [ + { type: "array", items: plan2D }, + { type: "array", items: box2D }, + ], + }, + ), + ); + TestValidator.equals("Box3D[] can't cover Array")(false)( + OpenApiTypeChecker.covers(components)( + { type: "array", items: box3D }, + { + type: "array", + items: { + oneOf: [plan2D, box2D], + }, + }, + ), + ); +}; + +type Plan2D = { + center: Point2D; + size: Point2D; + geometries: Geometry2D[]; +}; +type Plan3D = { + center: Point3D; + size: Point3D; + geometries: Geometry3D[]; +}; +type Geometry3D = { + position: Point3D; + scale: Point3D; +}; +type Geometry2D = { + position: Point2D; + scale: Point2D; +}; +type Point2D = { + x: number; + y: number; +}; +type Point3D = { + x: number; + y: number; + z: number; +}; +type Box2D = { + size: Point2D; + nested: Box2D; +}; +type Box3D = { + size: Point3D; + nested: Box3D; +}; diff --git a/test/features/test_json_schema_type_checker_cover_nullable.ts b/test/features/test_json_schema_type_checker_cover_nullable.ts new file mode 100644 index 0000000..83839e5 --- /dev/null +++ b/test/features/test_json_schema_type_checker_cover_nullable.ts @@ -0,0 +1,39 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApiTypeChecker } from "@samchon/openapi"; + +export const test_json_schema_type_checker_cover_nullable = (): void => { + TestValidator.equals("(string | null) covers string")(true)( + OpenApiTypeChecker.covers({})( + { + oneOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + { + type: "string", + }, + ), + ); + TestValidator.equals("string can't cover (string | null)")(false)( + OpenApiTypeChecker.covers({})( + { + type: "string", + }, + { + oneOf: [ + { + type: "string", + }, + { + type: "null", + }, + ], + }, + ), + ); +}; diff --git a/test/features/test_json_schema_type_checker_cover_number.ts b/test/features/test_json_schema_type_checker_cover_number.ts new file mode 100644 index 0000000..e208691 --- /dev/null +++ b/test/features/test_json_schema_type_checker_cover_number.ts @@ -0,0 +1,122 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApiTypeChecker } from "@samchon/openapi"; + +export const test_json_schema_type_checker_cover_number = (): void => { + //---- + // SUCCESS SCENARIOS + //---- + // COMMON + TestValidator.equals("number covers integer")(true)( + OpenApiTypeChecker.covers({})({ type: "number" }, { type: "integer" }), + ); + TestValidator.equals("multipleOf covers multiplied")(true)( + OpenApiTypeChecker.covers({})( + { type: "number", multipleOf: 3 }, + { type: "number", multipleOf: 9 }, + ), + ); + TestValidator.equals("enum cover relationship")(true)( + OpenApiTypeChecker.covers({})( + { + oneOf: [{ const: 1 }, { const: 2 }, { const: 3 }], + }, + { + oneOf: [{ const: 1 }, { const: 2 }], + }, + ), + ); + + // MINIMUM + TestValidator.equals("minimum covers when equal")(true)( + OpenApiTypeChecker.covers({})( + { type: "number", minimum: 1 }, + { type: "number", minimum: 1 }, + ), + ); + TestValidator.equals("minimum covers when less")(true)( + OpenApiTypeChecker.covers({})( + { type: "number", minimum: 1 }, + { type: "number", minimum: 2 }, + ), + ); + TestValidator.equals("exclusiveMinimum covers minimum only when less")(true)( + OpenApiTypeChecker.covers({})( + { type: "number", minimum: 1, exclusiveMinimum: true }, + { type: "number", minimum: 2 }, + ), + ); + + // MAXIMUM + TestValidator.equals("maximum covers when equal")(true)( + OpenApiTypeChecker.covers({})( + { type: "number", maximum: 2 }, + { type: "number", maximum: 2 }, + ), + ); + TestValidator.equals("maximum covers when greater")(true)( + OpenApiTypeChecker.covers({})( + { type: "number", maximum: 2 }, + { type: "number", maximum: 1 }, + ), + ); + TestValidator.equals("exclusiveMaximum covers minimum only when greater")( + true, + )( + OpenApiTypeChecker.covers({})( + { type: "number", maximum: 2, exclusiveMaximum: true }, + { type: "number", maximum: 1 }, + ), + ); + + //---- + // FAILURE SCENARIOS + //---- + // COMMON + TestValidator.equals("integer can't cover number")(false)( + OpenApiTypeChecker.covers({})({ type: "integer" }, { type: "number" }), + ); + TestValidator.equals("multipleOf can't cover none multiplied")(false)( + OpenApiTypeChecker.covers({})( + { type: "number", multipleOf: 3 }, + { type: "number", multipleOf: 4 }, + ), + ); + TestValidator.equals("enum non cover (but covered) relationship")(false)( + OpenApiTypeChecker.covers({})( + { + oneOf: [{ const: 1 }, { const: 2 }], + }, + { + oneOf: [{ const: 1 }, { const: 2 }, { const: 3 }], + }, + ), + ); + + // MINIMUM + TestValidator.equals("minimum can't cover when greater")(false)( + OpenApiTypeChecker.covers({})( + { type: "number", minimum: 2 }, + { type: "number", minimum: 1 }, + ), + ); + TestValidator.equals("exclusiveMinimum can't cover equal")(false)( + OpenApiTypeChecker.covers({})( + { type: "number", minimum: 1, exclusiveMinimum: true }, + { type: "number", minimum: 1 }, + ), + ); + + // MAXIMUM + TestValidator.equals("maximum can't cover when less")(false)( + OpenApiTypeChecker.covers({})( + { type: "number", maximum: 1 }, + { type: "number", maximum: 2 }, + ), + ); + TestValidator.equals("exclusiveMaximum can't cover equal")(false)( + OpenApiTypeChecker.covers({})( + { type: "number", maximum: 2, exclusiveMaximum: true }, + { type: "number", maximum: 2 }, + ), + ); +}; diff --git a/test/features/test_json_schema_type_checker_cover_object.ts b/test/features/test_json_schema_type_checker_cover_object.ts new file mode 100644 index 0000000..78f7b03 --- /dev/null +++ b/test/features/test_json_schema_type_checker_cover_object.ts @@ -0,0 +1,244 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApi, OpenApiTypeChecker } from "@samchon/openapi"; +import typia, { IJsonApplication } from "typia"; + +export const test_json_schema_type_checker_cover_object = (): void => { + const app: IJsonApplication = + typia.json.application<[Plan2D, Plan3D, Box2D, Box3D]>(); + const components: OpenApi.IComponents = app.components as any; + + const plan2D: OpenApi.IJsonSchema = components.schemas!.Plan2D; + const plan3D: OpenApi.IJsonSchema = components.schemas!.Plan3D; + const box2D: OpenApi.IJsonSchema = components.schemas!.Box2D; + const box3D: OpenApi.IJsonSchema = components.schemas!.Box3D; + + //---- + // SUCCESS SCENARIOS + //---- + // SINGLE OBJECT TYPE + TestValidator.equals("Plan3D covers Plan2D")(true)( + OpenApiTypeChecker.covers(components)(plan3D, plan2D), + ); + TestValidator.equals("Box3D covers Box2D")(true)( + OpenApiTypeChecker.covers(components)(box3D, box2D), + ); + + // UNION TYPE + TestValidator.equals("(Plan3D|Box3D) covers Plan2D")(true)( + OpenApiTypeChecker.covers(components)({ oneOf: [plan3D, box3D] }, plan2D), + ); + TestValidator.equals("(Plan3D|Box3D) covers Box2D")(true)( + OpenApiTypeChecker.covers(components)({ oneOf: [plan3D, box3D] }, box2D), + ); + TestValidator.equals("(Plan3D|Box3D) covers (Plan2D|Box2D)")(true)( + OpenApiTypeChecker.covers(components)( + { oneOf: [plan3D, box3D] }, + { oneOf: [box2D, box2D] }, + ), + ); + + // DYNAMIC FEATURES + TestValidator.equals("optional covers required")(true)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + properties: { + id: { type: "string" }, + }, + }, + { + type: "object", + properties: { + id: { type: "string" }, + }, + required: ["id"], + }, + ), + ); + TestValidator.equals("(additionalProperties := true) cover static")(true)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + additionalProperties: true, + }, + { + type: "object", + }, + ), + ); + TestValidator.equals("(addtionalProperties := object) covers static")(true)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + additionalProperties: { + type: "object", + }, + }, + { + type: "object", + }, + ), + ); + TestValidator.equals("(addtionalProperties := true) covers everything")(true)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + additionalProperties: true, + }, + { + type: "object", + additionalProperties: { + type: "object", + properties: { + id: { type: "string" }, + }, + }, + }, + ), + ); + TestValidator.equals("addtionalProperties covers relationship")(true)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + additionalProperties: box3D, + }, + { + type: "object", + additionalProperties: box2D, + }, + ), + ); + + //---- + // FAILURE SCENARIOS + //---- + // SINGLE OBJECT TYPE + TestValidator.equals("Plan2D can't cover Plan3D")(false)( + OpenApiTypeChecker.covers(components)(plan2D, plan3D), + ); + TestValidator.equals("Box2D can't cover Box3D")(false)( + OpenApiTypeChecker.covers(components)(box2D, box3D), + ); + + // UNION TYPE + TestValidator.equals("Plan3D can't cover (Plan2D|Box2D)")(false)( + OpenApiTypeChecker.covers(components)(plan3D, { oneOf: [plan2D, box2D] }), + ); + TestValidator.equals("Box3D can't cover (Plan2D|Box2D)")(false)( + OpenApiTypeChecker.covers(components)(box3D, { oneOf: [plan2D, box2D] }), + ); + + // DYNAMIC FEATURES + TestValidator.equals("required can't cover optional")(false)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + properties: { + id: { type: "string" }, + }, + required: ["id"], + }, + { + type: "object", + properties: { + id: { type: "string" }, + }, + }, + ), + ); + TestValidator.equals("static can't cover (additionalProperties := true)")( + false, + )( + OpenApiTypeChecker.covers(components)( + { + type: "object", + }, + { + type: "object", + additionalProperties: true, + }, + ), + ); + TestValidator.equals("static can't cover (additionalProperties := object)")( + false, + )( + OpenApiTypeChecker.covers(components)( + { + type: "object", + }, + { + type: "object", + additionalProperties: { + type: "object", + }, + }, + ), + ); + TestValidator.equals("nothing can cover (addtionalProperties := true)")( + false, + )( + OpenApiTypeChecker.covers(components)( + { + type: "object", + additionalProperties: { + type: "object", + properties: { + id: { type: "string" }, + }, + }, + }, + { + type: "object", + additionalProperties: true, + }, + ), + ); + TestValidator.equals("relationship can't cover addtionalProperties")(false)( + OpenApiTypeChecker.covers(components)( + { + type: "object", + additionalProperties: box2D, + }, + { + type: "object", + additionalProperties: box3D, + }, + ), + ); +}; + +type Plan2D = { + center: Point2D; + size: Point2D; + geometries: Geometry2D[]; +}; +type Plan3D = { + center: Point3D; + size: Point3D; + geometries: Geometry3D[]; +}; +type Geometry3D = { + position: Point3D; + scale: Point3D; +}; +type Geometry2D = { + position: Point2D; + scale: Point2D; +}; +type Point2D = { + x: number; + y: number; +}; +type Point3D = { + x: number; + y: number; + z: number; +}; +type Box2D = { + size: Point2D; + nested: Box2D; +}; +type Box3D = { + size: Point3D; + nested: Box3D; +}; diff --git a/test/features/test_json_schema_type_checker_cover_string.ts b/test/features/test_json_schema_type_checker_cover_string.ts new file mode 100644 index 0000000..de7ee19 --- /dev/null +++ b/test/features/test_json_schema_type_checker_cover_string.ts @@ -0,0 +1,128 @@ +import { TestValidator } from "@nestia/e2e"; +import { OpenApiTypeChecker } from "@samchon/openapi"; +import typia, { tags } from "typia"; + +export const test_json_schema_type_checker_cover_string = (): void => { + // SUCCESS SCENARIOS + TestValidator.equals("enum cover relationship")(true)( + OpenApiTypeChecker.covers({})( + { + oneOf: [ + { + const: "a", + }, + { + const: "b", + }, + { + const: "c", + }, + ], + }, + { + oneOf: [ + { + const: "a", + }, + { + const: "b", + }, + ], + }, + ), + ); + TestValidator.equals("minLength covers when equal")(true)( + OpenApiTypeChecker.covers({})( + { type: "string", minLength: 1 }, + { type: "string", minLength: 1 }, + ), + ); + TestValidator.equals("minLength covers when less")(true)( + OpenApiTypeChecker.covers({})( + { type: "string", minLength: 1 }, + { type: "string", minLength: 2 }, + ), + ); + TestValidator.equals("maxLength covers when equal")(true)( + OpenApiTypeChecker.covers({})( + { type: "string", maxLength: 2 }, + { type: "string", maxLength: 2 }, + ), + ); + TestValidator.equals("maxLength covers when greater")(true)( + OpenApiTypeChecker.covers({})( + { type: "string", maxLength: 2 }, + { type: "string", maxLength: 1 }, + ), + ); + TestValidator.equals("pattern covers when equal")(true)( + OpenApiTypeChecker.covers({})( + { type: "string", pattern: "^a.*" }, + { type: "string", pattern: "^a.*" }, + ), + ); + + // FAILURE SCENARIOS + TestValidator.equals("enum non cover (but covered) relationship")(false)( + OpenApiTypeChecker.covers({})( + { + oneOf: [ + { + const: "a", + }, + { + const: "b", + }, + ], + }, + { + oneOf: [ + { + const: "a", + }, + { + const: "b", + }, + { + const: "c", + }, + ], + }, + ), + ); + TestValidator.equals("minLength can't cover when greater")(false)( + OpenApiTypeChecker.covers({})( + { type: "string", minLength: 2 }, + { type: "string", minLength: 1 }, + ), + ); + TestValidator.equals("maxLength can't cover when less")(false)( + OpenApiTypeChecker.covers({})( + { type: "string", maxLength: 1 }, + { type: "string", maxLength: 2 }, + ), + ); + TestValidator.equals("pattern can't cover when different")(false)( + OpenApiTypeChecker.covers({})( + { type: "string", pattern: "^a.*" }, + { type: "string", pattern: "^b.*" }, + ), + ); + + // CHECK FORMAT CASE + for (const x of typia.misc.literals()) + for (const y of typia.misc.literals()) + TestValidator.equals(`format ${x} covers ${y}`)( + x === y || + (x === "idn-email" && y === "email") || + (x === "idn-hostname" && y === "hostname") || + (["uri", "iri"].includes(x) && y === "url") || + (x === "iri" && y === "uri") || + (x === "iri-reference" && y === "uri-reference"), + )( + OpenApiTypeChecker.covers({})( + { type: "string", format: x }, + { type: "string", format: y }, + ), + ); +}; diff --git a/test/index.ts b/test/index.ts index 8a55354..ad8fdd7 100644 --- a/test/index.ts +++ b/test/index.ts @@ -1,90 +1,25 @@ -import { - IMigrateDocument, - OpenApi, - OpenApiV3, - OpenApiV3_1, - SwaggerV2, -} from "@samchon/openapi"; -import fs from "fs"; -import path from "path"; -import typia from "typia"; +import { DynamicExecutor } from "@nestia/e2e"; -import { test_downgrade_v20 } from "./features/test_downgrade_v20"; -import { test_downgrade_v30 } from "./features/test_downgrade_v30"; - -const CONVERTED: string = `${__dirname}/../../examples/converted`; - -const validate = ( - title: string, - document: - | SwaggerV2.IDocument - | OpenApiV3.IDocument - | OpenApiV3_1.IDocument - | OpenApi.IDocument, -): OpenApi.IDocument => { - console.log(" -", title); - console.log(" - type assertion"); - typia.assert(document); - - console.log(" - convert to emended v3.1"); - return typia.assert(OpenApi.convert(document)); -}; -const iterate = async (directory: string): Promise => { - for (const file of await fs.promises.readdir(directory)) { - const location: string = `${directory}/${file}`; - const stat: fs.Stats = await fs.promises.stat(location); - const name: string = file.substring(0, file.length - 5); - - if (stat.isDirectory() === true) await iterate(location); - else if (file.endsWith(".json") === true) { - const document: OpenApi.IDocument = await validate( - path.resolve(location), - JSON.parse(await fs.promises.readFile(location, "utf8")), - ); - await fs.promises.writeFile( - `${CONVERTED}/${name}-v31.json`, - JSON.stringify(document, null, 2), - ); - - console.log(` - downgrade to v2.0`); - const v20: SwaggerV2.IDocument = OpenApi.downgrade(document, "2.0"); - typia.assert(v20); - typia.assert(OpenApi.convert(v20)); - await fs.promises.writeFile( - `${CONVERTED}/${name}-v20.json`, - JSON.stringify(v20, null, 2), - ); - - console.log(` - downgrade to v3.0`); - const v30: OpenApiV3.IDocument = OpenApi.downgrade(document, "3.0"); - typia.assert(v30); - typia.assert(OpenApi.convert(v30)); - await fs.promises.writeFile( - `${CONVERTED}/${name}-v30.json`, - JSON.stringify(v30, null, 2), - ); - - console.log(` - migration`); - const result: IMigrateDocument = OpenApi.migrate(document); - typia.assert(result); - await fs.promises.writeFile( - `${CONVERTED}/${name}-migration.json`, - JSON.stringify(result, null, 2), - ); - } - } -}; const main = async (): Promise => { - try { - await fs.promises.mkdir(CONVERTED); - } catch {} - console.log("Test OpenAPI conversion"); - await iterate(`${__dirname}/../../examples/v2.0`); - await iterate(`${__dirname}/../../examples/v3.0`); - await iterate(`${__dirname}/../../examples/v3.1`); - - test_downgrade_v20(); - test_downgrade_v30(); + // DO TEST + const report: DynamicExecutor.IReport = await DynamicExecutor.validate({ + prefix: "test_", + parameters: () => [], + })(__dirname + "/features"); + + // REPORT EXCEPTIONS + const exceptions: Error[] = report.executions + .filter((exec) => exec.error !== null) + .map((exec) => exec.error!); + if (exceptions.length === 0) { + console.log("Success"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + } else { + for (const exp of exceptions) console.log(exp); + console.log("Failed"); + console.log("Elapsed time", report.time.toLocaleString(), `ms`); + } + if (exceptions.length) process.exit(-1); }; main().catch((exp) => { console.error(exp);