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
@@ -0,0 +1,5 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Allow absolute API spec, override, and overlay paths in generators.yml.
type: feat
7 changes: 5 additions & 2 deletions packages/cli/cli/src/commands/upgrade/updateApiSpec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generatorsYml, loadGeneratorsConfiguration } from "@fern-api/configuration-loader";
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { AbsoluteFilePath, resolveConfiguredFilepath } from "@fern-api/fs-utils";
import { Logger } from "@fern-api/logger";
import { Project } from "@fern-api/project-loader";
import { CliError } from "@fern-api/task-context";
Expand Down Expand Up @@ -381,7 +381,10 @@ async function getAndFetchFromAPIDefinitionLocation({
return;
}
if (apiLocation.origin != null) {
const filePath = join(workspacePath, RelativeFilePath.of(apiLocation.schema.path));
const filePath = resolveConfiguredFilepath({
absolutePathToWorkspace: workspacePath,
configuredFilepath: apiLocation.schema.path
});

if (apiLocation.schema.type === "graphql") {
cliContext.logger.info(`GraphQL schema origin found, fetching schema from ${apiLocation.origin}`);
Expand Down
71 changes: 33 additions & 38 deletions packages/cli/workspace/lazy-fern-workspace/src/OSSWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ import { Audiences, generatorsYml } from "@fern-api/configuration";
import { extractErrorMessage, isNonNullish } from "@fern-api/core-utils";
import { FdrAPI } from "@fern-api/fdr-sdk";
import { RawSchemas } from "@fern-api/fern-definition-schema";
import { AbsoluteFilePath, cwd, dirname, join, RelativeFilePath, relativize } from "@fern-api/fs-utils";
import {
AbsoluteFilePath,
cwd,
dirname,
join,
RelativeFilePath,
relativize,
resolveConfiguredFilepath,
resolveConfiguredFilepaths
} from "@fern-api/fs-utils";
import type { GraphQlOperationExamplesInput } from "@fern-api/graphql-to-fdr";
import { IntermediateRepresentation, serialization } from "@fern-api/ir-sdk";
import { mergeIntermediateRepresentation } from "@fern-api/ir-utils";
Expand All @@ -29,11 +38,11 @@ import {
} from "@fern-api/openapi-to-ir";
import { OpenRPCConverter, OpenRPCConverterContext3_1 } from "@fern-api/openrpc-to-ir";
import { CliError, TaskContext } from "@fern-api/task-context";

import { ErrorCollector } from "@fern-api/v3-importer-commons";
import { readFile } from "fs/promises";
import yaml from "js-yaml";
import { OpenAPIV3_1 } from "openapi-types";
import path from "path";
import { v4 as uuidv4 } from "uuid";
import { loadOpenRpc } from "./loaders/index.js";
import { OpenAPILoader } from "./loaders/OpenAPILoader.js";
Expand Down Expand Up @@ -73,17 +82,6 @@ function collapseSpecBooleanSetting(
return values.every((v) => v == null || v === true);
}

function toRelativePath(raw: string, field: string): RelativeFilePath {
try {
return RelativeFilePath.of(raw);
} catch {
throw new CliError({
message: `"${field}: ${raw}" must be a relative path, not an absolute one.`,
code: CliError.Code.ConfigError
});
}
}

function convertRemoveDiscriminantsFromSchemas(
specs: (OpenAPISpec | ProtobufSpec)[]
): generatorsYml.RemoveDiscriminantsFromSchemas {
Expand Down Expand Up @@ -342,10 +340,11 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace {
const errorCollectors: ErrorCollector[] = [];

for (const document of documents) {
const absoluteFilepathToSpec = join(
this.absoluteFilePath,
RelativeFilePath.of(document.source?.file ?? "")
);
const sourceFile = document.source?.file;
const absoluteFilepathToSpec =
sourceFile != null && path.isAbsolute(sourceFile)
? AbsoluteFilePath.of(sourceFile)
: join(this.absoluteFilePath, RelativeFilePath.of(sourceFile ?? ""));
const relativeFilepathToSpec = relativize(cwd(), absoluteFilepathToSpec);

const errorCollector = new ErrorCollector({ logger: context.logger, relativeFilepathToSpec });
Expand Down Expand Up @@ -652,33 +651,29 @@ export class OSSWorkspace extends BaseOpenAPIWorkspace {

for (const spec of specsOverride) {
if (generatorsYml.isOpenApiSpecSchema(spec)) {
const absoluteFilepath = join(this.absoluteFilePath, toRelativePath(spec.openapi, "openapi"));
const absoluteFilepath = resolveConfiguredFilepath({
absolutePathToWorkspace: this.absoluteFilePath,
configuredFilepath: spec.openapi
});
// Handle both single override path and array of override paths
let absoluteFilepathToOverrides: AbsoluteFilePath | AbsoluteFilePath[] | undefined;
const specOverridePaths: AbsoluteFilePath[] = [];

// Add spec-level overrides
if (spec.overrides != null) {
if (Array.isArray(spec.overrides)) {
specOverridePaths.push(
...spec.overrides.map((override) =>
join(this.absoluteFilePath, toRelativePath(override, "overrides"))
)
);
} else {
specOverridePaths.push(
join(this.absoluteFilePath, toRelativePath(spec.overrides, "overrides"))
);
}
}

// Set the final overrides array
if (specOverridePaths.length > 0) {
absoluteFilepathToOverrides =
specOverridePaths.length === 1 ? specOverridePaths[0] : specOverridePaths;
const resolvedOverrides = resolveConfiguredFilepaths({
absolutePathToWorkspace: this.absoluteFilePath,
configuredFilepaths: spec.overrides
});
absoluteFilepathToOverrides = Array.isArray(resolvedOverrides)
? resolvedOverrides.length === 1
? resolvedOverrides[0]
: resolvedOverrides
: resolvedOverrides;
}
const absoluteFilepathToOverlays = spec.overlays
? join(this.absoluteFilePath, toRelativePath(spec.overlays, "overlays"))
? resolveConfiguredFilepath({
absolutePathToWorkspace: this.absoluteFilePath,
configuredFilepath: spec.overlays
})
: undefined;

// Create a minimal OpenAPI spec with default settings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Spec } from "@fern-api/api-workspace-commons";
import { generatorsYml } from "@fern-api/configuration";
import { AbsoluteFilePath } from "@fern-api/fs-utils";
import assert from "assert";
import { join } from "path";

import { OSSWorkspace } from "../OSSWorkspace.js";
Expand Down Expand Up @@ -116,4 +117,23 @@ describe("convertSpecsOverrideToSpecs", () => {
expect(specs[0]?.absoluteFilepathToOverrides).toBeUndefined();
expect(specs[1]?.absoluteFilepathToOverrides).toBe(join(baseDir, "o.yml"));
});

it("accepts absolute openapi, override, and overlay paths", async () => {
const specs = await workspace.callConvertSpecsOverrideToSpecs([
{
openapi: "/external/api.yml",
overrides: ["/external/o1.yml", "/external/o2.yml"],
overlays: "/external/overlay.yml"
}
]);

expect(specs).toHaveLength(1);
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("openapi");
assert(spec.type === "openapi");
expect(spec.absoluteFilepath).toBe("/external/api.yml");
expect(spec.absoluteFilepathToOverrides).toEqual(["/external/o1.yml", "/external/o2.yml"]);
expect(spec.absoluteFilepathToOverlays).toBe("/external/overlay.yml");
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isOpenAPIV2, isOpenAPIV3, OpenAPISpec } from "@fern-api/api-workspace-commons";
import { AbsoluteFilePath, join, relative } from "@fern-api/fs-utils";
import { AbsoluteFilePath, isAbsoluteFilePath, join, relativeOrOriginalPath } from "@fern-api/fs-utils";
import { Source as OpenApiIrSource } from "@fern-api/openapi-ir";
import { Document, getParseOptions } from "@fern-api/openapi-ir-parser";
import { TaskContext } from "@fern-api/task-context";
Expand Down Expand Up @@ -36,14 +36,14 @@ export class OpenAPILoader {
}): Promise<Document | undefined> {
try {
const contents = (await readFile(spec.absoluteFilepath)).toString();
let sourceRelativePath = relative(this.absoluteFilePath, spec.source.file);
if (spec.source.relativePathToDependency != null) {
sourceRelativePath = join(spec.source.relativePathToDependency, sourceRelativePath);
let sourceFilepath = relativeOrOriginalPath(this.absoluteFilePath, spec.source.file);
if (spec.source.relativePathToDependency != null && !isAbsoluteFilePath(sourceFilepath)) {
sourceFilepath = join(spec.source.relativePathToDependency, sourceFilepath);
}
const source =
spec.source.type === "protobuf"
? OpenApiIrSource.protobuf({ file: sourceRelativePath })
: OpenApiIrSource.openapi({ file: sourceRelativePath });
? OpenApiIrSource.protobuf({ file: sourceFilepath })
: OpenApiIrSource.openapi({ file: sourceFilepath });

if (contents.includes("openapi") || contents.includes("swagger")) {
try {
Expand Down
175 changes: 173 additions & 2 deletions packages/cli/workspace/loader/src/__test__/loadWorkspace.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { RawSchemas } from "@fern-api/fern-definition-schema";
import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils";
import { AbsoluteFilePath, join, RelativeFilePath, relativePathForDisplay } from "@fern-api/fs-utils";
import { WorkspaceLoaderFailureType } from "@fern-api/lazy-fern-workspace";
import { Logger } from "@fern-api/logger";
import { createMockTaskContext } from "@fern-api/task-context";
import assert from "assert";
import { mkdtemp, writeFile } from "fs/promises";
import { tmpdir } from "os";
import { join as pathJoin } from "path";

import { handleFailedWorkspaceParserResult } from "../handleFailedWorkspaceParserResult.js";
import { loadAPIWorkspace } from "../loadAPIWorkspace.js";
import { loadAPIWorkspace, loadSingleNamespaceAPIWorkspace } from "../loadAPIWorkspace.js";

function createCapturingLogger(): Logger & { errors: string[]; debugs: string[] } {
const errors: string[] = [];
Expand Down Expand Up @@ -71,6 +74,174 @@ describe("loadWorkspace", () => {
expect(workspace.didSucceed).toBe(true);
assert(workspace.didSucceed);
});

it("open api with absolute spec path", async () => {
const absolutePathToFixtures = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures"));
const absolutePathToOpenApi = join(absolutePathToFixtures, RelativeFilePath.of("openapi.yml"));

const specs = await loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace: absolutePathToFixtures,
namespace: undefined,
definitions: [
{
schema: {
type: "oss",
path: absolutePathToOpenApi
},
origin: undefined,
overrides: undefined,
overlays: undefined,
audiences: [],
settings: undefined
}
]
});

expect(Array.isArray(specs)).toBe(true);
assert(Array.isArray(specs));
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("openapi");
assert(spec.type === "openapi");
expect(spec.absoluteFilepath).toBe(absolutePathToOpenApi);
});

it("open api with absolute override and overlay paths", async () => {
const absolutePathToFixtures = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures"));
const absolutePathToOpenApi = join(absolutePathToFixtures, RelativeFilePath.of("openapi.yml"));

const specs = await loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace: absolutePathToFixtures,
namespace: undefined,
definitions: [
{
schema: {
type: "oss",
path: "openapi.yml"
},
origin: undefined,
overrides: absolutePathToOpenApi,
overlays: absolutePathToOpenApi,
audiences: [],
settings: undefined
}
]
});

expect(Array.isArray(specs)).toBe(true);
assert(Array.isArray(specs));
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("openapi");
assert(spec.type === "openapi");
expect(spec.absoluteFilepathToOverrides).toBe(absolutePathToOpenApi);
expect(spec.absoluteFilepathToOverlays).toBe(absolutePathToOpenApi);
});

it("open rpc with absolute spec path", async () => {
const absolutePathToFixtures = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures"));
const absolutePathToOpenRpc = join(absolutePathToFixtures, RelativeFilePath.of("openapi.yml"));

const specs = await loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace: absolutePathToFixtures,
namespace: undefined,
definitions: [
{
schema: {
type: "openrpc",
path: absolutePathToOpenRpc
},
origin: undefined,
overrides: undefined,
overlays: undefined,
audiences: [],
settings: undefined
}
]
});

expect(Array.isArray(specs)).toBe(true);
assert(Array.isArray(specs));
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("openrpc");
assert(spec.type === "openrpc");
expect(spec.absoluteFilepath).toBe(absolutePathToOpenRpc);
});

it("graphql with absolute schema and examples paths", async () => {
const absolutePathToFixtures = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures"));
const absolutePathToGraphQL = join(absolutePathToFixtures, RelativeFilePath.of("openapi.yml"));

const specs = await loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace: absolutePathToFixtures,
namespace: undefined,
definitions: [
{
schema: {
type: "graphql",
path: absolutePathToGraphQL,
examples: absolutePathToGraphQL
},
origin: undefined,
overrides: undefined,
overlays: undefined,
audiences: [],
settings: undefined
}
]
});

expect(Array.isArray(specs)).toBe(true);
assert(Array.isArray(specs));
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("graphql");
assert(spec.type === "graphql");
expect(spec.absoluteFilepath).toBe(absolutePathToGraphQL);
expect(spec.absoluteFilepathToExamples).toBe(absolutePathToGraphQL);
});

it("protobuf with absolute root and target paths", async () => {
const absolutePathToFixtures = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures"));
const absolutePathToProtobufRoot = AbsoluteFilePath.of(await mkdtemp(pathJoin(tmpdir(), "fern-proto-root-")));
const absolutePathToProtobufTarget = AbsoluteFilePath.of(pathJoin(absolutePathToProtobufRoot, "service.proto"));
await writeFile(absolutePathToProtobufTarget, 'syntax = "proto3";\n');

const specs = await loadSingleNamespaceAPIWorkspace({
absolutePathToWorkspace: absolutePathToFixtures,
namespace: undefined,
definitions: [
{
schema: {
type: "protobuf",
root: absolutePathToProtobufRoot,
target: absolutePathToProtobufTarget,
dependencies: [],
localGeneration: true,
fromOpenAPI: false
},
origin: undefined,
overrides: undefined,
overlays: undefined,
audiences: [],
settings: undefined
}
]
});

expect(Array.isArray(specs)).toBe(true);
assert(Array.isArray(specs));
const spec = specs[0];
assert(spec != null);
expect(spec.type).toBe("protobuf");
assert(spec.type === "protobuf");
expect(spec.absoluteFilepathToProtobufRoot).toBe(absolutePathToProtobufRoot);
expect(spec.absoluteFilepathToProtobufTarget).toBe(absolutePathToProtobufTarget);
expect(spec.relativeFilepathToProtobufRoot).toBe(
relativePathForDisplay(absolutePathToFixtures, absolutePathToProtobufRoot)
);
});
});

describe("loadWorkspace MISCONFIGURED_DIRECTORY", () => {
Expand Down
Loading
Loading