diff --git a/package.json b/package.json index 3939b6a..99b7a12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@samchon/openapi", - "version": "2.0.0-dev.20241129", + "version": "2.0.0-dev.20241129-4", "description": "OpenAPI definitions and converters for 'typia' and 'nestia'.", "main": "./lib/index.js", "module": "./lib/index.mjs", diff --git a/src/converters/ChatGptConverter.ts b/src/converters/ChatGptConverter.ts index 7ae4417..bcaf397 100644 --- a/src/converters/ChatGptConverter.ts +++ b/src/converters/ChatGptConverter.ts @@ -21,6 +21,7 @@ export namespace ChatGptConverter { reference: props.config.reference, constraint: false, }, + validate: validate(props.errors), }); if (params === null) return null; for (const key of Object.keys(params.$defs)) @@ -44,19 +45,7 @@ export namespace ChatGptConverter { reference: props.config.reference, constraint: false, }, - validate: (schema, accessor) => { - if ( - OpenApiTypeChecker.isObject(schema) && - !!schema.additionalProperties - ) { - if (props.errors) - props.errors.push( - `${accessor}.additionalProperties: ChatGPT does not allow additionalProperties, the dynamic key typed object.`, - ); - return false; - } - return true; - }, + validate: validate(props.errors), }); if (schema === null) return null; for (const key of Object.keys(props.$defs)) @@ -65,6 +54,22 @@ export namespace ChatGptConverter { return transform(schema); }; + const validate = + (errors: string[] | undefined) => + (schema: OpenApi.IJsonSchema, accessor: string): boolean => { + if ( + OpenApiTypeChecker.isObject(schema) && + !!schema.additionalProperties + ) { + if (errors) + errors.push( + `${accessor}.additionalProperties: ChatGPT does not allow additionalProperties, the dynamic key typed object.`, + ); + return false; + } + return true; + }; + const transform = (schema: ILlmSchemaV3_1): IChatGptSchema => { const union: Array = []; const attribute: IChatGptSchema.__IAttribute = { diff --git a/src/converters/GeminiConverter.ts b/src/converters/GeminiConverter.ts index bd775b8..be6f24f 100644 --- a/src/converters/GeminiConverter.ts +++ b/src/converters/GeminiConverter.ts @@ -2,6 +2,7 @@ import { OpenApi } from "../OpenApi"; import { IGeminiSchema } from "../structures/IGeminiSchema"; import { ILlmSchemaV3 } from "../structures/ILlmSchemaV3"; import { LlmTypeCheckerV3 } from "../utils/LlmTypeCheckerV3"; +import { MapUtil } from "../utils/MapUtil"; import { OpenApiTypeChecker } from "../utils/OpenApiTypeChecker"; import { LlmConverterV3 } from "./LlmConverterV3"; import { LlmParametersFinder } from "./LlmParametersFinder"; @@ -47,6 +48,40 @@ export namespace GeminiConverter { return false; } } else if (OpenApiTypeChecker.isOneOf(next)) { + // NULLABLE CASE + const notNull = next.oneOf.filter( + (v) => OpenApiTypeChecker.isNull(v) === false, + ); + if (notNull.length < 2) return true; + + // ENUM CASE + const constants: OpenApi.IJsonSchema.IConstant[] = notNull.filter( + (v) => OpenApiTypeChecker.isConstant(v), + ); + const dict: Map<"boolean" | "number" | "string", any> = new Map(); + for (const v of constants) + MapUtil.take(dict)(typeof v.const as "number")(() => []).push( + v.const, + ); + if (dict.size === 1) { + if (notNull.length === constants.length) return true; + const atomic = notNull.filter( + (v) => + OpenApiTypeChecker.isBoolean(v) || + OpenApiTypeChecker.isInteger(v) || + OpenApiTypeChecker.isNumber(v) || + OpenApiTypeChecker.isString(v), + ); + if (atomic.length === 1) + if (atomic[0].type === "integer") + return ( + dict.has("number") && + dict.get("number")!.every((v: number) => Number.isInteger(v)) + ); + else return dict.has(atomic[0].type); + } + + // REAL ONE-OF TYPE if (props.errors) props.errors.push(`${accessor}: Gemini does not allow union type.`); return false; diff --git a/src/converters/LlmConverterV3_1.ts b/src/converters/LlmConverterV3_1.ts index 8788e0c..0e24c1e 100644 --- a/src/converters/LlmConverterV3_1.ts +++ b/src/converters/LlmConverterV3_1.ts @@ -13,6 +13,7 @@ export namespace LlmConverterV3_1 { schema: OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference; errors?: string[]; accessor?: string; + validate?: (input: OpenApi.IJsonSchema, accessor: string) => boolean; }): ILlmSchemaV3_1.IParameters | null => { const entity: OpenApi.IJsonSchema.IObject | null = LlmParametersFinder.find(props); diff --git a/test/features/llm/gemini/test_gemini_schema_enum.ts b/test/features/llm/gemini/test_gemini_schema_enum.ts new file mode 100644 index 0000000..5abf2c0 --- /dev/null +++ b/test/features/llm/gemini/test_gemini_schema_enum.ts @@ -0,0 +1,25 @@ +import { TestValidator } from "@nestia/e2e"; +import { LlmSchemaConverter } from "@samchon/openapi/lib/converters/LlmSchemaConverter"; +import typia, { IJsonSchemaCollection, tags } from "typia"; + +export const test_gemini_schema_enum = (): void => { + const collection: IJsonSchemaCollection = + typia.json.schemas< + [ + 0 | 1 | 2, + (number & {}) | 1.2 | 2.3 | 3.4, + (number & tags.Type<"int32">) | 1 | 2 | 3, + "one" | "two" | "three", + ] + >(); + for (const schema of collection.schemas) { + const errors: string[] = []; + const gemini = LlmSchemaConverter.schema("gemini")({ + config: LlmSchemaConverter.defaultConfig("gemini"), + components: collection.components, + schema, + errors, + }); + TestValidator.equals("success")(!!gemini)(true); + } +}; diff --git a/test/features/llm/gemini/test_gemini_schema_nullable.ts b/test/features/llm/gemini/test_gemini_schema_nullable.ts new file mode 100644 index 0000000..1d64807 --- /dev/null +++ b/test/features/llm/gemini/test_gemini_schema_nullable.ts @@ -0,0 +1,46 @@ +import { TestValidator } from "@nestia/e2e"; +import { LlmSchemaConverter } from "@samchon/openapi/lib/converters/LlmSchemaConverter"; +import typia, { IJsonSchemaCollection } from "typia"; + +export const test_gemini_schema_nullable = (): void => { + const collection: IJsonSchemaCollection = typia.json.schemas< + [ + 0 | 1 | 2 | 3 | null, + (number & {}) | 1.2 | 2.3 | 3.4 | null, + { + id: string | null; + value: number; + } | null, + Array | null, + Array<{ + nested: Array<{ + id: string | null; + }>; + nullable: Array; + }>, + { + first: ToJsonNull; + second: ToJsonNull | null; + }, + { + first: ToJsonNull | null; + second: ToJsonNull | null; + third?: ToJsonNull | null; + }, + ] + >(); + for (const schema of collection.schemas) { + const errors: string[] = []; + const gemini = LlmSchemaConverter.schema("gemini")({ + config: LlmSchemaConverter.defaultConfig("gemini"), + components: collection.components, + schema, + errors, + }); + TestValidator.equals("success")(!!gemini)(true); + } +}; + +interface ToJsonNull { + toJSON: () => null; +}