Skip to content

Commit 9eccf34

Browse files
Copilotardatan
andauthored
Fix endpoint type inference for OpenAPI server variable templates (#3664)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ardatan <20847995+ardatan@users.noreply.github.com>
1 parent 8830fe1 commit 9eccf34

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

packages/fets/src/client/types.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,20 @@ export interface ClientOptions {
473473
globalParams?: ClientRequestParams;
474474
}
475475

476+
type ServerVariableType<TVarName extends string, TVariables> =
477+
TVariables extends Record<string, unknown>
478+
? TVarName extends keyof TVariables
479+
? TVariables[TVarName] extends { enum: readonly (infer TEnum extends string)[] }
480+
? TEnum
481+
: string
482+
: string
483+
: string;
484+
485+
type ResolveServerUrl<TUrl extends string, TVariables> =
486+
TUrl extends `${infer Before}{${infer VarName}}${infer After}`
487+
? `${Before}${ServerVariableType<VarName, TVariables>}${ResolveServerUrl<After, TVariables>}`
488+
: TUrl;
489+
476490
export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit<
477491
ClientOptions,
478492
'endpoint'
@@ -489,15 +503,15 @@ export type ClientOptionsWithStrictEndpoint<TOAS extends OpenAPIDocument> = Omit
489503
endpoint: TEndpoint;
490504
}
491505
: TOAS extends {
492-
servers: { url: infer TEndpoint extends string }[];
506+
servers: { url: infer TEndpoint extends string; variables?: infer TVariables }[];
493507
}
494508
? {
495509
/**
496510
* The base URL of the API defined in the OAS document.
497511
*
498512
* @see https://swagger.io/docs/specification/api-host-and-base-path/
499513
*/
500-
endpoint: TEndpoint;
514+
endpoint: ResolveServerUrl<TEndpoint, TVariables>;
501515
}
502516
: TOAS extends {
503517
host: infer THost extends string;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
export default {
2+
openapi: '3.0.0',
3+
info: {
4+
title: 'Server Variables Test API',
5+
version: '1.0.0',
6+
},
7+
servers: [
8+
{
9+
url: 'https://{username}.server.com:{port}/{version}',
10+
variables: {
11+
username: {
12+
default: 'demo',
13+
description: 'This value is assigned by the service provider.',
14+
},
15+
port: {
16+
enum: ['8443', '443'],
17+
default: '8443',
18+
},
19+
version: {
20+
default: 'v1',
21+
},
22+
},
23+
},
24+
],
25+
paths: {
26+
'/users': {
27+
get: {
28+
operationId: 'getUsers',
29+
responses: {
30+
200: {
31+
description: 'A list of users',
32+
content: {
33+
'application/json': {
34+
schema: {
35+
type: 'array',
36+
items: {
37+
type: 'object',
38+
properties: {
39+
id: { type: 'string' },
40+
name: { type: 'string' },
41+
},
42+
},
43+
},
44+
},
45+
},
46+
},
47+
},
48+
},
49+
},
50+
},
51+
} as const;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { createClient, type NormalizeOAS } from 'fets';
2+
import type { ClientOptionsWithStrictEndpoint } from '../../src/client/types';
3+
import serverVariablesOas from './fixtures/example-server-variables-oas';
4+
5+
type NormOAS = NormalizeOAS<typeof serverVariablesOas>;
6+
7+
// Valid endpoint matching the server variable template (port from enum, username/version are strings)
8+
const client = createClient<NormOAS>({
9+
endpoint: 'https://me.server.com:443/v1',
10+
});
11+
12+
// Port from enum: '8443' also valid
13+
const client2 = createClient<NormOAS>({
14+
endpoint: 'https://demo.server.com:8443/v2',
15+
});
16+
17+
void client;
18+
void client2;
19+
20+
// Verify the endpoint type is properly inferred via ClientOptionsWithStrictEndpoint
21+
// Port '444' is not in the enum ['8443', '443'] - this should be a type error
22+
const _invalidOpts: ClientOptionsWithStrictEndpoint<NormOAS> = {
23+
// @ts-expect-error - port '444' is not in the enum ['8443', '443']
24+
endpoint: 'https://me.server.com:444/v1',
25+
};
26+
void _invalidOpts;
27+
28+
const getUsersRes = await client['/users'].get();
29+
30+
if (getUsersRes.ok) {
31+
const users = await getUsersRes.json();
32+
void users[0]?.id;
33+
}

0 commit comments

Comments
 (0)