mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +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 { parseOpenAPI, traverse } from '@gitbook/openapi-parser';
|
||||
import serverPrecedenceSpec from './fixtures/spec-server-precedence.json';
|
||||
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) {
|
||||
const response = await fetch(url);
|
||||
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)
|
||||
? schema.security
|
||||
: schema.security
|
||||
@@ -112,6 +112,27 @@ function getPathObjectParameter(
|
||||
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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user