Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ export function generateIr({
taskContext.logger.warn(message);
}
}
const plainBasePath =
fernBasePathParsed != null && fernBasePathParsed.pathParameters.length === 0
? fernBasePathParsed.basePath
: undefined;

Object.entries(openApi.paths ?? {}).forEach(([path, pathItem]) => {
if (pathItem == null) {
Expand All @@ -157,16 +161,16 @@ export function generateIr({
}
switch (operation.type) {
case "async":
endpointsWithExample.push(...operation.sync);
endpointsWithExample.push(...operation.async);
endpointsWithExample.push(...prependBasePathToEndpoints(operation.sync, plainBasePath));
endpointsWithExample.push(...prependBasePathToEndpoints(operation.async, plainBasePath));
break;
case "http":
endpointsWithExample.push(...operation.value);
endpointsWithExample.push(...prependBasePathToEndpoints(operation.value, plainBasePath));
break;
case "streaming":
endpointsWithExample.push(...operation.streaming);
endpointsWithExample.push(...prependBasePathToEndpoints(operation.streaming, plainBasePath));
if (operation.nonStreaming) {
endpointsWithExample.push(...operation.nonStreaming);
endpointsWithExample.push(...prependBasePathToEndpoints(operation.nonStreaming, plainBasePath));
}
break;
case "webhook":
Expand Down Expand Up @@ -413,17 +417,14 @@ export function generateIr({
document: openApi
}),
specVersion: openApi.info.version != null && openApi.info.version.length > 0 ? openApi.info.version : undefined,
basePath: (() => {
const parsed = getFernBasePath(openApi);
return parsed?.basePath;
})(),
basePathParameters: (() => {
const parsed = getFernBasePath(openApi);
if (parsed == null || parsed.pathParameters.length === 0) {
return undefined;
}
return parsed.pathParameters;
})(),
basePath:
fernBasePathParsed != null && fernBasePathParsed.pathParameters.length > 0
? fernBasePathParsed.basePath
: undefined,
basePathParameters:
fernBasePathParsed != null && fernBasePathParsed.pathParameters.length > 0
? fernBasePathParsed.pathParameters
: undefined,
title: openApi.info.title ?? "",
description: openApi.info.description,
groups: Object.fromEntries(
Expand Down Expand Up @@ -531,6 +532,33 @@ function maybeRemoveDiscriminantsFromSchemas(
return result;
}

function prependBasePath(path: string, basePath: string | undefined): string {
if (basePath == null || basePath === "/") {
return path;
}

const normalizedBasePath = basePath.replace(/\/+$/, "");
if (path === normalizedBasePath || path.startsWith(`${normalizedBasePath}/`)) {
return path;
}

const normalizedPath = path.replace(/^\/+/, "");
return normalizedPath.length > 0 ? `${normalizedBasePath}/${normalizedPath}` : normalizedBasePath;
}

function prependBasePathToEndpoints(
endpoints: EndpointWithExample[],
basePath: string | undefined
): EndpointWithExample[] {
if (basePath == null || basePath === "/") {
return endpoints;
}
return endpoints.map((endpoint) => ({
...endpoint,
path: prependBasePath(endpoint.path, basePath)
}));
}

/**
* Collects parent schema IDs that have at least one allOf child NOT participating
* in any discriminated union. These parents are "shared" across union and non-union
Expand Down
41 changes: 33 additions & 8 deletions packages/cli/api-importers/openapi/openapi-ir-parser/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ export function parse({
source,
namespace: document.namespace
});
ir = merge(ir, openapiIr, getParseOptions({ options: document.settings, overrides: options }));
ir = merge(
ir,
openapiIr,
getParseOptions({ options: document.settings, overrides: options }),
context
);
documentIndex++;
break;
}
Expand Down Expand Up @@ -403,11 +408,31 @@ function hasGroupedServers(servers: AnyServerInput[]): boolean {
return servers.some((server) => server.type === "grouped");
}

function mergeBasePath(
ir1: OpenApiIntermediateRepresentation,
ir2: OpenApiIntermediateRepresentation,
context: TaskContext
): Pick<OpenApiIntermediateRepresentation, "basePath" | "basePathParameters"> {
if (ir1.basePath != null && ir2.basePath != null && ir1.basePath !== ir2.basePath) {
context.failWithoutThrowing(
`Conflicting parameterized x-fern-base-path values: '${ir1.basePath}' and '${ir2.basePath}'.`
);
}

return {
basePath: ir1.basePath ?? ir2.basePath,
basePathParameters: ir1.basePathParameters ?? ir2.basePathParameters
};
}

function merge(
ir1: OpenApiIntermediateRepresentation,
ir2: OpenApiIntermediateRepresentation,
options?: ParseOpenAPIOptions
options: ParseOpenAPIOptions | undefined,
context: TaskContext
): OpenApiIntermediateRepresentation {
const mergedBasePath = mergeBasePath(ir1, ir2, context);

// Only perform multi-API environment grouping if the feature flag is enabled
const shouldGroupEnvironments = options?.groupMultiApiEnvironments === true;

Expand All @@ -418,8 +443,8 @@ function merge(
specVersion: ir1.specVersion ?? ir2.specVersion,
title: ir1.title ?? ir2.title,
description: ir1.description ?? ir2.description,
basePath: ir1.basePath ?? ir2.basePath,
basePathParameters: ir1.basePathParameters ?? ir2.basePathParameters,
basePath: mergedBasePath.basePath,
basePathParameters: mergedBasePath.basePathParameters,
servers: [...ir1.servers, ...ir2.servers],
websocketServers: [...ir1.websocketServers, ...ir2.websocketServers],
tags: {
Expand Down Expand Up @@ -591,8 +616,8 @@ function merge(
specVersion: ir1.specVersion ?? ir2.specVersion,
title: ir1.title ?? ir2.title,
description: ir1.description ?? ir2.description,
basePath: ir1.basePath ?? ir2.basePath,
basePathParameters: ir1.basePathParameters ?? ir2.basePathParameters,
basePath: mergedBasePath.basePath,
basePathParameters: mergedBasePath.basePathParameters,
// Cast grouped servers to Server[] - buildEnvironments.ts handles the grouped structure
// biome-ignore lint/suspicious/noExplicitAny: Required to preserve grouped server metadata through type system
servers: mergedServers as any as Server[],
Expand Down Expand Up @@ -655,8 +680,8 @@ function merge(
specVersion: ir1.specVersion ?? ir2.specVersion,
title: ir1.title ?? ir2.title,
description: ir1.description ?? ir2.description,
basePath: ir1.basePath ?? ir2.basePath,
basePathParameters: ir1.basePathParameters ?? ir2.basePathParameters,
basePath: mergedBasePath.basePath,
basePathParameters: mergedBasePath.basePathParameters,
servers: dedupeServers([...ir1.servers, ...ir2.servers] as AnyServerInput[]) as Server[],
websocketServers: [...ir1.websocketServers, ...ir2.websocketServers],
tags: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{
"specVersion": "1.0.0",
"title": "Auth API",
"servers": [],
"websocketServers": [],
"tags": {
"tagsById": {}
},
"hasEndpointsMarkedInternal": false,
"endpoints": [
{
"audiences": [],
"operationId": "getToken",
"tags": [
"auth"
],
"namespace": "auth",
"pathParameters": [],
"queryParameters": [],
"headers": [],
"generatedRequestName": "GetTokenRequest",
"errors": {},
"servers": [],
"authed": false,
"method": "POST",
"path": "/token",
"examples": [
{
"pathParameters": [],
"queryParameters": [],
"headers": [],
"codeSamples": [],
"type": "full"
}
],
"source": {
"file": "../auth-api.yml",
"type": "openapi"
}
},
{
"audiences": [],
"operationId": "listItems",
"tags": [],
"pathParameters": [],
"queryParameters": [],
"headers": [],
"generatedRequestName": "ListItemsRequest",
"errors": {},
"servers": [],
"authed": false,
"method": "GET",
"path": "/api/v3/items",
"examples": [
{
"pathParameters": [],
"queryParameters": [],
"headers": [],
"codeSamples": [],
"type": "full"
}
],
"source": {
"file": "../main-api.yml",
"type": "openapi"
}
}
],
"webhooks": [],
"channels": {},
"groupedSchemas": {
"rootSchemas": {},
"namespacedSchemas": {
"auth": {}
}
},
"variables": {},
"nonRequestReferencedSchemas": {},
"securitySchemes": {},
"globalHeaders": [],
"idempotencyHeaders": [],
"groups": {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
"specVersion": "1.0",
"title": "Acme API",
"description": "The Acme API.",
"basePath": "/v1",
"servers": [
{
"url": "https://api.acme.com"
Expand All @@ -29,7 +28,7 @@
"servers": [],
"authed": false,
"method": "GET",
"path": "/example",
"path": "/v1/example",
"examples": [
{
"pathParameters": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
{
"absoluteFilePath": "/DUMMY_PATH",
"importedDefinitions": {},
"namedDefinitionFiles": {
"__package__.yml": {
"absoluteFilepath": "/DUMMY_PATH",
"contents": {
"service": {
"auth": false,
"base-path": "",
"endpoints": {
"listItems": {
"auth": undefined,
"docs": undefined,
"examples": [
{},
],
"method": "GET",
"pagination": undefined,
"path": "/api/v3/items",
"source": {
"openapi": "../main-api.yml",
},
},
},
"source": {
"openapi": "../main-api.yml",
},
},
},
"rawContents": "service:
auth: false
base-path: ''
endpoints:
listItems:
path: /api/v3/items
method: GET
source:
openapi: ../main-api.yml
examples:
- {}
source:
openapi: ../main-api.yml
",
},
"auth/__package__.yml": {
"absoluteFilepath": "/DUMMY_PATH",
"contents": {
"service": {
"auth": false,
"base-path": "",
"endpoints": {
"getToken": {
"auth": undefined,
"docs": undefined,
"examples": [
{},
],
"method": "POST",
"pagination": undefined,
"path": "/token",
"source": {
"openapi": "../auth-api.yml",
},
},
},
"source": {
"openapi": "../auth-api.yml",
},
},
},
"rawContents": "service:
auth: false
base-path: ''
endpoints:
getToken:
path: /token
method: POST
source:
openapi: ../auth-api.yml
examples:
- {}
source:
openapi: ../auth-api.yml
",
},
},
"packageMarkers": {},
"rootApiFile": {
"contents": {
"display-name": "Auth API",
"error-discrimination": {
"strategy": "status-code",
},
"name": "api",
},
"defaultUrl": undefined,
"rawContents": "name: api
error-discrimination:
strategy: status-code
display-name: Auth API
",
},
"specVersion": "1.0.0",
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
openapi: 3.0.0
info:
title: Auth API
version: 1.0.0
x-fern-base-path: /
paths:
/token:
post:
operationId: getToken
responses:
"200":
description: Token response
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"organization": "seed",
"version": "0.0.0"
}
Loading
Loading