diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index b0c1835ceb..ece1edfbbc 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -195,7 +195,7 @@ importers: '@azure-tools/typespec-azure-core': '>=0.50.0 <1.0.0' '@azure-tools/typespec-azure-resource-manager': '>=0.50.0 <1.0.0' '@azure-tools/typespec-azure-rulesets': '>=0.50.0 <1.0.0' - '@azure-tools/typespec-client-generator-core': '>=0.50.0 <1.0.0' + '@azure-tools/typespec-client-generator-core': '>=0.50.2 <1.0.0' '@azure-tools/typespec-ts': workspace:^0.38.1 '@types/mocha': ^5.2.7 '@types/node': ^18.0.0 @@ -210,17 +210,17 @@ importers: ts-node: ^8.5.2 typescript: ~5.6.2 dependencies: - '@azure-tools/typespec-autorest': 0.50.0_uhdlfwfaqxosr7y2pebvadhcty + '@azure-tools/typespec-autorest': 0.50.0_kvndtfjdk75abzopisqylz2rty '@azure-tools/typespec-azure-core': 0.50.0_oaywhha3am3mqfpwn5f5vofo2y '@azure-tools/typespec-azure-resource-manager': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 - '@azure-tools/typespec-azure-rulesets': 0.50.0_jq7ochw5isoilldyczjvkmq2xi - '@azure-tools/typespec-client-generator-core': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 + '@azure-tools/typespec-azure-rulesets': 0.50.0_gpbzmbee75dkov3o2z2zos6xre + '@azure-tools/typespec-client-generator-core': 0.50.2_ekyyo47wopztcpfcizlgiutz7y '@azure-tools/typespec-ts': link:../typespec-ts '@typespec/compiler': 0.64.0 '@typespec/http': 0.64.0_@typespec+compiler@0.64.0 '@typespec/json-schema': 0.64.0_@typespec+compiler@0.64.0 '@typespec/openapi': 0.64.0_3iuxys3wzzu2f6cvuvfg7gykxi - '@typespec/openapi3': 0.64.0_zeg3iovenpc7wszixzl5q4s6rq + '@typespec/openapi3': 0.64.0_glb6whlqofhhikp2a6rpbf2vk4 '@typespec/rest': 0.64.0_3iuxys3wzzu2f6cvuvfg7gykxi '@typespec/versioning': 0.64.0_@typespec+compiler@0.64.0 prettier: 3.1.1 @@ -238,7 +238,7 @@ importers: '@azure-tools/typespec-autorest': '>=0.50.0 <1.0.0' '@azure-tools/typespec-azure-core': '>=0.50.0 <1.0.0' '@azure-tools/typespec-azure-resource-manager': '>=0.50.0 <1.0.0' - '@azure-tools/typespec-client-generator-core': '>=0.50.0 <1.0.0' + '@azure-tools/typespec-client-generator-core': '>=0.50.2 <1.0.0' '@azure/abort-controller': ^2.1.2 '@azure/core-auth': ^1.6.0 '@azure/core-lro': ^3.1.0 @@ -295,10 +295,10 @@ importers: devDependencies: '@azure-rest/core-client': 2.3.1 '@azure-tools/azure-http-specs': 0.1.0-alpha.5_mgwhnvsnod2voefnqq4m33gc3m - '@azure-tools/typespec-autorest': 0.50.0_uhdlfwfaqxosr7y2pebvadhcty + '@azure-tools/typespec-autorest': 0.50.0_kvndtfjdk75abzopisqylz2rty '@azure-tools/typespec-azure-core': 0.50.0_oaywhha3am3mqfpwn5f5vofo2y '@azure-tools/typespec-azure-resource-manager': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 - '@azure-tools/typespec-client-generator-core': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 + '@azure-tools/typespec-client-generator-core': 0.50.2_ekyyo47wopztcpfcizlgiutz7y '@azure/abort-controller': 2.1.2 '@azure/core-auth': 1.6.0 '@azure/core-lro': 3.1.0 @@ -469,7 +469,7 @@ packages: - supports-color dev: true - /@azure-tools/typespec-autorest/0.50.0_uhdlfwfaqxosr7y2pebvadhcty: + /@azure-tools/typespec-autorest/0.50.0_kvndtfjdk75abzopisqylz2rty: resolution: {integrity: sha512-CYzqN11NGU2HNJcycph7HCpjQoOR+XzyySDi6Z6rsXhZa/XTPDYJtmGHNVHXYGgvxJPxPJ9jm13DiLf/ReJnSA==} engines: {node: '>=18.0.0'} peerDependencies: @@ -484,7 +484,7 @@ packages: dependencies: '@azure-tools/typespec-azure-core': 0.50.0_oaywhha3am3mqfpwn5f5vofo2y '@azure-tools/typespec-azure-resource-manager': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 - '@azure-tools/typespec-client-generator-core': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 + '@azure-tools/typespec-client-generator-core': 0.50.2_ekyyo47wopztcpfcizlgiutz7y '@typespec/compiler': 0.64.0 '@typespec/http': 0.64.0_@typespec+compiler@0.64.0 '@typespec/openapi': 0.64.0_3iuxys3wzzu2f6cvuvfg7gykxi @@ -523,7 +523,7 @@ packages: change-case: 5.4.4 pluralize: 8.0.0 - /@azure-tools/typespec-azure-rulesets/0.50.0_jq7ochw5isoilldyczjvkmq2xi: + /@azure-tools/typespec-azure-rulesets/0.50.0_gpbzmbee75dkov3o2z2zos6xre: resolution: {integrity: sha512-b2YjhaUqPxk53eswZKPzK1IzTJJe/AD+Yi/G15+fiar7oozQJtZpe7ysrSsknzKEoy92iiknsIVa5giRie0ATg==} engines: {node: '>=18.0.0'} peerDependencies: @@ -534,12 +534,12 @@ packages: dependencies: '@azure-tools/typespec-azure-core': 0.50.0_oaywhha3am3mqfpwn5f5vofo2y '@azure-tools/typespec-azure-resource-manager': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 - '@azure-tools/typespec-client-generator-core': 0.50.0_ziwq2vcxhnefmvyineli4kclg4 + '@azure-tools/typespec-client-generator-core': 0.50.2_ekyyo47wopztcpfcizlgiutz7y '@typespec/compiler': 0.64.0 dev: false - /@azure-tools/typespec-client-generator-core/0.50.0_ziwq2vcxhnefmvyineli4kclg4: - resolution: {integrity: sha512-Zk62SZb6W5neTtajcQAKll4zYSf3aKaMEDLymMTajXTsWxAlrb7sqnc8vTZWSIymaRI0A9olEL2luw9OLywUYA==} + /@azure-tools/typespec-client-generator-core/0.50.2_ekyyo47wopztcpfcizlgiutz7y: + resolution: {integrity: sha512-xSyJ5OWqu9BToUoQrmoN6a9pxHpTUqDEyc5pmhRwzeuz3zOIvFs2DFKimE2wqVmhFTYg6LTVqle/UU4sr/vdyQ==} engines: {node: '>=18.0.0'} peerDependencies: '@azure-tools/typespec-azure-core': ~0.50.0 @@ -548,6 +548,7 @@ packages: '@typespec/openapi': ~0.64.0 '@typespec/rest': ~0.64.0 '@typespec/versioning': ~0.64.0 + '@typespec/xml': ~0.64.0 dependencies: '@azure-tools/typespec-azure-core': 0.50.0_oaywhha3am3mqfpwn5f5vofo2y '@typespec/compiler': 0.64.0 @@ -555,6 +556,7 @@ packages: '@typespec/openapi': 0.64.0_3iuxys3wzzu2f6cvuvfg7gykxi '@typespec/rest': 0.64.0_3iuxys3wzzu2f6cvuvfg7gykxi '@typespec/versioning': 0.64.0_@typespec+compiler@0.64.0 + '@typespec/xml': 0.64.0_@typespec+compiler@0.64.0 change-case: 5.4.4 pluralize: 8.0.0 yaml: 2.5.1 @@ -2406,7 +2408,7 @@ packages: '@typespec/compiler': 0.64.0 '@typespec/http': 0.64.0_@typespec+compiler@0.64.0 - /@typespec/openapi3/0.64.0_zeg3iovenpc7wszixzl5q4s6rq: + /@typespec/openapi3/0.64.0_glb6whlqofhhikp2a6rpbf2vk4: resolution: {integrity: sha512-k6WQ/5lTAnlg8TvdzB89W4mO8HhS3MHbdr5VMS4sS8j1K8uCC9xUPR0v+5TF9EDpKpa53LewetzNduP9KjMUmA==} engines: {node: '>=18.0.0'} hasBin: true @@ -2429,6 +2431,7 @@ packages: '@typespec/json-schema': 0.64.0_@typespec+compiler@0.64.0 '@typespec/openapi': 0.64.0_3iuxys3wzzu2f6cvuvfg7gykxi '@typespec/versioning': 0.64.0_@typespec+compiler@0.64.0 + '@typespec/xml': 0.64.0_@typespec+compiler@0.64.0 openapi-types: 12.1.3 yaml: 2.5.1 dev: false @@ -2537,7 +2540,6 @@ packages: '@typespec/compiler': ~0.64.0 dependencies: '@typespec/compiler': 0.64.0 - dev: true /@ungap/promise-all-settled/1.1.2: resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} diff --git a/packages/typespec-test/package.json b/packages/typespec-test/package.json index cffe7c185d..c7521ba7bb 100644 --- a/packages/typespec-test/package.json +++ b/packages/typespec-test/package.json @@ -8,7 +8,7 @@ "@azure-tools/typespec-autorest": ">=0.50.0 <1.0.0", "@typespec/openapi3": ">=0.64.0 <1.0.0", "@azure-tools/typespec-azure-core": ">=0.50.0 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.50.0 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.50.2 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.50.0 <1.0.0", "@azure-tools/typespec-azure-rulesets": ">=0.50.0 <1.0.0", "@typespec/compiler": ">=0.64.0 <1.0.0", diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md b/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md index 131a23991d..400e8dcc3e 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/review/openai-generic.api.md @@ -118,7 +118,7 @@ export interface CreateChatCompletionRequest { model: "gpt4" | "gpt-4-0314" | "gpt-4-0613" | "gpt-4-32k" | "gpt-4-32k-0314" | "gpt-4-32k-0613" | "gpt-3.5-turbo" | "gpt-3.5-turbo-16k" | "gpt-3.5-turbo-0301" | "gpt-3.5-turbo-0613" | "gpt-3.5-turbo-16k-0613"; n?: number | null; presence_penalty?: number | null; - stop?: Stop | null; + stop?: Stop; stream?: boolean | null; temperature?: number | null; top_p?: number | null; @@ -151,8 +151,8 @@ export interface CreateCompletionRequest { model: "babbage-002" | "davinci-002" | "text-davinci-003" | "text-davinci-002" | "text-davinci-001" | "code-davinci-002" | "text-curie-001" | "text-babbage-001" | "text-ada-001"; n?: number | null; presence_penalty?: number | null; - prompt: Prompt | null; - stop?: Stop | null; + prompt: Prompt; + stop?: Stop; stream?: boolean | null; suffix?: string | null; temperature?: number | null; @@ -797,10 +797,16 @@ export interface OpenAIFile { } // @public -export type Prompt = string | string[] | number[] | number[][]; +export type Prompt = Prompt_1 | null; // @public -export type Stop = string | string[]; +export type Prompt_1 = string | string[] | number[] | number[][]; + +// @public +export type Stop = Stop_1 | null; + +// @public +export type Stop_1 = string | string[]; // (No @packageDocumentation comment for this package) diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts index 6948700d4a..f4a4847e10 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/index.ts @@ -34,7 +34,9 @@ export { CompletionUsage, CreateCompletionRequest, Prompt, + Prompt_1, Stop, + Stop_1, CreateCompletionResponse, CreateFineTuningJobRequest, FineTuningJob, diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/index.ts index 4217d92f18..80f03378d5 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/index.ts @@ -31,7 +31,9 @@ export { CompletionUsage, CreateCompletionRequest, Prompt, + Prompt_1, Stop, + Stop_1, CreateCompletionResponse, CreateFineTuningJobRequest, FineTuningJob, diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts index eb36935e2b..db3618b7c9 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/models/models.ts @@ -1133,7 +1133,7 @@ export interface CreateCompletionRequest { * Note that <|endoftext|> is the document separator that the model sees during training, so if a * prompt is not specified the model will generate as if from the beginning of a new document. */ - prompt: Prompt | null; + prompt: Prompt; /** The suffix that comes after a completion of inserted text. */ suffix?: string | null; /** @@ -1166,7 +1166,7 @@ export interface CreateCompletionRequest { */ max_tokens?: number | null; /** Up to 4 sequences where the API will stop generating further tokens. */ - stop?: Stop | null; + stop?: Stop; /** * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear * in the text so far, increasing the model's likelihood to talk about new topics. @@ -1251,14 +1251,18 @@ export function createCompletionRequestSerializer( } /** Alias for Prompt */ -export type Prompt = string | string[] | number[] | number[][]; +export type Prompt = Prompt_1 | null; +/** Alias for Prompt */ +export type Prompt_1 = string | string[] | number[] | number[][]; export function promptSerializer(item: Prompt): any { return item; } /** Alias for Stop */ -export type Stop = string | string[]; +export type Stop = Stop_1 | null; +/** Alias for Stop */ +export type Stop_1 = string | string[]; export function stopSerializer(item: Stop): any { return item; @@ -1747,7 +1751,7 @@ export interface CreateChatCompletionRequest { */ max_tokens?: number | null; /** Up to 4 sequences where the API will stop generating further tokens. */ - stop?: Stop | null; + stop?: Stop; /** * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear * in the text so far, increasing the model's likelihood to talk about new topics. diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md index 2146cd3a88..c332f42b7b 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/review/openai-non-branded.api.md @@ -118,7 +118,7 @@ export interface CreateChatCompletionRequest { model: "gpt4" | "gpt-4-0314" | "gpt-4-0613" | "gpt-4-32k" | "gpt-4-32k-0314" | "gpt-4-32k-0613" | "gpt-3.5-turbo" | "gpt-3.5-turbo-16k" | "gpt-3.5-turbo-0301" | "gpt-3.5-turbo-0613" | "gpt-3.5-turbo-16k-0613"; n?: number | null; presencePenalty?: number | null; - stop?: Stop | null; + stop?: Stop; stream?: boolean | null; temperature?: number | null; topP?: number | null; @@ -151,8 +151,8 @@ export interface CreateCompletionRequest { model: "babbage-002" | "davinci-002" | "text-davinci-003" | "text-davinci-002" | "text-davinci-001" | "code-davinci-002" | "text-curie-001" | "text-babbage-001" | "text-ada-001"; n?: number | null; presencePenalty?: number | null; - prompt: Prompt | null; - stop?: Stop | null; + prompt: Prompt; + stop?: Stop; stream?: boolean | null; suffix?: string | null; temperature?: number | null; @@ -797,10 +797,16 @@ export interface OpenAIFile { } // @public -export type Prompt = string | string[] | number[] | number[][]; +export type Prompt = Prompt_1 | null; // @public -export type Stop = string | string[]; +export type Prompt_1 = string | string[] | number[] | number[][]; + +// @public +export type Stop = Stop_1 | null; + +// @public +export type Stop_1 = string | string[]; // (No @packageDocumentation comment for this package) diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts index aef13818a1..9ed0de027c 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/index.ts @@ -33,7 +33,9 @@ export { CompletionUsage, CreateCompletionRequest, Prompt, + Prompt_1, Stop, + Stop_1, CreateCompletionResponse, CreateFineTuningJobRequest, FineTuningJob, diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/index.ts index d7eab75081..857ce785fd 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/index.ts @@ -30,7 +30,9 @@ export { CompletionUsage, CreateCompletionRequest, Prompt, + Prompt_1, Stop, + Stop_1, CreateCompletionResponse, CreateFineTuningJobRequest, FineTuningJob, diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts index f764558bf2..569d87c012 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/models/models.ts @@ -1132,7 +1132,7 @@ export interface CreateCompletionRequest { * Note that <|endoftext|> is the document separator that the model sees during training, so if a * prompt is not specified the model will generate as if from the beginning of a new document. */ - prompt: Prompt | null; + prompt: Prompt; /** The suffix that comes after a completion of inserted text. */ suffix?: string | null; /** @@ -1165,7 +1165,7 @@ export interface CreateCompletionRequest { */ maxTokens?: number | null; /** Up to 4 sequences where the API will stop generating further tokens. */ - stop?: Stop | null; + stop?: Stop; /** * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear * in the text so far, increasing the model's likelihood to talk about new topics. @@ -1250,14 +1250,18 @@ export function createCompletionRequestSerializer( } /** Alias for Prompt */ -export type Prompt = string | string[] | number[] | number[][]; +export type Prompt = Prompt_1 | null; +/** Alias for Prompt */ +export type Prompt_1 = string | string[] | number[] | number[][]; export function promptSerializer(item: Prompt): any { return item; } /** Alias for Stop */ -export type Stop = string | string[]; +export type Stop = Stop_1 | null; +/** Alias for Stop */ +export type Stop_1 = string | string[]; export function stopSerializer(item: Stop): any { return item; @@ -1746,7 +1750,7 @@ export interface CreateChatCompletionRequest { */ maxTokens?: number | null; /** Up to 4 sequences where the API will stop generating further tokens. */ - stop?: Stop | null; + stop?: Stop; /** * Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear * in the text so far, increasing the model's likelihood to talk about new topics. diff --git a/packages/typespec-ts/package.json b/packages/typespec-ts/package.json index 76e073ca59..92e84550ca 100644 --- a/packages/typespec-ts/package.json +++ b/packages/typespec-ts/package.json @@ -71,7 +71,7 @@ "@azure-tools/typespec-autorest": ">=0.50.0 <1.0.0", "@azure-tools/typespec-azure-core": ">=0.50.0 <1.0.0", "@azure-tools/typespec-azure-resource-manager": ">=0.50.0 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.50.0 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.50.2 <1.0.0", "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.6.0", "@azure/core-lro": "^3.1.0", @@ -114,7 +114,7 @@ }, "peerDependencies": { "@azure-tools/typespec-azure-core": ">=0.50.0 <1.0.0", - "@azure-tools/typespec-client-generator-core": ">=0.50.0 <1.0.0", + "@azure-tools/typespec-client-generator-core": ">=0.50.2 <1.0.0", "@typespec/compiler": ">=0.64.0 <1.0.0", "@typespec/http": ">=0.64.0 <1.0.0", "@typespec/rest": ">=0.64.0 <1.0.0", diff --git a/packages/typespec-ts/src/framework/hooks/sdkTypes.ts b/packages/typespec-ts/src/framework/hooks/sdkTypes.ts index 3f4f6c575d..076a02ad43 100644 --- a/packages/typespec-ts/src/framework/hooks/sdkTypes.ts +++ b/packages/typespec-ts/src/framework/hooks/sdkTypes.ts @@ -98,6 +98,7 @@ export function provideSdkTypes(context: SdkContext) { ); break; case "nullable": + sdkTypesContext.types.set(sdkModel.__raw!, sdkModel); sdkTypesContext.types.set(sdkModel.type.__raw!, sdkModel.type); break; } diff --git a/packages/typespec-ts/src/modular/emitModels.ts b/packages/typespec-ts/src/modular/emitModels.ts index 6c5542611e..f7f449dc36 100644 --- a/packages/typespec-ts/src/modular/emitModels.ts +++ b/packages/typespec-ts/src/modular/emitModels.ts @@ -78,7 +78,10 @@ function isGenerableType( type.kind === "union" || type.kind === "dict" || type.kind === "array" || - type.kind === "nullable" + (type.kind === "nullable" && + isGenerableType(type.type) && + Boolean(type.name) && + !type.isGeneratedName) ); } export function emitTypes( @@ -210,6 +213,9 @@ function emitType(context: SdkContext, type: SdkType, sourceFile: SourceFile) { addSerializationFunctions(context, type, sourceFile); } else if (type.kind === "array") { addSerializationFunctions(context, type, sourceFile); + } else if (type.kind === "nullable") { + const nullableType = buildNullableType(context, type); + addDeclaration(sourceFile, nullableType, type); } } @@ -266,6 +272,8 @@ export function getModelNamespaces( return []; } else if (model.kind === "array" || model.kind === "dict") { return getModelNamespaces(context, model.valueType); + } else if (model.kind === "nullable") { + return getModelNamespaces(context, model.type); } return []; } @@ -330,6 +338,19 @@ function buildUnionType( return unionDeclaration; } +function buildNullableType(context: SdkContext, type: SdkNullableType) { + const nullableDeclaration: TypeAliasDeclarationStructure = { + kind: StructureKind.TypeAlias, + name: normalizeModelName(context, type), + isExported: true, + type: getTypeExpression(context, type.type) + " | null" + }; + nullableDeclaration.docs = [ + type.doc ?? `Alias for ${nullableDeclaration.name}` + ]; + return nullableDeclaration; +} + export function buildEnumTypes( context: SdkContext, type: SdkEnumType @@ -482,7 +503,8 @@ export function normalizeModelName( | SdkEnumType | SdkUnionType | SdkArrayType - | SdkDictionaryType, + | SdkDictionaryType + | SdkNullableType, nameType: NameType = NameType.Interface, skipPolymorphicUnionSuffix = false ): string { @@ -495,6 +517,10 @@ export function normalizeModelName( nameType )}>`; } + // TODO see https://github.com/Azure/typespec-azure/issues/2125 + if (type.kind === "nullable") { + return normalizeName(type.name, nameType, true); + } if (type.kind !== "model" && type.kind !== "enum" && type.kind !== "union") { return getTypeExpression(context, type); } diff --git a/packages/typespec-ts/src/modular/type-expressions/get-nullable-expression.ts b/packages/typespec-ts/src/modular/type-expressions/get-nullable-expression.ts new file mode 100644 index 0000000000..27290a0d3f --- /dev/null +++ b/packages/typespec-ts/src/modular/type-expressions/get-nullable-expression.ts @@ -0,0 +1,18 @@ +import { SdkNullableType } from "@azure-tools/typespec-client-generator-core"; +import { resolveReference } from "../../framework/reference.js"; +import { getTypeExpression, EmitTypeOptions } from "./get-type-expression.js"; +import { shouldEmitInline } from "./utils.js"; +import { SdkContext } from "../../utils/interfaces.js"; + +export function getNullableExpression( + context: SdkContext, + type: SdkNullableType, + options: EmitTypeOptions = {} +): string { + if (shouldEmitInline(type, options)) { + const nonNullableType = getTypeExpression(context, type.type, options); + return `(${nonNullableType}) | null`; + } else { + return resolveReference(type); + } +} diff --git a/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts b/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts index d5adb0686a..1ab29dc60a 100644 --- a/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts +++ b/packages/typespec-ts/src/modular/type-expressions/get-type-expression.ts @@ -8,6 +8,7 @@ import { getModelExpression } from "./get-model-expression.js"; import { getUnionExpression } from "./get-union-expression.js"; import { NameType, normalizeName } from "@azure-tools/rlc-common"; import { SdkContext } from "../../utils/interfaces.js"; +import { getNullableExpression } from "./get-nullable-expression.js"; export interface EmitTypeOptions { emitInline?: boolean; @@ -88,10 +89,8 @@ export function getTypeExpression( } case "model": return getModelExpression(context, type); - case "nullable": { - const nonNullableType = getTypeExpression(context, type.type, options); - return `(${nonNullableType}) | null`; - } + case "nullable": + return getNullableExpression(context, type, options); case "offsetDateTime": return "string"; case "tuple": { diff --git a/packages/typespec-ts/src/utils/modelUtils.ts b/packages/typespec-ts/src/utils/modelUtils.ts index 523a74c167..d1479fd66f 100644 --- a/packages/typespec-ts/src/utils/modelUtils.ts +++ b/packages/typespec-ts/src/utils/modelUtils.ts @@ -390,30 +390,33 @@ function getSchemaForUnion( const values = []; let namedUnionMember = false; - if (asEnum?.open && asEnum.members.size > 0) { - for (const [_, member] of asEnum.members.entries()) { - const memberType = getSchemaForType(dpgContext, member.type, { - ...options, - needRef: false - }); - values.push(memberType); - if (memberType.name) { - namedUnionMember = true; + if (!(options?.needRef && union.name && !asEnum)) { + if (asEnum?.open && asEnum.members.size > 0) { + for (const [_, member] of asEnum.members.entries()) { + const memberType = getSchemaForType(dpgContext, member.type, { + ...options, + needRef: options?.needRef ?? false + }); + values.push(memberType); + if (memberType.name) { + namedUnionMember = true; + } } - } - } else { - for (const variant of variants) { - // We already know it's not a model type - const variantType = getSchemaForType(dpgContext, variant.type, { - ...options, - needRef: false - }); - values.push(variantType); - if (variantType.typeName) { - namedUnionMember = true; + } else { + for (const variant of variants) { + // We already know it's not a model type + const variantType = getSchemaForType(dpgContext, variant.type, { + ...options, + needRef: isAnonymousModelType(variant.type) ? false : true + }); + values.push(variantType); + if (variantType.typeName) { + namedUnionMember = true; + } } } } + const schema: any = {}; if (values.length > 0) { schema.enum = values; @@ -433,27 +436,31 @@ function getSchemaForUnion( (item) => `${getTypeName(item, [SchemaContext.Output]) ?? item}` ) .join(" | "); - if (!union.expression) { - const unionName = union.name - ? normalizeName(union.name, NameType.Interface) - : undefined; - schema.name = unionName; - schema.type = "object"; - schema.typeName = unionName; - schema.outputTypeName = unionName + "Output"; - schema.alias = unionAlias; - schema.outputAlias = outputUnionAlias; - } else if (union.expression && !union.name) { - schema.type = "union"; - schema.typeName = unionAlias; - schema.outputTypeName = outputUnionAlias; - } else { - schema.type = "union"; - schema.typeName = union.name ?? unionAlias; - schema.outputTypeName = union.name - ? union.name + "Output" - : outputUnionAlias; - } + schema.alias = unionAlias; + schema.outputAlias = outputUnionAlias; + } + if (!union.expression) { + const unionName = union.name + ? normalizeName(union.name, NameType.Interface) + : undefined; + schema.name = unionName; + schema.type = "object"; + schema.typeName = unionName; + schema.outputTypeName = unionName + "Output"; + } else if (union.expression && !union.name) { + schema.type = "union"; + schema.typeName = schema.alias; + schema.outputTypeName = schema.outputAlias; + delete schema.alias; + delete schema.outputAlias; + } else { + schema.type = "union"; + schema.typeName = union.name ?? schema.alias; + schema.outputTypeName = union.name + ? union.name + "Output" + : schema.outputAlias; + delete schema.alias; + delete schema.outputAlias; } return schema; diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/nullable/nullableUnion.md b/packages/typespec-ts/test/modularUnit/scenarios/models/nullable/nullableUnion.md new file mode 100644 index 0000000000..694ae4a1b4 --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/nullable/nullableUnion.md @@ -0,0 +1,118 @@ +# should handle recursive nullable union + +## TypeSpec + +```tsp +union A { + null, + { + code?: string, + message?: string, + propA?: A, + }, +} +op post(@body body: A): { @body body: A }; +``` + +## Models + +```ts models +/** model interface _PostRequest */ +export interface _PostRequest { + code?: string; + message?: string; + propA?: A; +} + +export function _postRequestSerializer(item: _PostRequest): any { + return { + code: item["code"], + message: item["message"], + propA: !item["propA"] + ? item["propA"] + : _postRequestSerializer(item["propA"]), + }; +} + +export function _postRequestDeserializer(item: any): _PostRequest { + return { + code: item["code"], + message: item["message"], + propA: !item["propA"] + ? item["propA"] + : _postRequestDeserializer(item["propA"]), + }; +} + +/** Alias for A */ +export type A = { + code?: string; + message?: string; + propA?: A; +} | null; +/** Alias for A */ +export type A_1 = { + code?: string; + message?: string; + propA?: A; +} | null; +``` + +## Operations + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { + _postRequestSerializer, + _postRequestDeserializer, + A_1, +} from "../models/models.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _postSend( + context: Client, + body: A_1, + options: PostOptionalParams = { requestOptions: {} }, +): StreamableMethod { + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: "application/json", + headers: { + accept: "application/json", + ...options.requestOptions?.headers, + }, + body: !body ? body : _postRequestSerializer(body), + }); +} + +export async function _postDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["200"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return { + code: ["code"], + message: ["message"], + propA: !["propA"] ? ["propA"] : _postRequestDeserializer(["propA"]), + }; +} + +export async function post( + context: Client, + body: A_1, + options: PostOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _postSend(context, body, options); + return _postDeserialize(result); +} +``` diff --git a/packages/typespec-ts/test/unit/modelsGenerator.spec.ts b/packages/typespec-ts/test/unit/modelsGenerator.spec.ts index ea53eeb1aa..e37565e9fe 100644 --- a/packages/typespec-ts/test/unit/modelsGenerator.spec.ts +++ b/packages/typespec-ts/test/unit/modelsGenerator.spec.ts @@ -119,6 +119,58 @@ describe("Input/output model type", () => { await verifyPropertyType(tspType, typeScriptType); }); + it("should generate nullable recursive union", async () => { + const tspDefinition = ` + union A { + null, + { + code?: string, + message?: string, + propA?: A, + }, + } + op post(@body body: A): { @body body: A }; + `; + const schemaOutput = await emitModelsFromTypeSpec( + tspDefinition + ); + assert.ok(schemaOutput); + const { inputModelFile, outputModelFile } = schemaOutput!; + await assertEqualContent( + inputModelFile?.content!, + ` + /** Alias for A */ + export type A = null | { code?: string; message?: string; propA?: A }; + ` + ); + await assertEqualContent( + outputModelFile?.content!, + ` + /** Alias for AOutput */ + export type AOutput = null | { + code?: string; + message?: string; + propA?: AOutput; + }; + ` + ); + const parametersOutput = await emitParameterFromTypeSpec(tspDefinition); + assert.ok(parametersOutput); + await assertEqualContent( + parametersOutput?.content!, + ` + import type { RequestParameters } from "@azure-rest/core-client"; + import type { A } from "./models.js"; + + export interface PostBodyParam { + body: A; + } + + export type PostParameters = PostBodyParam & RequestParameters; + ` + ); + }); + it("should generate nullable array", async () => { const tspDefinition = ` alias nullableArray = int32 | null;`; diff --git a/packages/typespec-ts/test/unit/transform/transformSchemas.spec.ts b/packages/typespec-ts/test/unit/transform/transformSchemas.spec.ts index 47cc0481d6..0009b243cc 100644 --- a/packages/typespec-ts/test/unit/transform/transformSchemas.spec.ts +++ b/packages/typespec-ts/test/unit/transform/transformSchemas.spec.ts @@ -451,14 +451,7 @@ describe("#transformSchemas", () => { fromCore: false, isMultipartBody: false, typeName: "A", - properties: { - '"foo"': { - description: undefined, - required: true, - type: "string", - usage: ["input", "output"] - } - }, + properties: {}, outputTypeName: "AOutput", usage: ["input", "output"] }, @@ -469,14 +462,7 @@ describe("#transformSchemas", () => { fromCore: false, isMultipartBody: false, typeName: "B", - properties: { - '"bar"': { - description: undefined, - required: true, - type: "string", - usage: ["input", "output"] - } - }, + properties: {}, outputTypeName: "BOutput", usage: ["input", "output"] } @@ -611,14 +597,7 @@ describe("#transformSchemas", () => { fromCore: false, isMultipartBody: false, typeName: "A", - properties: { - '"foo"': { - type: "string", - description: undefined, - required: true, - usage: ["input", "output"] - } - }, + properties: {}, outputTypeName: "AOutput", usage: ["input", "output"] }, @@ -629,14 +608,7 @@ describe("#transformSchemas", () => { fromCore: false, isMultipartBody: false, typeName: "B", - properties: { - '"baz"': { - type: "string", - description: undefined, - required: true, - usage: ["input", "output"] - } - }, + properties: {}, outputTypeName: "BOutput", usage: ["input", "output"] }