diff --git a/generators/typescript/sdk/features.yml b/generators/typescript/sdk/features.yml index 102cf06da885..c117e0797ade 100644 --- a/generators/typescript/sdk/features.yml +++ b/generators/typescript/sdk/features.yml @@ -166,6 +166,12 @@ features: benefiting from SDK-level configuration like authentication, retries, timeouts, and logging. This is useful for calling API endpoints not yet supported in the SDK. + - id: CUSTOM_FETCHER + advanced: true + description: | + The SDK provides a way for you to customize the underlying HTTP client / Fetch function. If you're running in an + unsupported environment, this provides a way for you to break glass and ensure the SDK works. + - id: RUNTIME_COMPATIBILITY advanced: true description: | @@ -183,8 +189,3 @@ features: - Deno v1.25+ - Bun 1.0+ - React Native - - ### Customizing Fetch Client - - The SDK provides a way for you to customize the underlying HTTP client / Fetch function. If you're running in an - unsupported environment, this provides a way for you to break glass and ensure the SDK works. diff --git a/generators/typescript/sdk/generator/src/SdkGenerator.ts b/generators/typescript/sdk/generator/src/SdkGenerator.ts index 67b64018840a..737d1e0c7bac 100644 --- a/generators/typescript/sdk/generator/src/SdkGenerator.ts +++ b/generators/typescript/sdk/generator/src/SdkGenerator.ts @@ -560,6 +560,7 @@ export class SdkGenerator { endpointSnippets: this.endpointSnippets, fileResponseType: config.fileResponseType, fetchSupport: config.fetchSupport, + allowCustomFetcher: config.allowCustomFetcher, generateSubpackageExports: config.generateSubpackageExports }), ir: intermediateRepresentation diff --git a/generators/typescript/sdk/generator/src/__test__/ReadmeConfigBuilder.test.ts b/generators/typescript/sdk/generator/src/__test__/ReadmeConfigBuilder.test.ts index de2b5fc439df..445f5768a07a 100644 --- a/generators/typescript/sdk/generator/src/__test__/ReadmeConfigBuilder.test.ts +++ b/generators/typescript/sdk/generator/src/__test__/ReadmeConfigBuilder.test.ts @@ -203,6 +203,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -239,6 +240,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -279,6 +281,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -310,6 +313,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -335,6 +339,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -368,6 +373,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -393,6 +399,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -430,6 +437,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -461,6 +469,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -492,6 +501,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "node-fetch", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -523,6 +533,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -573,6 +584,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -602,6 +614,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -636,6 +649,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); @@ -679,6 +693,7 @@ describe("ReadmeConfigBuilder", () => { endpointSnippets: [endpointSnippet], fileResponseType: "stream", fetchSupport: "native", + allowCustomFetcher: false, generateSubpackageExports: false }); diff --git a/generators/typescript/sdk/generator/src/__test__/ReadmeSnippetBuilder.test.ts b/generators/typescript/sdk/generator/src/__test__/ReadmeSnippetBuilder.test.ts index ef260f208a60..c3663aa2f184 100644 --- a/generators/typescript/sdk/generator/src/__test__/ReadmeSnippetBuilder.test.ts +++ b/generators/typescript/sdk/generator/src/__test__/ReadmeSnippetBuilder.test.ts @@ -173,6 +173,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -201,6 +202,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -225,6 +227,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -248,6 +251,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -275,6 +279,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -306,6 +311,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -334,6 +340,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -360,6 +367,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -387,6 +395,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -398,9 +407,9 @@ describe("ReadmeSnippetBuilder", () => { }); }); - // ── Runtime compatibility ────────────────────────────────────────── - describe("runtime compatibility snippets", () => { - it("generates custom fetcher snippet", () => { + // ── Custom fetcher ────────────────────────────────────────────────── + describe("custom fetcher snippets", () => { + it("generates custom fetcher snippet when allowCustomFetcher is true", () => { const endpoint = createEndpoint("ep1", "createUser"); const service = createService("svc1", createFernFilepath([]), [endpoint]); const endpointSnippet = createEndpointSnippet("ep1", "POST", "await client.createUser();"); @@ -413,15 +422,37 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); const snippets = builder.buildReadmeSnippets(); - const runtimeSnippets = snippets.RUNTIME_COMPATIBILITY; - assert(Array.isArray(runtimeSnippets)); - expect(runtimeSnippets).toHaveLength(1); - expect(runtimeSnippets[0]).toContain("fetcher:"); - expect(runtimeSnippets[0]).toContain("provide your implementation here"); + const fetcherSnippets = snippets.CUSTOM_FETCHER; + assert(Array.isArray(fetcherSnippets)); + expect(fetcherSnippets).toHaveLength(1); + expect(fetcherSnippets[0]).toContain("fetcher:"); + expect(fetcherSnippets[0]).toContain("provide your implementation here"); + }); + + it("returns false for custom fetcher snippets when allowCustomFetcher is false", () => { + const endpoint = createEndpoint("ep1", "createUser"); + const service = createService("svc1", createFernFilepath([]), [endpoint]); + const endpointSnippet = createEndpointSnippet("ep1", "POST", "await client.createUser();"); + + const context = createMockSdkContext({ + services: { svc1: service }, + packageName: "@acme/sdk" + }); + const builder = new ReadmeSnippetBuilder({ + context, + endpointSnippets: [endpointSnippet], + fileResponseType: "stream", + allowCustomFetcher: false, + generateSubpackageExports: false + }); + + const snippets = builder.buildReadmeSnippets(); + expect(snippets.CUSTOM_FETCHER).toBe(false); }); }); @@ -440,6 +471,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -475,6 +507,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -506,6 +539,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -548,6 +582,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -584,6 +619,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -607,6 +643,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -644,6 +681,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -668,6 +706,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -707,6 +746,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: true }); @@ -747,6 +787,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -783,6 +824,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: true }); @@ -813,6 +855,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -866,6 +909,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -889,6 +933,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -924,6 +969,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "binary-response", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -961,6 +1007,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -983,6 +1030,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "binary-response", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1016,6 +1064,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1056,6 +1105,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1076,6 +1126,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1099,6 +1150,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1122,6 +1174,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1144,6 +1197,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [endpointSnippet], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1182,6 +1236,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [snippet1, snippet2], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1206,6 +1261,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [snippetGet, snippetPost], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1247,6 +1303,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [snippet1, snippet2], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1284,6 +1341,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [snippet1, snippet2], fileResponseType: "stream", + allowCustomFetcher: true, generateSubpackageExports: false }); @@ -1362,6 +1420,7 @@ describe("ReadmeSnippetBuilder", () => { context, endpointSnippets: [snippet1, snippet2, snippet3], fileResponseType: "binary-response", + allowCustomFetcher: true, generateSubpackageExports: true }); diff --git a/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeConfigBuilder.test.ts.snap b/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeConfigBuilder.test.ts.snap index d8b0fd50431c..2c02c6aa39cf 100644 --- a/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeConfigBuilder.test.ts.snap +++ b/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeConfigBuilder.test.ts.snap @@ -91,16 +91,8 @@ const client = new MySDKClient({ "advanced": false, "description": undefined, "id": "RUNTIME_COMPATIBILITY", - "snippets": [ - "import { MySDKClient } from "@acme/sdk"; - -const client = new MySDKClient({ - ... - fetcher: // provide your implementation here -}); -", - ], - "snippetsAreOptional": false, + "snippets": [], + "snippetsAreOptional": true, }, ], "introduction": "Welcome to Acme SDK", diff --git a/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap b/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap index d5417c4adebd..c53b7539f744 100644 --- a/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap +++ b/generators/typescript/sdk/generator/src/__test__/__snapshots__/ReadmeSnippetBuilder.test.ts.snap @@ -52,6 +52,15 @@ const response = await client.createUser(..., { }); const data = await response.json(); +", + ], + "CUSTOM_FETCHER": [ + "import { MySDKClient } from "@acme/sdk"; + +const client = new MySDKClient({ + ... + fetcher: // provide your implementation here +}); ", ], "EXCEPTION_HANDLING": [ @@ -100,15 +109,7 @@ const request: AcmeSdk.CreateUserRequest = { }); ", ], - "RUNTIME_COMPATIBILITY": [ - "import { MySDKClient } from "@acme/sdk"; - -const client = new MySDKClient({ - ... - fetcher: // provide your implementation here -}); -", - ], + "RUNTIME_COMPATIBILITY": [], "STREAMING_RESPONSE": [ "const stream = await client.streamEvents();", ], diff --git a/generators/typescript/sdk/generator/src/readme/ReadmeConfigBuilder.ts b/generators/typescript/sdk/generator/src/readme/ReadmeConfigBuilder.ts index 1c1339f18a25..900d2c365c38 100644 --- a/generators/typescript/sdk/generator/src/readme/ReadmeConfigBuilder.ts +++ b/generators/typescript/sdk/generator/src/readme/ReadmeConfigBuilder.ts @@ -14,22 +14,26 @@ export class ReadmeConfigBuilder { private readonly endpointSnippets: FernGeneratorExec.Endpoint[]; private readonly fileResponseType: "stream" | "binary-response"; private readonly fetchSupport: "node-fetch" | "native"; + private readonly allowCustomFetcher: boolean; private readonly generateSubpackageExports: boolean; constructor({ endpointSnippets, fileResponseType, fetchSupport, + allowCustomFetcher, generateSubpackageExports }: { endpointSnippets: FernGeneratorExec.Endpoint[]; fileResponseType: "stream" | "binary-response"; fetchSupport: "node-fetch" | "native"; + allowCustomFetcher: boolean; generateSubpackageExports: boolean; }) { this.endpointSnippets = endpointSnippets; this.fileResponseType = fileResponseType; this.fetchSupport = fetchSupport; + this.allowCustomFetcher = allowCustomFetcher; this.generateSubpackageExports = generateSubpackageExports; } @@ -46,6 +50,7 @@ export class ReadmeConfigBuilder { context, endpointSnippets: this.endpointSnippets, fileResponseType: this.fileResponseType, + allowCustomFetcher: this.allowCustomFetcher, generateSubpackageExports: this.generateSubpackageExports }); const snippets = readmeSnippetBuilder.buildReadmeSnippets(); @@ -81,13 +86,16 @@ export class ReadmeConfigBuilder { description = authenticationDescription; } + // Features with description-only content (no code snippets) should still be rendered + const isDescriptionOnlyFeature = feature.id === "RUNTIME_COMPATIBILITY"; + features.push({ id: feature.id, advanced: feature.advanced, description, snippets: snippetForFeature === false ? [] : (snippetForFeature ?? []), addendum: feature.addendum ? this.processTemplateText(feature.addendum) : undefined, - snippetsAreOptional: isAuthenticationWithDescription + snippetsAreOptional: isAuthenticationWithDescription || isDescriptionOnlyFeature }); } return { diff --git a/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts b/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts index 95377663a26a..c0324f914cf4 100644 --- a/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts +++ b/generators/typescript/sdk/generator/src/readme/ReadmeSnippetBuilder.ts @@ -38,9 +38,11 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder { public static readonly STREAMING_RESPONSE_FEATURE_ID: FernGeneratorCli.FeatureId = "STREAMING_RESPONSE"; public static readonly LOGGING_FEATURE_ID: FernGeneratorCli.FeatureId = "LOGGING"; private static readonly CUSTOM_FETCH_FEATURE_ID: FernGeneratorCli.FeatureId = "CUSTOM_FETCH"; + private static readonly CUSTOM_FETCHER_FEATURE_ID: FernGeneratorCli.FeatureId = "CUSTOM_FETCHER"; private readonly context: SdkContext; private readonly isPaginationEnabled: boolean; + private readonly allowCustomFetcher: boolean; private readonly generateSubpackageExports: boolean; private readonly endpoints: Record = {}; private readonly snippets: Record = {}; @@ -55,17 +57,20 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder { context, endpointSnippets, fileResponseType, + allowCustomFetcher, generateSubpackageExports }: { context: SdkContext; endpointSnippets: FernGeneratorExec.Endpoint[]; fileResponseType: "stream" | "binary-response"; + allowCustomFetcher: boolean; generateSubpackageExports: boolean; }) { super({ endpointSnippets }); this.context = context; this.fileResponseType = fileResponseType; this.isPaginationEnabled = context.config.generatePaginatedClients ?? false; + this.allowCustomFetcher = allowCustomFetcher; this.generateSubpackageExports = generateSubpackageExports; this.endpoints = this.buildEndpoints(); @@ -99,6 +104,7 @@ export class ReadmeSnippetBuilder extends AbstractReadmeSnippetBuilder { this.buildAdditionalQueryStringParametersSnippets(); snippets[ReadmeSnippetBuilder.LOGGING_FEATURE_ID] = this.buildLoggingSnippets(); snippets[ReadmeSnippetBuilder.CUSTOM_FETCH_FEATURE_ID] = this.buildCustomFetchSnippets(); + snippets[ReadmeSnippetBuilder.CUSTOM_FETCHER_FEATURE_ID] = this.buildCustomFetcherSnippets(); if (this.isPaginationEnabled) { const paginationSnippets = this.buildPaginationSnippets(); @@ -540,7 +546,10 @@ const data = await response.json(); ); } - private buildRuntimeCompatibilitySnippets(): string[] { + private buildCustomFetcherSnippets(): string[] | false { + if (!this.allowCustomFetcher) { + return false; + } const snippet = this.writeCode( code` import { ${this.rootClientConstructorName} } from "${this.rootPackageName}"; @@ -554,6 +563,10 @@ const ${this.clientVariableName} = new ${this.rootClientConstructorName}({ return [snippet]; } + private buildRuntimeCompatibilitySnippets(): string[] { + return []; + } + private getEndpointsForFeature(featureId: FernIr.FeatureId): EndpointWithFilepath[] { const endpointIds = this.getEndpointIdsForFeature(featureId); return endpointIds != null ? this.getEndpoints(endpointIds) : this.getEndpoints([this.defaultEndpointId]); diff --git a/generators/typescript/sdk/versions.yml b/generators/typescript/sdk/versions.yml index 57c6ba59a081..8224d072999f 100644 --- a/generators/typescript/sdk/versions.yml +++ b/generators/typescript/sdk/versions.yml @@ -1,4 +1,15 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 3.59.1 + changelogEntry: + - summary: | + Fix generated README.md including the "Customizing Fetch Client" section + under "Runtime Compatibility" even when `allowCustomFetcher` is `false`. + The section and its code snippet are now conditionally omitted when the + custom fetcher option is disabled. + type: fix + createdAt: "2026-03-17" + irVersion: 65 + - version: 3.59.0 changelogEntry: - summary: | diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts index 4d301fce03a4..7d3f4b062e3e 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/options.ts @@ -120,10 +120,11 @@ export interface ParseOpenAPIOptions { /** * Controls how `const` values in OpenAPI specs are represented. * - `literals`: Convert const values directly to literals with defaults. - * - `enums`: Convert const values to single-element enums (current behavior). - * Defaults to `enums`. + * - `enums`: Convert const values to single-element enums; blocks transitive coercion via `coerce-enums-to-literals`. + * - `enums-coerceable-to-literals`: Convert const values to single-element enums, but allow `coerce-enums-to-literals` to coerce them transitively. + * Defaults to `enums-coerceable-to-literals`. */ - coerceConstsTo: "literals" | "enums"; + coerceConstsTo: "literals" | "enums" | "enums-coerceable-to-literals"; /** * If true, treat OpenAPI `type: string, format: byte` as a base64/bytes primitive @@ -167,7 +168,7 @@ export const DEFAULT_PARSE_OPENAPI_SETTINGS: ParseOpenAPIOptions = { pathParameterOrder: generatorsYml.PathParameterOrder.UrlOrder, resolveSchemaCollisions: false, inferForwardCompatible: false, - coerceConstsTo: "enums", + coerceConstsTo: "enums-coerceable-to-literals", respectByteFormat: false }; diff --git a/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts b/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts index 0bc72e47d2f7..fdc0b4db17d3 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts +++ b/packages/cli/api-importers/openapi/openapi-ir-parser/src/schema/convertSchemas.ts @@ -466,8 +466,11 @@ export function convertSchemaObject( // const // NOTE(patrickthornton): This is an attribute of OpenAPIV3_1.SchemaObject; // at some point we should probably migrate to that object altogether. - const isFromConst = "const" in schema; - if (isFromConst) { + const hasConst = "const" in schema; + // When coerceConstsTo is "enums", block the coerceEnumsToLiterals path + // so const-derived enums stay as enums. "enums-coerceable-to-literals" allows it. + const blockConstCoercionToLiteral = hasConst && context.options.coerceConstsTo === "enums"; + if (hasConst) { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `const` is an OpenAPI 3.1 attribute not in the V3 types const constValue = (schema as Record).const; if (context.options.coerceConstsTo === "literals") { @@ -487,7 +490,7 @@ export function convertSchemaObject( }); } } - // Default: coerce to enum (current behavior) + // "enums" and "enums-coerceable-to-literals": coerce to enum schema.enum = [constValue]; } @@ -527,7 +530,7 @@ export function convertSchemaObject( if ( context.options.coerceEnumsToLiterals && - !isFromConst && + !blockConstCoercionToLiteral && schema.enum.length === 1 && schema.enum[0] != null && fernEnum == null diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json index 3ab5b79bedb8..e357d10d55be 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/anyOf.json @@ -106,7 +106,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json index c45552e2e7fb..87c5369a2fef 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/application-json.json @@ -86,7 +86,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json index b9e17c10a559..aa57f26a64d0 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/availability.json @@ -321,7 +321,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json index aab780c38691..5ea8c1438ea8 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/const.json @@ -120,7 +120,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json index 15fc4552bbc9..e2d6ceebd2b4 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/dates.json @@ -130,7 +130,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json index a3da2edae812..5b20700d2dd7 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/inline-schema-reference.json @@ -118,7 +118,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json index 447cfce0ca6b..de76abf5b8b9 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/preserve-single-schema-oneof.json @@ -85,7 +85,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json index ddbceeb5bc85..d45d72504e56 100644 --- a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir-in-memory/url-reference.json @@ -73,7 +73,7 @@ "pathParameterOrder": "url-order", "resolveSchemaCollisions": false, "inferForwardCompatible": false, - "coerceConstsTo": "enums", + "coerceConstsTo": "enums-coerceable-to-literals", "respectByteFormat": false } } \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums-coerceable-to-literals.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums-coerceable-to-literals.json new file mode 100644 index 000000000000..28010438c7e8 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi-ir/const-with-coerce-enums-coerceable-to-literals.json @@ -0,0 +1,262 @@ +{ + "title": "Pet Store API", + "servers": [], + "websocketServers": [], + "tags": { + "tagsById": {} + }, + "hasEndpointsMarkedInternal": false, + "endpoints": [ + { + "summary": "Create a new pet", + "audiences": [], + "tags": [], + "pathParameters": [], + "queryParameters": [], + "headers": [], + "generatedRequestName": "PostPetsRequest", + "request": { + "schema": { + "generatedName": "PostPetsRequest", + "schema": "CreatePetRequest", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "contentType": "application/json", + "fullExamples": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "json" + }, + "response": { + "description": "Pet created successfully", + "schema": { + "generatedName": "PostPetsResponse", + "schema": "Pet", + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "reference" + }, + "fullExamples": [], + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "statusCode": 201, + "type": "json" + }, + "errors": {}, + "servers": [], + "authed": false, + "method": "POST", + "path": "/pets", + "examples": [ + { + "pathParameters": [], + "queryParameters": [], + "headers": [], + "request": { + "properties": { + "type": { + "value": { + "value": "pet", + "type": "string" + }, + "type": "literal" + }, + "name": { + "value": { + "value": "Fluffy", + "type": "string" + }, + "type": "primitive" + } + }, + "type": "object" + }, + "response": { + "value": { + "properties": { + "id": { + "value": { + "value": 123, + "type": "int" + }, + "type": "primitive" + }, + "type": { + "value": { + "value": "pet", + "type": "string" + }, + "type": "literal" + }, + "name": { + "value": { + "value": "Fluffy", + "type": "string" + }, + "type": "primitive" + }, + "status": { + "value": { + "value": "active", + "type": "string" + }, + "type": "literal" + } + }, + "type": "object" + }, + "type": "withoutStreaming" + }, + "codeSamples": [], + "type": "full" + } + ], + "source": { + "file": "../openapi.yml", + "type": "openapi" + } + } + ], + "webhooks": [], + "channels": {}, + "groupedSchemas": { + "rootSchemas": { + "CreatePetRequest": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "createPetRequestType", + "key": "type", + "schema": { + "value": { + "value": "pet", + "type": "string" + }, + "generatedName": "CreatePetRequestType", + "groupName": [], + "type": "literal" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "createPetRequestName", + "key": "name", + "schema": { + "schema": { + "example": "Fluffy", + "type": "string" + }, + "generatedName": "CreatePetRequestName", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "CreatePetRequest", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + }, + "Pet": { + "allOf": [], + "properties": [ + { + "conflict": {}, + "generatedName": "petId", + "key": "id", + "schema": { + "schema": { + "example": 123, + "type": "int" + }, + "generatedName": "PetId", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petType", + "key": "type", + "schema": { + "value": { + "value": "pet", + "type": "string" + }, + "generatedName": "PetType", + "groupName": [], + "type": "literal" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petName", + "key": "name", + "schema": { + "schema": { + "example": "Fluffy", + "type": "string" + }, + "generatedName": "PetName", + "groupName": [], + "type": "primitive" + }, + "audiences": [] + }, + { + "conflict": {}, + "generatedName": "petStatus", + "key": "status", + "schema": { + "value": { + "value": "active", + "type": "string" + }, + "generatedName": "PetStatus", + "groupName": [], + "type": "literal" + }, + "audiences": [] + } + ], + "allOfPropertyConflicts": [], + "generatedName": "Pet", + "groupName": [], + "additionalProperties": false, + "source": { + "file": "../openapi.yml", + "type": "openapi" + }, + "type": "object" + } + }, + "namespacedSchemas": {} + }, + "variables": {}, + "nonRequestReferencedSchemas": {}, + "securitySchemes": {}, + "globalHeaders": [], + "idempotencyHeaders": [], + "groups": {} +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums-coerceable-to-literals.json b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums-coerceable-to-literals.json new file mode 100644 index 000000000000..8f63fe00d7f0 --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/__snapshots__/openapi/const-with-coerce-enums-coerceable-to-literals.json @@ -0,0 +1,139 @@ +{ + "absoluteFilePath": "/DUMMY_PATH", + "importedDefinitions": {}, + "namedDefinitionFiles": { + "__package__.yml": { + "absoluteFilepath": "/DUMMY_PATH", + "contents": { + "service": { + "auth": false, + "base-path": "", + "endpoints": { + "createANewPet": { + "auth": undefined, + "display-name": "Create a new pet", + "docs": undefined, + "examples": [ + { + "request": { + "name": "Fluffy", + "type": "pet", + }, + "response": { + "body": { + "id": 123, + "name": "Fluffy", + "status": "active", + "type": "pet", + }, + }, + }, + ], + "method": "POST", + "pagination": undefined, + "path": "/pets", + "request": { + "body": { + "properties": { + "name": "string", + "type": "literal<"pet">", + }, + }, + "content-type": "application/json", + "headers": undefined, + "name": "CreatePetRequest", + "path-parameters": undefined, + "query-parameters": undefined, + }, + "response": { + "docs": "Pet created successfully", + "status-code": 201, + "type": "Pet", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + "types": { + "Pet": { + "docs": undefined, + "inline": undefined, + "properties": { + "id": "integer", + "name": "string", + "status": "literal<"active">", + "type": "literal<"pet">", + }, + "source": { + "openapi": "../openapi.yml", + }, + }, + }, + }, + "rawContents": "service: + auth: false + base-path: '' + endpoints: + createANewPet: + path: /pets + method: POST + source: + openapi: ../openapi.yml + display-name: Create a new pet + request: + name: CreatePetRequest + body: + properties: + type: literal<"pet"> + name: string + content-type: application/json + response: + docs: Pet created successfully + type: Pet + status-code: 201 + examples: + - request: + type: pet + name: Fluffy + response: + body: + id: 123 + type: pet + name: Fluffy + status: active + source: + openapi: ../openapi.yml +types: + Pet: + properties: + id: integer + type: literal<"pet"> + name: string + status: literal<"active"> + source: + openapi: ../openapi.yml +", + }, + }, + "packageMarkers": {}, + "rootApiFile": { + "contents": { + "display-name": "Pet Store API", + "error-discrimination": { + "strategy": "status-code", + }, + "name": "api", + }, + "defaultUrl": undefined, + "rawContents": "name: api +error-discrimination: + strategy: status-code +display-name: Pet Store API +", + }, +} \ No newline at end of file diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums-coerceable-to-literals/fern/generators.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums-coerceable-to-literals/fern/generators.yml new file mode 100644 index 000000000000..ff080fcf043f --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums-coerceable-to-literals/fern/generators.yml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://schema.buildwithfern.dev/generators-yml.json +api: + specs: + - openapi: ../openapi.yml + settings: + coerce-enums-to-literals: true + coerce-consts-to: enums-coerceable-to-literals diff --git a/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums-coerceable-to-literals/openapi.yml b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums-coerceable-to-literals/openapi.yml new file mode 100644 index 000000000000..8932b15417ea --- /dev/null +++ b/packages/cli/api-importers/openapi/openapi-ir-to-fern-tests/src/__test__/fixtures/const-with-coerce-enums-coerceable-to-literals/openapi.yml @@ -0,0 +1,57 @@ +openapi: 3.1.0 +info: + title: Pet Store API + version: 1.0.0 + +paths: + /pets: + post: + summary: Create a new pet + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreatePetRequest" + responses: + "201": + description: Pet created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/Pet" + +components: + schemas: + CreatePetRequest: + type: object + required: + - type + - name + properties: + type: + const: "pet" + name: + type: string + example: "Fluffy" + + Pet: + type: object + required: + - id + - type + - name + - status + properties: + id: + type: integer + example: 123 + type: + const: "pet" + name: + type: string + example: "Fluffy" + status: + type: string + enum: + - active diff --git a/packages/cli/cli/versions.yml b/packages/cli/cli/versions.yml index 1cf448524478..1572412f80c9 100644 --- a/packages/cli/cli/versions.yml +++ b/packages/cli/cli/versions.yml @@ -1,4 +1,16 @@ # yaml-language-server: $schema=../../../fern-versions-yml.schema.json +- version: 4.33.0 + changelogEntry: + - summary: | + Add `enums-coerceable-to-literals` option for `coerce-consts-to`, and make + it the new default. This behaves like `enums` (const values become + single-element enums) but allows `coerce-enums-to-literals` to transitively + coerce them to literals, which is consistent with past default behavior. The + existing `enums` option continues to block this transitive coercion. + type: feat + createdAt: "2026-03-17" + irVersion: 65 + - version: 4.32.2 changelogEntry: - summary: | diff --git a/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts b/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts index ac963c1142c7..186382fd96ad 100644 --- a/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts +++ b/packages/cli/config/src/schemas/settings/BaseApiSettingsSchema.ts @@ -65,10 +65,11 @@ export const BaseApiSettingsSchema = z.object({ /** * Controls how `const` values in OpenAPI specs are represented. * - `literals`: Convert const values directly to literals with defaults. - * - `enums`: Convert const values to single-element enums (current behavior). - * Defaults to `enums`. + * - `enums`: Convert const values to single-element enums; blocks transitive coercion via `coerce-enums-to-literals`. + * - `enums-coerceable-to-literals`: Convert const values to single-element enums, but allow `coerce-enums-to-literals` to coerce them transitively. + * Defaults to `enums-coerceable-to-literals`. */ - coerceConstsTo: z.enum(["literals", "enums"]).optional() + coerceConstsTo: z.enum(["literals", "enums", "enums-coerceable-to-literals"]).optional() }); export type BaseApiSettingsSchema = z.infer; diff --git a/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts b/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts index 8d1566330289..dfffbb4ab3b9 100644 --- a/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts +++ b/packages/cli/configuration/src/generators-yml/GeneratorsConfiguration.ts @@ -94,7 +94,7 @@ export interface APIDefinitionSettings { defaultIntegerFormat: generatorsYml.DefaultIntegerFormat | undefined; resolveSchemaCollisions: boolean | undefined; inferForwardCompatible: boolean | undefined; - coerceConstsTo: "literals" | "enums" | undefined; + coerceConstsTo: "literals" | "enums" | "enums-coerceable-to-literals" | undefined; } export interface APIDefinitionLocation { diff --git a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts index 0e474b580fc8..964eb20df5a6 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/BaseApiSettingsSchema.ts @@ -56,8 +56,9 @@ export interface BaseApiSettingsSchema { /** * Controls how `const` values in OpenAPI specs are represented. * - `literals`: Convert const values directly to literals with defaults. - * - `enums`: Convert const values to single-element enums (current behavior). - * Defaults to `enums`. + * - `enums`: Convert const values to single-element enums; blocks transitive coercion via `coerce-enums-to-literals`. + * - `enums-coerceable-to-literals`: Convert const values to single-element enums, but allow `coerce-enums-to-literals` to coerce them transitively. + * Defaults to `enums-coerceable-to-literals`. */ "coerce-consts-to"?: GeneratorsYml.CoerceConstsTo; } diff --git a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts index 9801a3fa5bc3..3a390df2decd 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/api/resources/generators/types/CoerceConstsTo.ts @@ -4,5 +4,6 @@ export const CoerceConstsTo = { Literals: "literals", Enums: "enums", + EnumsCoerceableToLiterals: "enums-coerceable-to-literals", } as const; export type CoerceConstsTo = (typeof CoerceConstsTo)[keyof typeof CoerceConstsTo]; diff --git a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts index 11cf45779449..28f5fe09412c 100644 --- a/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts +++ b/packages/cli/configuration/src/generators-yml/schemas/serialization/resources/generators/types/CoerceConstsTo.ts @@ -7,8 +7,8 @@ import type * as serializers from "../../../index.js"; export const CoerceConstsTo: core.serialization.Schema< serializers.CoerceConstsTo.Raw, GeneratorsYml.CoerceConstsTo -> = core.serialization.enum_(["literals", "enums"]); +> = core.serialization.enum_(["literals", "enums", "enums-coerceable-to-literals"]); export declare namespace CoerceConstsTo { - export type Raw = "literals" | "enums"; + export type Raw = "literals" | "enums" | "enums-coerceable-to-literals"; } diff --git a/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts b/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts index c846b1c228e4..59f9e3d62a42 100644 --- a/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts +++ b/packages/commons/api-workspace-commons/src/openapi/BaseOpenAPIWorkspace.ts @@ -25,7 +25,7 @@ export declare namespace BaseOpenAPIWorkspace { removeDiscriminantsFromSchemas: generatorsYml.RemoveDiscriminantsFromSchemas | undefined; defaultIntegerFormat: generatorsYml.DefaultIntegerFormat | undefined; pathParameterOrder: generatorsYml.PathParameterOrder | undefined; - coerceConstsTo: "literals" | "enums" | undefined; + coerceConstsTo: "literals" | "enums" | "enums-coerceable-to-literals" | undefined; } export type Settings = Partial; @@ -49,7 +49,7 @@ export abstract class BaseOpenAPIWorkspace extends AbstractAPIWorkspace