From ba457c823b0388cb90f9b602253604acf05c2982 Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sat, 27 Jul 2024 23:30:13 +0900 Subject: [PATCH 1/2] Close #1181: support enumeration level description comment in JSON schema. --- benchmark/package.json | 2 +- debug/features/enum.ts | 39 +++++++++++++++ debug/features/internal/TestValidator.ts | 47 +++++++++++++++++++ debug/package.json | 2 +- errors/package.json | 2 +- package.json | 2 +- packages/typescript-json/package.json | 4 +- .../metadata/iterate_metadata_constant.ts | 13 +++++ .../internal/application_description.ts | 6 +-- src/programmers/internal/application_title.ts | 20 ++++++++ .../internal/application_v31_alias.ts | 13 +---- .../internal/application_v31_constant.ts | 4 ++ .../internal/application_v31_object.ts | 19 +------- .../metadata/IMetadataConstantValue.ts | 3 ++ src/schemas/metadata/MetadataConstantValue.ts | 5 ++ test-esm/package.json | 2 +- test/package.json | 2 +- test/schemas/json/v3_1/UltimateUnion.json | 2 + ...issue_1179_tuple_type_in_dynamic_object.ts | 14 ++++-- .../test_issue_1181_enum_description.ts | 39 +++++++++++++++ 20 files changed, 197 insertions(+), 43 deletions(-) create mode 100644 debug/features/enum.ts create mode 100644 debug/features/internal/TestValidator.ts create mode 100644 src/programmers/internal/application_title.ts create mode 100644 test/src/features/issues/test_issue_1181_enum_description.ts diff --git a/benchmark/package.json b/benchmark/package.json index 2467d15459..4a21342bfc 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -72,6 +72,6 @@ "suppress-warnings": "^1.0.2", "tstl": "^3.0.0", "uuid": "^9.0.1", - "typia": "../typia-6.5.5.tgz" + "typia": "../typia-6.6.0-dev.20240727.tgz" } } \ No newline at end of file diff --git a/debug/features/enum.ts b/debug/features/enum.ts new file mode 100644 index 0000000000..dcb6fd5959 --- /dev/null +++ b/debug/features/enum.ts @@ -0,0 +1,39 @@ +import typia from "typia"; + +import { TestValidator } from "./internal/TestValidator"; + +const app = typia.json.application<[ConstEnum]>(); +console.log(app.components.schemas?.ConstEnum); + +TestValidator.equals("enum-description")(app.components.schemas?.ConstEnum)({ + oneOf: [ + { + const: 1, + title: "The value one", + description: + "The value one.\n\nThe value one defined in the constant enumeration.", + }, + { + const: 2, + title: "The value two", + description: + "The value two.\n\nThe value two defined in the constant enumeration.", + }, + ], +}); + +const enum ConstEnum { + /** + * The value one. + * + * The value one defined in the constant enumeration. + */ + ONE = 1, + + /** + * The value two. + * + * The value two defined in the constant enumeration. + */ + TWO = 2, +} diff --git a/debug/features/internal/TestValidator.ts b/debug/features/internal/TestValidator.ts new file mode 100644 index 0000000000..a2d533e47a --- /dev/null +++ b/debug/features/internal/TestValidator.ts @@ -0,0 +1,47 @@ +export namespace TestValidator { + export const equals = + (title: string, exception: (key: string) => boolean = () => false) => + (x: T) => + (y: T) => { + const diff: string[] = json_equal_to(exception)(x)(y); + if (diff.length) + throw new Error( + `Bug on ${title}: found different values - [${diff.join(", ")}]`, + ); + }; +} + +const json_equal_to = + (exception: (key: string) => boolean) => + (x: T) => + (y: T): string[] => { + const container: string[] = []; + const iterate = + (accessor: string) => + (x: any) => + (y: any): void => { + if (typeof x !== typeof y) container.push(accessor); + else if (x instanceof Array) + if (!(y instanceof Array)) container.push(accessor); + else array(accessor)(x)(y); + else if (x instanceof Object) object(accessor)(x)(y); + else if (x !== y) container.push(accessor); + }; + const array = + (accessor: string) => + (x: any[]) => + (y: any[]): void => { + if (x.length !== y.length) container.push(`${accessor}.length`); + x.forEach((xItem, i) => iterate(`${accessor}[${i}]`)(xItem)(y[i])); + }; + const object = + (accessor: string) => + (x: any) => + (y: any): void => + Object.keys(x) + .filter((key) => x[key] !== undefined && !exception(key)) + .forEach((key) => iterate(`${accessor}.${key}`)(x[key])(y[key])); + + iterate("")(x)(y); + return container; + }; diff --git a/debug/package.json b/debug/package.json index b72521d721..33baeaf943 100644 --- a/debug/package.json +++ b/debug/package.json @@ -16,6 +16,6 @@ "typescript": "^5.4.2" }, "dependencies": { - "typia": "../typia-6.5.5.tgz" + "typia": "../typia-6.6.0-dev.20240727.tgz" } } \ No newline at end of file diff --git a/errors/package.json b/errors/package.json index 475a83de28..1d4805b4df 100644 --- a/errors/package.json +++ b/errors/package.json @@ -32,6 +32,6 @@ "typescript": "^5.3.2" }, "dependencies": { - "typia": "../typia-6.5.5.tgz" + "typia": "../typia-6.6.0-dev.20240727.tgz" } } \ No newline at end of file diff --git a/package.json b/package.json index 76ad6d7a55..a9bbbc4f18 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typia", - "version": "6.5.5", + "version": "6.6.0-dev.20240727", "description": "Superfast runtime validators with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/typescript-json/package.json b/packages/typescript-json/package.json index 0f2e1f0bf6..b9b7a35433 100644 --- a/packages/typescript-json/package.json +++ b/packages/typescript-json/package.json @@ -1,6 +1,6 @@ { "name": "typescript-json", - "version": "6.5.5", + "version": "6.6.0-dev.20240727", "description": "Superfast runtime validators with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -63,7 +63,7 @@ }, "homepage": "https://typia.io", "dependencies": { - "typia": "6.5.5" + "typia": "6.6.0-dev.20240727" }, "peerDependencies": { "typescript": ">=4.8.0 <5.6.0" diff --git a/src/factories/internal/metadata/iterate_metadata_constant.ts b/src/factories/internal/metadata/iterate_metadata_constant.ts index 57a304ab0b..cfb3a1cb76 100644 --- a/src/factories/internal/metadata/iterate_metadata_constant.ts +++ b/src/factories/internal/metadata/iterate_metadata_constant.ts @@ -6,6 +6,7 @@ import { MetadataConstantValue } from "../../../schemas/metadata/MetadataConstan import { ArrayUtil } from "../../../utils/ArrayUtil"; +import { CommentFactory } from "../../CommentFactory"; import { MetadataFactory } from "../../MetadataFactory"; export const iterate_metadata_constant = @@ -15,6 +16,15 @@ export const iterate_metadata_constant = if (!options.constant) return false; const filter = (flag: ts.TypeFlags) => (type.getFlags() & flag) !== 0; + const comment = () => { + if (!filter(ts.TypeFlags.EnumLiteral)) return {}; + return { + jsDocTags: type.symbol?.getJsDocTags(), + description: type.symbol + ? CommentFactory.description(type.symbol) ?? null + : undefined, + }; + }; if (type.isLiteral()) { const value: string | number | bigint = typeof type.value === "object" @@ -34,11 +44,13 @@ export const iterate_metadata_constant = MetadataConstantValue.create({ value, tags: [], + ...comment(), }), (a, b) => a.value === b.value, ); return true; } else if (filter(ts.TypeFlags.BooleanLiteral)) { + comment(); const value: boolean = checker.typeToString(type) === "true"; const constant: MetadataConstant = ArrayUtil.take( meta.constants, @@ -54,6 +66,7 @@ export const iterate_metadata_constant = MetadataConstantValue.create({ value, tags: [], + ...comment(), }), (a, b) => a.value === b.value, ); diff --git a/src/programmers/internal/application_description.ts b/src/programmers/internal/application_description.ts index f3a656f238..4a1ff08bf3 100644 --- a/src/programmers/internal/application_description.ts +++ b/src/programmers/internal/application_description.ts @@ -1,11 +1,11 @@ import { IJsDocTagInfo } from "../../module"; export const application_description = (props: { - description: string | null | undefined; - jsDocTags: IJsDocTagInfo[]; + description?: string | null | undefined; + jsDocTags?: IJsDocTagInfo[]; }): string | undefined => props.jsDocTags - .find((tag) => tag.name === "description") + ?.find((tag) => tag.name === "description") ?.text?.[0]?.text?.split("\r\n") .join("\n") ?? props.description ?? diff --git a/src/programmers/internal/application_title.ts b/src/programmers/internal/application_title.ts new file mode 100644 index 0000000000..44fac1004d --- /dev/null +++ b/src/programmers/internal/application_title.ts @@ -0,0 +1,20 @@ +import { CommentFactory } from "../../factories/CommentFactory"; + +import { IJsDocTagInfo } from "../../module"; + +export const application_title = (schema: { + description?: string | null | undefined; + jsDocTags?: IJsDocTagInfo[] | undefined; +}): string | undefined => { + const info: IJsDocTagInfo | undefined = schema.jsDocTags?.find( + (tag) => tag.name === "title", + ); + if (info?.text?.length) return CommentFactory.merge(info.text); + else if (!schema.description?.length) return undefined; + + const index: number = schema.description.indexOf("\n"); + const top: string = ( + index === -1 ? schema.description : schema.description.substring(0, index) + ).trim(); + return top.endsWith(".") ? top.substring(0, top.length - 1) : undefined; +}; diff --git a/src/programmers/internal/application_v31_alias.ts b/src/programmers/internal/application_v31_alias.ts index 74317bd70c..f1c7a3030b 100644 --- a/src/programmers/internal/application_v31_alias.ts +++ b/src/programmers/internal/application_v31_alias.ts @@ -1,11 +1,9 @@ import { OpenApi } from "@samchon/openapi"; -import { CommentFactory } from "../../factories/CommentFactory"; - -import { IJsDocTagInfo } from "../../schemas/metadata/IJsDocTagInfo"; import { MetadataAlias } from "../../schemas/metadata/MetadataAlias"; import { application_description } from "./application_description"; +import { application_title } from "./application_title"; import { application_v31_object } from "./application_v31_object"; import { application_v31_schema } from "./application_v31_schema"; @@ -33,14 +31,7 @@ export const application_v31_alias = )(components)({ deprecated: alias.jsDocTags.some((tag) => tag.name === "deprecated") || undefined, - title: (() => { - const info: IJsDocTagInfo | undefined = alias.jsDocTags.find( - (tag) => tag.name === "title", - ); - return info?.text?.length - ? CommentFactory.merge(info.text) - : undefined; - })(), + title: application_title(alias), description: application_description(alias), })(alias.value); if (schema !== null) diff --git a/src/programmers/internal/application_v31_constant.ts b/src/programmers/internal/application_v31_constant.ts index d1a4bbdef6..d7eb0cc42d 100644 --- a/src/programmers/internal/application_v31_constant.ts +++ b/src/programmers/internal/application_v31_constant.ts @@ -2,7 +2,9 @@ import { OpenApi } from "@samchon/openapi"; import { MetadataConstant } from "../../schemas/metadata/MetadataConstant"; +import { application_description } from "./application_description"; import { application_plugin } from "./application_plugin"; +import { application_title } from "./application_title"; /** * @internal @@ -15,6 +17,8 @@ export const application_v31_constant = ( application_plugin( { const: value.value as boolean | number | string, + title: application_title(value), + description: application_description(value), } satisfies OpenApi.IJsonSchema.IConstant, value.tags ?? [], ), diff --git a/src/programmers/internal/application_v31_object.ts b/src/programmers/internal/application_v31_object.ts index 02fb747601..36c4034fa8 100644 --- a/src/programmers/internal/application_v31_object.ts +++ b/src/programmers/internal/application_v31_object.ts @@ -9,6 +9,7 @@ import { MetadataObject } from "../../schemas/metadata/MetadataObject"; import { PatternUtil } from "../../utils/PatternUtil"; import { application_description } from "./application_description"; +import { application_title } from "./application_title"; import { application_v31_schema } from "./application_v31_schema"; import { metadata_to_pattern } from "./metadata_to_pattern"; @@ -67,23 +68,7 @@ const create_object_schema = deprecated: property.jsDocTags.some((tag) => tag.name === "deprecated") || undefined, - title: (() => { - const info: IJsDocTagInfo | undefined = property.jsDocTags.find( - (tag) => tag.name === "title", - ); - if (info?.text?.length) return CommentFactory.merge(info.text); - else if (!property.description?.length) return undefined; - - const index: number = property.description.indexOf("\n"); - const top: string = ( - index === -1 - ? property.description - : property.description.substring(0, index) - ).trim(); - return top.endsWith(".") - ? top.substring(0, top.length - 1) - : undefined; - })(), + title: application_title(property), description: application_description(property), })(property.value); diff --git a/src/schemas/metadata/IMetadataConstantValue.ts b/src/schemas/metadata/IMetadataConstantValue.ts index adf68a886a..ba8cecb90e 100644 --- a/src/schemas/metadata/IMetadataConstantValue.ts +++ b/src/schemas/metadata/IMetadataConstantValue.ts @@ -1,8 +1,11 @@ import { Atomic } from "../../typings/Atomic"; +import { IJsDocTagInfo } from "./IJsDocTagInfo"; import { IMetadataTypeTag } from "./IMetadataTypeTag"; export interface IMetadataConstantValue { value: T; tags: IMetadataTypeTag[][] | undefined; + description?: string | null; + jsDocTags?: IJsDocTagInfo[]; } diff --git a/src/schemas/metadata/MetadataConstantValue.ts b/src/schemas/metadata/MetadataConstantValue.ts index bdf41b9cdf..a2646c52c7 100644 --- a/src/schemas/metadata/MetadataConstantValue.ts +++ b/src/schemas/metadata/MetadataConstantValue.ts @@ -1,16 +1,21 @@ import { ClassProperties } from "../../typings/ClassProperties"; +import { IJsDocTagInfo } from "./IJsDocTagInfo"; import { IMetadataConstantValue } from "./IMetadataConstantValue"; import { IMetadataTypeTag } from "./IMetadataTypeTag"; export class MetadataConstantValue { public readonly value: boolean | bigint | number | string; public tags: IMetadataTypeTag[][] | undefined; + public readonly description?: string | null; + public readonly jsDocTags?: IJsDocTagInfo[]; private name_?: string; private constructor(props: ClassProperties) { this.value = props.value; this.tags = props.tags; + this.description = props.description; + this.jsDocTags = props.jsDocTags; } public static create( diff --git a/test-esm/package.json b/test-esm/package.json index 8118dff9f8..c2d9c83f49 100644 --- a/test-esm/package.json +++ b/test-esm/package.json @@ -36,6 +36,6 @@ "typescript": "^5.4.5" }, "dependencies": { - "typia": "../typia-6.5.5.tgz" + "typia": "../typia-6.6.0-dev.20240727.tgz" } } \ No newline at end of file diff --git a/test/package.json b/test/package.json index 9ab666e470..fc9037a148 100644 --- a/test/package.json +++ b/test/package.json @@ -51,6 +51,6 @@ "suppress-warnings": "^1.0.2", "tstl": "^3.0.0", "uuid": "^9.0.1", - "typia": "../typia-6.5.5.tgz" + "typia": "../typia-6.6.0-dev.20240727.tgz" } } \ No newline at end of file diff --git a/test/schemas/json/v3_1/UltimateUnion.json b/test/schemas/json/v3_1/UltimateUnion.json index 8f37009a0a..1c2e3b049f 100644 --- a/test/schemas/json/v3_1/UltimateUnion.json +++ b/test/schemas/json/v3_1/UltimateUnion.json @@ -93,6 +93,7 @@ "$ref": "#/components/schemas/OpenApi.IJsonSchema.IUnknown" } ], + "title": "Type schema info", "description": "Type schema info.\n\n`OpenApi.IJsonSchema` is a type schema info of the OpenAPI.\n\n`OpenApi.IJsonSchema` basically follows the JSON schema definition of\nOpenAPI v3.1, but a little bit shrinked to remove ambiguous and duplicated\nexpressions of OpenAPI v3.1 for the convenience and clarity.\n\n- Decompose mixed type: {@link OpenApiV3_1.IJsonSchema.IMixed}\n- Resolve nullable property: {@link OpenApiV3_1.IJsonSchema.__ISignificant.nullable}\n- Array type utilizes only single {@link OpenAPI.IJsonSchema.IArray.items}\n- Tuple type utilizes only {@link OpenApi.IJsonSchema.ITuple.prefixItems}\n- Merge {@link OpenApiV3_1.IJsonSchema.IAnyOf} to {@link OpenApi.IJsonSchema.IOneOf}\n- Merge {@link OpenApiV3_1.IJsonSchema.IRecursiveReference} to {@link OpenApi.IJsonSchema.IReference}\n- Merge {@link OpenApiV3_1.IJsonSchema.IAllOf} to {@link OpenApi.IJsonSchema.IObject}" }, "OpenApi.IJsonSchema.IConstant": { @@ -787,6 +788,7 @@ "$ref": "#/components/schemas/OpenApi.ISecurityScheme.IOpenId" } ], + "title": "Security scheme of Swagger Documents", "description": "Security scheme of Swagger Documents.\n\n`OpenApi.ISecurityScheme` is a data structure representing content of\n`securitySchemes` in `swagger.json` file. It is composed with 5 types of\nsecurity schemes as an union type like below." }, "OpenApi.ISecurityScheme.IApiKey": { diff --git a/test/src/features/issues/test_issue_1179_tuple_type_in_dynamic_object.ts b/test/src/features/issues/test_issue_1179_tuple_type_in_dynamic_object.ts index 19009cf94c..014b9015d1 100644 --- a/test/src/features/issues/test_issue_1179_tuple_type_in_dynamic_object.ts +++ b/test/src/features/issues/test_issue_1179_tuple_type_in_dynamic_object.ts @@ -1,10 +1,16 @@ import typia from "typia"; export const test_issue_1179_tuple_type_in_dynamic_object = (): void => { - new Array(100).forEach(() => { - const foo: Foo = typia.random(); - typia.assert(foo); - }); + const foo: Foo = typia.random(); + typia.assert(foo); + + const bar: Foo = { + paths: { + "/": ["home"], + "/about": ["about"], + }, + }; + typia.assert(bar); }; interface Foo { diff --git a/test/src/features/issues/test_issue_1181_enum_description.ts b/test/src/features/issues/test_issue_1181_enum_description.ts new file mode 100644 index 0000000000..81dc265a45 --- /dev/null +++ b/test/src/features/issues/test_issue_1181_enum_description.ts @@ -0,0 +1,39 @@ +import typia from "typia"; + +import { TestValidator } from "../../helpers/TestValidator"; + +export const test_issue_1181_enum_description = (): void => { + const app = typia.json.application<[ConstEnum]>(); + TestValidator.equals("enum-description")(app.components.schemas?.ConstEnum)({ + oneOf: [ + { + const: 1, + title: "The value one", + description: + "The value one.\n\nThe value one defined in the constant enumeration.", + }, + { + const: 2, + title: "The value two", + description: + "The value two.\n\nThe value two defined in the constant enumeration.", + }, + ], + }); +}; + +const enum ConstEnum { + /** + * The value one. + * + * The value one defined in the constant enumeration. + */ + ONE = 1, + + /** + * The value two. + * + * The value two defined in the constant enumeration. + */ + TWO = 2, +} From 14d386c65a3b306b8695a479b982d188aaef342a Mon Sep 17 00:00:00 2001 From: Jeongho Nam Date: Sat, 27 Jul 2024 23:59:26 +0900 Subject: [PATCH 2/2] Publish v6.6.0 update. --- benchmark/package.json | 2 +- errors/package.json | 2 +- package.json | 2 +- packages/typescript-json/package.json | 4 ++-- test-esm/package.json | 2 +- test/package.json | 2 +- website/package.json | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/benchmark/package.json b/benchmark/package.json index 4a21342bfc..46ae33ed06 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -72,6 +72,6 @@ "suppress-warnings": "^1.0.2", "tstl": "^3.0.0", "uuid": "^9.0.1", - "typia": "../typia-6.6.0-dev.20240727.tgz" + "typia": "../typia-6.6.0.tgz" } } \ No newline at end of file diff --git a/errors/package.json b/errors/package.json index 1d4805b4df..d4aa4b4252 100644 --- a/errors/package.json +++ b/errors/package.json @@ -32,6 +32,6 @@ "typescript": "^5.3.2" }, "dependencies": { - "typia": "../typia-6.6.0-dev.20240727.tgz" + "typia": "../typia-6.6.0.tgz" } } \ No newline at end of file diff --git a/package.json b/package.json index a9bbbc4f18..daee82295f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "typia", - "version": "6.6.0-dev.20240727", + "version": "6.6.0", "description": "Superfast runtime validators with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", diff --git a/packages/typescript-json/package.json b/packages/typescript-json/package.json index b9b7a35433..05fbbadce7 100644 --- a/packages/typescript-json/package.json +++ b/packages/typescript-json/package.json @@ -1,6 +1,6 @@ { "name": "typescript-json", - "version": "6.6.0-dev.20240727", + "version": "6.6.0", "description": "Superfast runtime validators with only one line", "main": "lib/index.js", "typings": "lib/index.d.ts", @@ -63,7 +63,7 @@ }, "homepage": "https://typia.io", "dependencies": { - "typia": "6.6.0-dev.20240727" + "typia": "6.6.0" }, "peerDependencies": { "typescript": ">=4.8.0 <5.6.0" diff --git a/test-esm/package.json b/test-esm/package.json index c2d9c83f49..d6d4555df6 100644 --- a/test-esm/package.json +++ b/test-esm/package.json @@ -36,6 +36,6 @@ "typescript": "^5.4.5" }, "dependencies": { - "typia": "../typia-6.6.0-dev.20240727.tgz" + "typia": "../typia-6.6.0.tgz" } } \ No newline at end of file diff --git a/test/package.json b/test/package.json index fc9037a148..d2dc0c30a5 100644 --- a/test/package.json +++ b/test/package.json @@ -51,6 +51,6 @@ "suppress-warnings": "^1.0.2", "tstl": "^3.0.0", "uuid": "^9.0.1", - "typia": "../typia-6.6.0-dev.20240727.tgz" + "typia": "../typia-6.6.0.tgz" } } \ No newline at end of file diff --git a/website/package.json b/website/package.json index e2042e4d44..60059ed5c8 100644 --- a/website/package.json +++ b/website/package.json @@ -37,7 +37,7 @@ "tgrid": "^1.0.2", "tstl": "^3.0.0", "typescript": "^5.5.4", - "typia": "^6.5.5" + "typia": "^6.6.0" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0",