mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-21 10:03:31 +00:00
Use precedence for OpenAPI servers (#4020)
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@gitbook/react-openapi": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Use precedence for OpenAPI servers
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"openapi": "3.0.0",
|
||||||
|
"info": {
|
||||||
|
"title": "Server Precedence Test",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
"servers": [{ "url": "https://root.example.com" }],
|
||||||
|
"paths": {
|
||||||
|
"/root-only": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "rootOnly",
|
||||||
|
"responses": { "200": { "description": "OK" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/path-override": {
|
||||||
|
"servers": [{ "url": "https://path.example.com" }],
|
||||||
|
"get": {
|
||||||
|
"operationId": "pathOverride",
|
||||||
|
"responses": { "200": { "description": "OK" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/operation-override": {
|
||||||
|
"servers": [{ "url": "https://path.example.com" }],
|
||||||
|
"get": {
|
||||||
|
"operationId": "operationOverride",
|
||||||
|
"servers": [{ "url": "https://operation.example.com" }],
|
||||||
|
"responses": { "200": { "description": "OK" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/operation-skip-path": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "operationSkipPath",
|
||||||
|
"servers": [{ "url": "https://operation.example.com" }],
|
||||||
|
"responses": { "200": { "description": "OK" } }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/no-servers": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "noServers",
|
||||||
|
"responses": { "200": { "description": "OK" } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
import { describe, expect, it } from 'bun:test';
|
import { describe, expect, it } from 'bun:test';
|
||||||
|
|
||||||
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
import { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
||||||
|
import serverPrecedenceSpec from './fixtures/spec-server-precedence.json';
|
||||||
import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
|
import { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
|
||||||
|
|
||||||
|
async function loadFixture(spec: object) {
|
||||||
|
const { filesystem } = await parseOpenAPI({
|
||||||
|
value: JSON.stringify(spec),
|
||||||
|
rootURL: 'memory://spec.json',
|
||||||
|
});
|
||||||
|
return filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFilesystem(url: string) {
|
async function fetchFilesystem(url: string) {
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
@@ -174,4 +183,56 @@ describe('#resolveOpenAPIOperation', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('server precedence', () => {
|
||||||
|
it('should use root-level servers when no path or operation servers are defined', async () => {
|
||||||
|
const filesystem = await loadFixture(serverPrecedenceSpec);
|
||||||
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
||||||
|
method: 'get',
|
||||||
|
path: '/root-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolved?.servers).toEqual([{ url: 'https://root.example.com' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use path-level servers over root-level servers', async () => {
|
||||||
|
const filesystem = await loadFixture(serverPrecedenceSpec);
|
||||||
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
||||||
|
method: 'get',
|
||||||
|
path: '/path-override',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolved?.servers).toEqual([{ url: 'https://path.example.com' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use operation-level servers over path and root-level servers', async () => {
|
||||||
|
const filesystem = await loadFixture(serverPrecedenceSpec);
|
||||||
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
||||||
|
method: 'get',
|
||||||
|
path: '/operation-override',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolved?.servers).toEqual([{ url: 'https://operation.example.com' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use operation-level servers over root-level when no path servers exist', async () => {
|
||||||
|
const filesystem = await loadFixture(serverPrecedenceSpec);
|
||||||
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
||||||
|
method: 'get',
|
||||||
|
path: '/operation-skip-path',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolved?.servers).toEqual([{ url: 'https://operation.example.com' }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fallback to root-level servers for endpoints without overrides', async () => {
|
||||||
|
const filesystem = await loadFixture(serverPrecedenceSpec);
|
||||||
|
const resolved = await resolveOpenAPIOperation(filesystem, {
|
||||||
|
method: 'get',
|
||||||
|
path: '/no-servers',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(resolved?.servers).toEqual([{ url: 'https://root.example.com' }]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function resolveOpenAPIOperation(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const servers = 'servers' in schema ? (schema.servers ?? []) : [];
|
const servers = getServers(schema, path, operation);
|
||||||
const schemaSecurity = Array.isArray(schema.security)
|
const schemaSecurity = Array.isArray(schema.security)
|
||||||
? schema.security
|
? schema.security
|
||||||
: schema.security
|
: schema.security
|
||||||
@@ -112,6 +112,27 @@ function getPathObjectParameter(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve servers for an operation following OpenAPI precedence rules.
|
||||||
|
* Per the spec, only the lowest-level servers array is used: operation > path > root.
|
||||||
|
*/
|
||||||
|
function getServers(
|
||||||
|
schema: OpenAPIV3.Document | OpenAPIV3_1.Document,
|
||||||
|
path: string,
|
||||||
|
operation: OpenAPIV3.OperationObject
|
||||||
|
): OpenAPIV3.ServerObject[] {
|
||||||
|
if ('servers' in operation && operation.servers) {
|
||||||
|
return operation.servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathObject = getPathObject(schema, path);
|
||||||
|
if (pathObject && 'servers' in pathObject && pathObject.servers) {
|
||||||
|
return pathObject.servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'servers' in schema ? (schema.servers ?? []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an operation by its path and method.
|
* Get an operation by its path and method.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user