Skip to content

Commit 656c429

Browse files
feat: resolve server url (#1936)
1 parent a2f95ac commit 656c429

File tree

7 files changed

+133
-41
lines changed

7 files changed

+133
-41
lines changed

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ module.exports = {
2424
lines: 64,
2525
},
2626
'packages/respect-core/': {
27-
statements: 84,
27+
statements: 83,
2828
branches: 74,
2929
functions: 84,
3030
lines: 84,

packages/respect-core/src/modules/__tests__/flow-runner/get-server-url.test.ts

Lines changed: 69 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ describe('getServerUrl', () => {
1212
} as unknown as TestContext;
1313
const descriptionName = 'test';
1414
const result = getServerUrl({ ctx, descriptionName });
15-
expect(result).toEqual({ url: 'https://example.com' });
15+
expect(result).toEqual({ url: 'https://example.com', parameters: [] });
1616
});
1717

1818
it('should return undefined when path does not include url and no servers provided', () => {
@@ -38,7 +38,7 @@ describe('getServerUrl', () => {
3838
} as unknown as TestContext;
3939
const descriptionName = 'test';
4040
const result = getServerUrl({ ctx, descriptionName });
41-
expect(result).toEqual({ url: 'https://example.com' });
41+
expect(result).toEqual({ url: 'https://example.com', parameters: [] });
4242
});
4343

4444
it('should return server url from sourceDescription x-serverUrl resolved from context', () => {
@@ -56,7 +56,7 @@ describe('getServerUrl', () => {
5656
} as unknown as TestContext;
5757
const descriptionName = 'test';
5858
const result = getServerUrl({ ctx, descriptionName });
59-
expect(result).toEqual({ url: 'https://example.com' });
59+
expect(result).toEqual({ url: 'https://example.com', parameters: [] });
6060
});
6161

6262
it('should return overwritten server url from sourceDescription x-serverUrl resolved from context', () => {
@@ -97,7 +97,7 @@ describe('getServerUrl', () => {
9797
} as unknown as OperationDetails & { servers: { url: string }[] };
9898
const descriptionName = 'test';
9999
const result = getServerUrl({ ctx, descriptionName, openapiOperation });
100-
expect(result).toEqual({ url: 'https://example1.com' });
100+
expect(result).toEqual({ url: 'https://example1.com', parameters: [] });
101101
});
102102

103103
it('should return "x-operation" url as server url when descriptionName is not provided', () => {
@@ -156,7 +156,7 @@ describe('getServerUrl', () => {
156156
} as unknown as TestContext;
157157

158158
const result = getServerUrl({ ctx, descriptionName: '' });
159-
expect(result).toEqual({ url: 'https://api.example.com' });
159+
expect(result).toEqual({ url: 'https://api.example.com', parameters: [] });
160160
});
161161

162162
it('should return undefined when neither x-serverUrl nor $sourceDescriptions server is available when descriptionName is not provided and there is only one sourceDescription', () => {
@@ -211,7 +211,7 @@ describe('getServerUrl', () => {
211211
openapiOperation: mockDescriptionOperation,
212212
} as unknown as GetServerUrlInput);
213213

214-
expect(result).toEqual({ url: 'https://server1.com' });
214+
expect(result).toEqual({ url: 'https://server1.com', parameters: [] });
215215
});
216216

217217
it('should return undefined when descriptionOperation.servers is empty', () => {
@@ -285,4 +285,67 @@ describe('getServerUrl', () => {
285285
const result = getServerUrl({ ctx, descriptionName });
286286
expect(result).toEqual({ url: 'https://cli.com' });
287287
});
288+
289+
it('should return server url from openapi server with variables', () => {
290+
const ctx = {
291+
$sourceDescriptions: {
292+
test: {
293+
paths: {
294+
'/test': {
295+
get: {
296+
servers: [
297+
{
298+
url: 'https://{region}@{task}.{region}.openapi-server-with-vars.{domain}/v1?param={task}#fragment,param={task}',
299+
variables: {
300+
domain: {
301+
default: 'com',
302+
enum: ['com', 'net'],
303+
},
304+
region: {
305+
default: 'us',
306+
enum: ['us', 'eu', 'asia'],
307+
},
308+
task: {
309+
default: 'ping',
310+
},
311+
},
312+
},
313+
],
314+
},
315+
},
316+
},
317+
},
318+
},
319+
} as unknown as TestContext;
320+
const openapiOperation = {
321+
servers: [
322+
{
323+
url: 'https://{region}@{task}.{region}.openapi-server-with-vars.{domain}/v1?param={task}#fragment,param={task}',
324+
variables: {
325+
domain: {
326+
default: 'com',
327+
enum: ['com', 'net'],
328+
},
329+
region: {
330+
default: 'us',
331+
enum: ['us', 'eu', 'asia'],
332+
},
333+
task: {
334+
default: 'ping',
335+
},
336+
},
337+
},
338+
],
339+
} as unknown as OperationDetails;
340+
const descriptionName = 'test';
341+
const result = getServerUrl({ ctx, descriptionName, openapiOperation });
342+
expect(result).toEqual({
343+
url: 'https://{region}@{task}.{region}.openapi-server-with-vars.{domain}/v1?param={task}#fragment,param={task}',
344+
parameters: [
345+
{ name: 'domain', value: 'com', in: 'path' },
346+
{ name: 'region', value: 'us', in: 'path' },
347+
{ name: 'task', value: 'ping', in: 'path' },
348+
],
349+
});
350+
});
288351
});

packages/respect-core/src/modules/__tests__/flow-runner/prepare-request.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ describe('prepareRequest', () => {
428428
summary: 'Get a list of breeds',
429429
tags: ['Breeds'],
430430
});
431-
expect(serverUrl).toEqual({ url: 'https://catfact.ninja/' });
431+
expect(serverUrl).toEqual({ url: 'https://catfact.ninja/', parameters: [] });
432432
expect(requestBody).toEqual(undefined);
433433
});
434434

packages/respect-core/src/modules/description-parser/get-request-data-from-openapi.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface OpenApiRequestData {
1414
requestBody?: Record<string, unknown>;
1515
contentType?: string;
1616
parameters: ParameterWithIn[];
17+
contentTypeParameters: ParameterWithIn[];
1718
}
1819

1920
export function getRequestDataFromOpenApi(
@@ -30,13 +31,15 @@ export function getRequestDataFromOpenApi(
3031
const accept = getAcceptHeader(operation);
3132
const parameters = getUniqueParameters([
3233
...transformParameters(operation.pathParameters),
33-
{ name: 'content-type', in: 'header' as const, value: contentType },
34-
...(accept ? [{ name: 'accept', in: 'header' as const, value: accept }] : []),
3534
...transformParameters(operation.parameters),
3635
]).filter(({ value }) => value);
3736

3837
return {
3938
parameters,
39+
contentTypeParameters: [
40+
...(contentType ? [{ name: 'content-type', in: 'header' as const, value: contentType }] : []),
41+
...(accept ? [{ name: 'accept', in: 'header' as const, value: accept }] : []),
42+
],
4043
requestBody,
4144
contentType,
4245
};

packages/respect-core/src/modules/flow-runner/get-server-url.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ export type GetServerUrlInput = {
1111
xOperation?: ExtendedOperation;
1212
};
1313

14+
export type ServerObject = {
15+
url: string;
16+
description?: string;
17+
variables?: Record<
18+
string,
19+
{
20+
default: string;
21+
enum?: string[];
22+
description?: string;
23+
}
24+
>;
25+
};
26+
1427
export function getServerUrl({
1528
ctx,
1629
descriptionName,
@@ -42,9 +55,9 @@ export function getServerUrl({
4255
};
4356
}
4457

45-
return {
46-
url: getValueFromContext('$' + `sourceDescriptions.${descriptionName}.servers.0.url`, ctx),
47-
};
58+
return resolveOpenApiServerUrlWithVariables(
59+
getValueFromContext('$' + `sourceDescriptions.${descriptionName}.servers.0`, ctx)
60+
);
4861
}
4962

5063
if (openapiOperation?.servers?.[0]) {
@@ -65,20 +78,23 @@ export function getServerUrl({
6578
: undefined;
6679
}
6780

68-
return serverUrlOverride ? { url: serverUrlOverride } : openapiOperation.servers[0];
81+
return serverUrlOverride
82+
? { url: serverUrlOverride }
83+
: resolveOpenApiServerUrlWithVariables(openapiOperation.servers[0]);
6984
}
7085

7186
if (!descriptionName && ctx?.sourceDescriptions && ctx.sourceDescriptions.length === 1) {
7287
const sourceDescription = ctx.sourceDescriptions[0];
7388

74-
let serverUrl = '';
7589
if ('x-serverUrl' in sourceDescription && sourceDescription['x-serverUrl']) {
76-
serverUrl = sourceDescription['x-serverUrl'];
90+
return { url: sourceDescription['x-serverUrl'] };
7791
} else {
78-
serverUrl = ctx.$sourceDescriptions[sourceDescription.name]?.servers[0]?.url || undefined;
92+
return (
93+
resolveOpenApiServerUrlWithVariables(
94+
ctx.$sourceDescriptions[sourceDescription.name]?.servers[0]
95+
) || undefined
96+
);
7997
}
80-
81-
return serverUrl ? { url: serverUrl } : undefined;
8298
}
8399

84100
if (
@@ -91,5 +107,23 @@ export function getServerUrl({
91107
}
92108

93109
// Get first available server url from the description
94-
return ctx.$sourceDescriptions[descriptionName].servers[0];
110+
return resolveOpenApiServerUrlWithVariables(
111+
ctx.$sourceDescriptions[descriptionName].servers?.[0]
112+
);
113+
}
114+
115+
function resolveOpenApiServerUrlWithVariables(server?: ServerObject) {
116+
if (!server) {
117+
return undefined;
118+
}
119+
return {
120+
url: server.url,
121+
parameters: Object.entries(server.variables || {})
122+
.map(([key, value]) => ({
123+
in: 'path',
124+
name: key,
125+
value: value.default,
126+
}))
127+
.filter(({ value }) => !!value),
128+
};
95129
}

packages/respect-core/src/modules/flow-runner/prepare-request.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ export async function prepareRequest(
5050

5151
let path = '';
5252
let method;
53-
const serverUrl: { url: string; descriptionName?: string } | undefined = getServerUrl({
53+
54+
const serverUrl: { url: string; parameters?: ParameterWithIn[] } | undefined = getServerUrl({
5455
ctx,
5556
descriptionName: openapiOperation?.descriptionName,
5657
openapiOperation,
@@ -89,7 +90,10 @@ export async function prepareRequest(
8990
typeof requestBody === 'object'
9091
? [{ in: 'header', name: 'content-type', value: 'application/json' }]
9192
: [],
92-
requestDataFromOpenAPI?.parameters || [],
93+
serverUrl?.parameters || [],
94+
requestDataFromOpenAPI?.contentTypeParameters || [],
95+
// if step.parameters is defined, we do not auto-populate parameters from the openapi operation
96+
step.parameters ? [] : requestDataFromOpenAPI?.parameters || [],
9397
resolveParameters(workflowLevelParameters, ctx),
9498
stepRequestBodyContentType
9599
? [{ in: 'header', name: 'content-type', value: stepRequestBodyContentType }]

packages/respect-core/src/utils/api-fetcher.ts

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,16 @@ export class ApiFetcher implements IFetcher {
153153
.join('; ');
154154
}
155155

156-
let resolvedPath = resolvePath(path, pathParams) || '';
156+
const resolvedPath = resolvePath(path, pathParams) || '';
157157
const pathWithSearchParams = `${resolvedPath}${
158158
searchParams.toString() ? '?' + searchParams.toString() : ''
159159
}`;
160-
const pathToFetch = `${trimTrailingSlash(serverUrl.url)}${pathWithSearchParams}`;
160+
const resolvedServerUrl = resolvePath(serverUrl.url, pathParams) || '';
161+
const urlToFetch = `${trimTrailingSlash(resolvedServerUrl)}${pathWithSearchParams}`;
161162

162-
if (pathToFetch.startsWith('/')) {
163+
if (urlToFetch.startsWith('/')) {
163164
logger.error(
164-
bgRed(` Wrong url: ${inverse(pathToFetch)} `) +
165+
bgRed(` Wrong url: ${inverse(urlToFetch)} `) +
165166
` Did you forget to provide a correct "serverUrl"?\n`
166167
);
167168
}
@@ -223,27 +224,16 @@ export class ApiFetcher implements IFetcher {
223224
// Start of the verbose logs
224225
this.initVerboseLogs({
225226
headerParams: maskedHeaderParams,
226-
host: serverUrl.url,
227+
host: resolvedServerUrl,
227228
method: (method || 'get').toUpperCase() as OperationMethod,
228229
path: maskedPathParams || '',
229230
body: maskedBody,
230231
});
231232

232233
const wrappedFetch = this.harLogs ? withHar(this.fetch, { har: this.harLogs }) : fetch;
233-
// Resolve pathToFetch with pathParams for the second time in order
234-
// to handle described servers->variables in the OpenAPI spec.
235-
// E.G.:
236-
// servers:
237-
// - url: 'https://api-sandbox.redocly.com/organizations/{organizationId}'
238-
// TODO: remove/update after the support of the described servers->variables in the Arazzo spec.
239-
resolvedPath = resolvePath(pathToFetch, pathParams) || '';
240-
if (!resolvedPath) {
241-
throw new Error('Path to fetch is undefined');
242-
}
243-
244234
const startTime = performance.now();
245235

246-
const result = await wrappedFetch(resolvedPath, {
236+
const result = await wrappedFetch(urlToFetch, {
247237
method: (method || 'get').toUpperCase() as OperationMethod,
248238
headers,
249239
...(!isEmpty(requestBody) && {
@@ -297,8 +287,6 @@ export class ApiFetcher implements IFetcher {
297287
time: responseTime,
298288
header: Object.fromEntries(result.headers?.entries() || []),
299289
contentType: responseContentType,
300-
query: Object.fromEntries(searchParams.entries()),
301-
path: pathParams,
302290
};
303291
};
304292
}

0 commit comments

Comments
 (0)