Skip to content

Commit 83673d8

Browse files
authored
feat: allow mandatory server variables (#341)
- synthesis missing server placeholder variables as strings with no default values - treat server placeholder variables with `""` or no default as mandatory - refactor to put placeholder extraction into one place
1 parent 5ad9298 commit 83673d8

File tree

11 files changed

+129
-97
lines changed

11 files changed

+129
-97
lines changed

integration-tests/typescript-angular/src/generated/azure-core-data-plane-service.tsp/client.service.ts

Lines changed: 3 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/typescript-axios/src/generated/azure-core-data-plane-service.tsp/client.ts

Lines changed: 2 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/typescript-fetch/src/generated/azure-core-data-plane-service.tsp/client.ts

Lines changed: 2 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/documentation/src/app/guides/concepts/servers-object-handling/page.mdx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {Callout} from "nextra/components"
1010
OpenAPI 3 has a `servers` property that can be used to define the base url for the whole document, or
1111
specific operations. This guide aims to explain how this is processed.
1212

13-
You can find the specifications definition of the servers object [here](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#server-object)
13+
You can find the specification's definition of the servers object [here](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#server-object)
1414

1515
This is fully supported, including placeholder/variable substitution, and overriding.
1616

@@ -110,6 +110,20 @@ export class ApiClient {
110110
As you can see the overrides for each operation are exposed as `ApiClientServers.operations.<operationId>()` following
111111
the same pattern as the root servers.
112112

113+
## Mandatory Variables
114+
Sometimes there are variables in urls that you can't provide a reasonable default for. A prime example would be
115+
an organization slug, eg: `https://api.myOrg.someSaas.com`
116+
117+
Whilst the specification doesn't specifically discuss a concept of mandatory variables, we allow these using one
118+
of two methods:
119+
- Providing a default of `""` (as `default` is a required property)
120+
- Omitting the variable entirely from the `variables` map
121+
122+
A warning will be emitted if you omit the variable entirely, so its suggested you use a default of `""` for
123+
this scenario.
124+
125+
Regardless, any unconsumed/unused variables will trigger an error with the intention of picking up on typos.
126+
113127
## Configuration
114128
This behavior is optional, and be turned off by passing:
115129
```

packages/openapi-code-generator/src/core/input.ts

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@ import type {
2929
IRServerVariable,
3030
MaybeIRModel,
3131
} from "./openapi-types-normalized"
32-
import {isRef} from "./openapi-utils"
32+
import {extractPlaceholders, isRef} from "./openapi-utils"
3333
import {
3434
camelCase,
3535
coalesce,
3636
deepEqual,
37+
isDefined,
3738
isHttpMethod,
3839
mediaTypeToIdentifier,
3940
} from "./utils"
@@ -233,20 +234,42 @@ export class Input {
233234
private normalizeServers(servers: Server[]): IRServer[] {
234235
return servers
235236
.filter((it) => it.url)
236-
.map((it) => ({
237-
url: it.url,
238-
description: it.description,
239-
variables: Object.fromEntries(
240-
Object.entries(it.variables ?? {}).map(([key, value]) => [
241-
key,
242-
{
243-
enum: value.enum ?? [],
244-
default: value.default,
245-
description: value.description,
246-
} satisfies IRServerVariable,
247-
]),
248-
),
249-
}))
237+
.map((it) => {
238+
const variables = Object.entries(it.variables ?? {}).map(
239+
([key, value]): IRServerVariable => ({
240+
name: key,
241+
enum: value.enum ?? [],
242+
default: value.default,
243+
description: value.description,
244+
}),
245+
)
246+
247+
const placeholders = extractPlaceholders(it.url)
248+
.map((it) => it.placeholder)
249+
.filter(isDefined)
250+
251+
for (const placeholder of placeholders) {
252+
const variable = variables.find((it) => it.name === placeholder)
253+
254+
if (!variable) {
255+
logger.warn(
256+
`missing placeholder variable for server url '${it.url}' - inserting variable of type string with no default`,
257+
)
258+
variables.push({
259+
name: placeholder,
260+
default: undefined,
261+
enum: [],
262+
description: undefined,
263+
})
264+
}
265+
}
266+
267+
return {
268+
url: it.url,
269+
description: it.description,
270+
variables,
271+
}
272+
})
250273
}
251274

252275
private normalizeRequestBodyObject(

packages/openapi-code-generator/src/core/openapi-types-normalized.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,13 @@ export type MaybeIRModel = IRModel | IRRef
106106
export interface IRServer {
107107
url: string
108108
description: string | undefined
109-
variables: {
110-
[k: string]: IRServerVariable
111-
}
109+
variables: IRServerVariable[]
112110
}
113111

114112
export interface IRServerVariable {
113+
name: string
115114
enum: string[]
116-
default: string
115+
default: string | undefined
117116
description: string | undefined
118117
}
119118

packages/openapi-code-generator/src/core/openapi-utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@ export function getNameFromRef({$ref}: Reference, prefix: string): string {
1919
return prefix + name.replace(/[-.]+/g, "_")
2020
}
2121

22+
export function extractPlaceholders(
23+
it: string,
24+
): {wholeString: string; placeholder: string | undefined}[] {
25+
const regex = /{([^{}]+)}/g
26+
27+
return Array.from(it.matchAll(regex)).map((match) => ({
28+
// includes the surrounding {}
29+
wholeString: match[0],
30+
// just the placeholder name
31+
placeholder: match[1],
32+
}))
33+
}
34+
2235
export function getTypeNameFromRef(reference: Reference) {
2336
return getNameFromRef(reference, "t_")
2437
}

packages/openapi-code-generator/src/typescript/client/client-operation-builder.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
IRParameter,
55
MaybeIRModel,
66
} from "../../core/openapi-types-normalized"
7+
import {extractPlaceholders} from "../../core/openapi-utils"
78
import {camelCase, isDefined} from "../../core/utils"
89
import type {SchemaBuilder} from "../common/schema-builders/schema-builder"
910
import type {TypeBuilder} from "../common/type-builder"
@@ -54,32 +55,29 @@ export class ClientOperationBuilder {
5455

5556
routeToTemplateString(paramName = "p"): string {
5657
const {route, parameters} = this.operation
57-
const placeholder = /{([^{}]+)}/g
58+
const placeholders = extractPlaceholders(route)
5859

59-
return Array.from(route.matchAll(placeholder)).reduce((result, match) => {
60-
const wholeString = match[0]
61-
const placeholderName = match[1]
62-
63-
if (!placeholderName) {
60+
return placeholders.reduce((result, {placeholder, wholeString}) => {
61+
if (!placeholder) {
6462
throw new Error(
65-
`invalid route parameter placeholder in route '${placeholder}'`,
63+
`invalid route parameter placeholder in route '${route}'`,
6664
)
6765
}
6866

6967
const parameter = parameters.find(
70-
(it) => it.name === placeholderName && it.in === "path",
68+
(it) => it.name === placeholder && it.in === "path",
7169
)
7270

7371
if (!parameter) {
7472
throw new Error(
75-
`invalid route ${route}. missing path parameter for '${placeholderName}'`,
73+
`invalid route ${route}. missing path parameter for '${placeholder}'`,
7674
)
7775
}
7876

7977
return result.replace(
8078
wholeString,
8179
// TODO: why do we camelCase here? Feels presumptuous
82-
`\${${paramName}["${camelCase(placeholderName)}"]}`,
80+
`\${${paramName}["${camelCase(placeholder)}"]}`,
8381
)
8482
}, route)
8583
}

packages/openapi-code-generator/src/typescript/client/client-servers-builder.spec.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -64,23 +64,25 @@ describe("typescript/common/client-servers-builder", () => {
6464
result = await runTest([
6565
{
6666
url: "{schema}://unit-tests-default.{tenant}.example.com",
67-
variables: {
68-
schema: {
67+
variables: [
68+
{
69+
name: "schema",
6970
default: "https",
7071
enum: ["https", "http"],
7172
description: "The protocol to use",
7273
},
73-
tenant: {
74+
{
75+
name: "tenant",
7476
default: "example",
7577
enum: [],
7678
description: "Your tenant slug",
7779
},
78-
},
80+
],
7981
description: "Default Unit test server",
8082
},
8183
{
8284
url: "https://unit-tests-other.example.com",
83-
variables: {},
85+
variables: [],
8486
description: "Secondary Unit test server",
8587
},
8688
])
@@ -148,12 +150,12 @@ describe("typescript/common/client-servers-builder", () => {
148150
[
149151
{
150152
url: "https://unit-tests-default.example.com",
151-
variables: {},
153+
variables: [],
152154
description: "Default Unit test server",
153155
},
154156
{
155157
url: "https://unit-tests-other.example.com",
156-
variables: {},
158+
variables: [],
157159
description: "Secondary Unit test server",
158160
},
159161
],
@@ -163,18 +165,20 @@ describe("typescript/common/client-servers-builder", () => {
163165
servers: [
164166
{
165167
url: "{schema}}://test-operation.{tenant}.example.com",
166-
variables: {
167-
schema: {
168+
variables: [
169+
{
170+
name: "schema",
168171
default: "https",
169172
enum: ["https", "http"],
170173
description: "The protocol to use",
171174
},
172-
tenant: {
175+
{
176+
name: "tenant",
173177
default: "example",
174178
enum: [],
175179
description: "Your tenant slug",
176180
},
177-
},
181+
],
178182
description: "Test operation server",
179183
},
180184
],
@@ -184,7 +188,7 @@ describe("typescript/common/client-servers-builder", () => {
184188
servers: [
185189
{
186190
url: "https://another-test-operation.example.com",
187-
variables: {},
191+
variables: [],
188192
description: "Another test operation server",
189193
},
190194
],

0 commit comments

Comments
 (0)