diff --git a/.pnp.cjs b/.pnp.cjs index 100f88939bf..796265bb8d2 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -4498,6 +4498,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@fern-api/mock", "workspace:packages/cli/mock"],\ ["@fern-api/openapi-ir-sdk", "workspace:packages/cli/openapi-ir-sdk"],\ ["@fern-api/openapi-ir-to-fern", "workspace:packages/cli/openapi-ir-to-fern"],\ + ["@fern-api/openapi-parser", "workspace:packages/cli/openapi-parser"],\ ["@fern-api/posthog-manager", "workspace:packages/cli/posthog-manager"],\ ["@fern-api/project-configuration", "workspace:packages/cli/config-management/project-configuration"],\ ["@fern-api/project-loader", "workspace:packages/cli/project-loader"],\ @@ -5863,7 +5864,6 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@fern-api/docs-config-sdk", "workspace:packages/docs-config-sdk"],\ ["@fern-api/fs-utils", "workspace:packages/commons/fs-utils"],\ ["@fern-api/generators-configuration", "workspace:packages/cli/config-management/generators-configuration"],\ - ["@fern-api/openapi-ir-sdk", "workspace:packages/cli/openapi-ir-sdk"],\ ["@fern-api/openapi-ir-to-fern", "workspace:packages/cli/openapi-ir-to-fern"],\ ["@fern-api/openapi-parser", "workspace:packages/cli/openapi-parser"],\ ["@fern-api/project-configuration", "workspace:packages/cli/config-management/project-configuration"],\ diff --git a/packages/cli/cli/package.json b/packages/cli/cli/package.json index 259997f62a1..6e0751ebec7 100644 --- a/packages/cli/cli/package.json +++ b/packages/cli/cli/package.json @@ -57,6 +57,7 @@ "@fern-api/mock": "workspace:*", "@fern-api/openapi-ir-sdk": "workspace:*", "@fern-api/openapi-ir-to-fern": "workspace:*", + "@fern-api/openapi-parser": "workspace:*", "@fern-api/posthog-manager": "workspace:*", "@fern-api/project-configuration": "workspace:*", "@fern-api/project-loader": "workspace:*", diff --git a/packages/cli/cli/src/cli.ts b/packages/cli/cli/src/cli.ts index f627526b2dc..d64bf35b604 100644 --- a/packages/cli/cli/src/cli.ts +++ b/packages/cli/cli/src/cli.ts @@ -730,11 +730,6 @@ function addWriteOverridesCommand(cli: Argv, cliContext: CliCo description: "When generating the initial overrides, also stub the models (in addition to the endpoints)", default: false - }), - yargs.option("existing-overrides", { - string: true, - description: - "The existing overrides file to add on to instead of writing a new one, we will default to the one specified in generators.yml." }) ], async (argv) => { @@ -747,7 +742,6 @@ function addWriteOverridesCommand(cli: Argv, cliContext: CliCo defaultToAllApiWorkspaces: true }), includeModels: !(argv.excludeModels as boolean), - overridesFilepath: argv.existingOverrides as string, cliContext }); } diff --git a/packages/cli/cli/src/commands/format/formatWorkspaces.ts b/packages/cli/cli/src/commands/format/formatWorkspaces.ts index f3769bac722..0093dcdf897 100644 --- a/packages/cli/cli/src/commands/format/formatWorkspaces.ts +++ b/packages/cli/cli/src/commands/format/formatWorkspaces.ts @@ -13,7 +13,7 @@ export async function formatWorkspaces({ }): Promise { await Promise.all( project.apiWorkspaces.map(async (workspace) => { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { return; } await cliContext.runTaskForWorkspace(workspace, async (context) => { diff --git a/packages/cli/cli/src/commands/generate-fdr/generateFdrApiDefinitionForWorkspaces.ts b/packages/cli/cli/src/commands/generate-fdr/generateFdrApiDefinitionForWorkspaces.ts index 4302b983484..33fb8226c61 100644 --- a/packages/cli/cli/src/commands/generate-fdr/generateFdrApiDefinitionForWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate-fdr/generateFdrApiDefinitionForWorkspaces.ts @@ -23,7 +23,7 @@ export async function generateFdrApiDefinitionForWorkspaces({ project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { const fernWorkspace = - workspace.type === "openapi" + workspace.type === "oss" ? await convertOpenApiWorkspaceToFernWorkspace(workspace, context) : workspace; diff --git a/packages/cli/cli/src/commands/generate-ir/generateIrForWorkspaces.ts b/packages/cli/cli/src/commands/generate-ir/generateIrForWorkspaces.ts index f17b92396f6..74569e45c42 100644 --- a/packages/cli/cli/src/commands/generate-ir/generateIrForWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate-ir/generateIrForWorkspaces.ts @@ -2,10 +2,10 @@ import { Audiences } from "@fern-api/config-management-commons"; import { AbsoluteFilePath, stringifyLargeObject } from "@fern-api/fs-utils"; import { GenerationLanguage } from "@fern-api/generators-configuration"; import { migrateIntermediateRepresentationThroughVersion } from "@fern-api/ir-migrations"; +import { serialization as IrSerialization } from "@fern-api/ir-sdk"; import { Project } from "@fern-api/project-loader"; import { TaskContext } from "@fern-api/task-context"; import { convertOpenApiWorkspaceToFernWorkspace, FernWorkspace } from "@fern-api/workspace-loader"; -import { serialization as IrSerialization } from "@fern-api/ir-sdk"; import { writeFile } from "fs/promises"; import path from "path"; import { CliContext } from "../../cli-context/CliContext"; @@ -32,7 +32,7 @@ export async function generateIrForWorkspaces({ project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { const fernWorkspace = - workspace.type === "openapi" + workspace.type === "oss" ? await convertOpenApiWorkspaceToFernWorkspace(workspace, context) : workspace; diff --git a/packages/cli/cli/src/commands/generate-openapi-ir/generateOpenAPIIrForWorkspaces.ts b/packages/cli/cli/src/commands/generate-openapi-ir/generateOpenAPIIrForWorkspaces.ts index 1454b53c4ab..2b9089f856c 100644 --- a/packages/cli/cli/src/commands/generate-openapi-ir/generateOpenAPIIrForWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate-openapi-ir/generateOpenAPIIrForWorkspaces.ts @@ -1,7 +1,7 @@ import { AbsoluteFilePath, stringifyLargeObject } from "@fern-api/fs-utils"; import { serialization } from "@fern-api/openapi-ir-sdk"; +import { parse } from "@fern-api/openapi-parser"; import { Project } from "@fern-api/project-loader"; -import { getOpenAPIIRFromOpenAPIWorkspace } from "@fern-api/workspace-loader"; import { writeFile } from "fs/promises"; import path from "path"; import { CliContext } from "../../cli-context/CliContext"; @@ -23,7 +23,10 @@ export async function generateOpenAPIIrForWorkspaces({ return; } - const openAPIIr = await getOpenAPIIRFromOpenAPIWorkspace(workspace, context); + const openAPIIr = await parse({ + workspace, + taskContext: context + }); const irOutputFilePath = path.resolve(irFilepath); const openApiIrJson = await serialization.OpenApiIntermediateRepresentation.jsonOrThrow(openAPIIr, { diff --git a/packages/cli/cli/src/commands/generate-overrides/writeOverridesForWorkspaces.ts b/packages/cli/cli/src/commands/generate-overrides/writeOverridesForWorkspaces.ts index f528bc48aa1..50d96a226a6 100644 --- a/packages/cli/cli/src/commands/generate-overrides/writeOverridesForWorkspaces.ts +++ b/packages/cli/cli/src/commands/generate-overrides/writeOverridesForWorkspaces.ts @@ -1,8 +1,9 @@ import { dirname, join, RelativeFilePath } from "@fern-api/fs-utils"; import { getEndpointLocation } from "@fern-api/openapi-ir-to-fern"; +import { parse } from "@fern-api/openapi-parser"; import { Project } from "@fern-api/project-loader"; import { TaskContext } from "@fern-api/task-context"; -import { getOpenAPIIRFromOpenAPIWorkspace, OpenAPIWorkspace } from "@fern-api/workspace-loader"; +import { OSSWorkspace } from "@fern-api/workspace-loader"; import { readFile, writeFile } from "fs/promises"; import yaml from "js-yaml"; import { CliContext } from "../../cli-context/CliContext"; @@ -10,24 +11,20 @@ import { CliContext } from "../../cli-context/CliContext"; export async function writeOverridesForWorkspaces({ project, includeModels, - overridesFilepath, cliContext }: { project: Project; includeModels: boolean; - overridesFilepath: string | undefined; cliContext: CliContext; }): Promise { await Promise.all( project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { await writeDefinitionForOpenAPIWorkspace({ workspace, context, - includeModels, - overridesFilepath: - overridesFilepath ?? workspace.generatorsConfiguration?.absolutePathToOpenAPIOverrides + includeModels }); } else { context.logger.warn("Skipping fern workspace definition generation"); @@ -55,66 +52,70 @@ async function readExistingOverrides(overridesFilepath: string, context: TaskCon async function writeDefinitionForOpenAPIWorkspace({ workspace, includeModels, - overridesFilepath, context }: { - workspace: OpenAPIWorkspace; + workspace: OSSWorkspace; includeModels: boolean; - overridesFilepath: string | undefined; context: TaskContext; }): Promise { - const openApiIr = await getOpenAPIIRFromOpenAPIWorkspace(workspace, context); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let existingOverrides: any = {}; - if (overridesFilepath !== undefined) { - existingOverrides = await readExistingOverrides(overridesFilepath, context); - } - - const paths: Record> = "path" in existingOverrides - ? (existingOverrides.path as Record>) - : {}; - for (const endpoint of openApiIr.endpoints) { - const endpointLocation = getEndpointLocation(endpoint); - if (!(endpoint.path in paths)) { - paths[endpoint.path] = {}; + for (const spec of workspace.specs) { + const ir = await parse({ + workspace: { + specs: [spec] + }, + taskContext: context + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let existingOverrides: any = {}; + if (spec.absoluteFilepathToOverrides !== undefined) { + existingOverrides = await readExistingOverrides(spec.absoluteFilepathToOverrides, context); } - const pathItem = paths[endpoint.path]; - if (pathItem != null && pathItem[endpoint.method] == null) { - const groupName = endpointLocation.file - .split("/") - .map((part) => part.replace(".yml", "")) - .filter((part) => part !== "__package__"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const sdkMethodNameExtensions: Record = {}; - if (groupName.length > 0) { - sdkMethodNameExtensions["x-fern-sdk-group-name"] = groupName; + + const paths: Record> = "path" in existingOverrides + ? (existingOverrides.path as Record>) + : {}; + for (const endpoint of ir.endpoints) { + const endpointLocation = getEndpointLocation(endpoint); + if (!(endpoint.path in paths)) { + paths[endpoint.path] = {}; + } + const pathItem = paths[endpoint.path]; + if (pathItem != null && pathItem[endpoint.method] == null) { + const groupName = endpointLocation.file + .split("/") + .map((part) => part.replace(".yml", "")) + .filter((part) => part !== "__package__"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sdkMethodNameExtensions: Record = {}; + if (groupName.length > 0) { + sdkMethodNameExtensions["x-fern-sdk-group-name"] = groupName; + } + sdkMethodNameExtensions["x-fern-sdk-method-name"] = endpointLocation.endpointId; + pathItem[endpoint.method.toLowerCase()] = sdkMethodNameExtensions; + } else if (existingOverrides == null) { + context.logger.warn(`Endpoint ${endpoint.path} ${endpoint.method} is defined multiple times`); } - sdkMethodNameExtensions["x-fern-sdk-method-name"] = endpointLocation.endpointId; - pathItem[endpoint.method.toLowerCase()] = sdkMethodNameExtensions; - } else if (existingOverrides == null) { - context.logger.warn(`Endpoint ${endpoint.path} ${endpoint.method} is defined multiple times`); } - } - const schemas: Record> = "path" in existingOverrides - ? (existingOverrides.path as Record>) - : {}; - if (includeModels) { - for (const [schemaId, schema] of Object.entries(openApiIr.schemas)) { - if (schemaId in schemas) { - continue; + const schemas: Record> = "path" in existingOverrides + ? (existingOverrides.path as Record>) + : {}; + if (includeModels) { + for (const [schemaId, schema] of Object.entries(ir.schemas)) { + if (schemaId in schemas) { + continue; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const typeNameOverride: Record = {}; + typeNameOverride["x-fern-type-name"] = + "nameOverride" in schema ? schema.nameOverride ?? schemaId : schemaId; + schemas[schemaId] = typeNameOverride; } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const typeNameOverride: Record = {}; - typeNameOverride["x-fern-type-name"] = - "nameOverride" in schema ? schema.nameOverride ?? schemaId : schemaId; - schemas[schemaId] = typeNameOverride; } - } - const components: Record> = { schemas }; + const components: Record> = { schemas }; - await writeFile( - join(dirname(workspace.absolutePathToOpenAPI), RelativeFilePath.of("openapi-overrides.yml")), - yaml.dump({ paths, components }) - ); + await writeFile( + join(dirname(spec.absoluteFilepath), RelativeFilePath.of("openapi-overrides.yml")), + yaml.dump({ paths, components }) + ); + } } diff --git a/packages/cli/cli/src/commands/register/registerWorkspacesV1.ts b/packages/cli/cli/src/commands/register/registerWorkspacesV1.ts index 0e0b0416a70..7caa11853e5 100644 --- a/packages/cli/cli/src/commands/register/registerWorkspacesV1.ts +++ b/packages/cli/cli/src/commands/register/registerWorkspacesV1.ts @@ -35,7 +35,7 @@ export async function registerWorkspacesV1({ await Promise.all( project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { context.failWithoutThrowing("Registering from OpenAPI not currently supported."); return; } diff --git a/packages/cli/cli/src/commands/register/registerWorkspacesV2.ts b/packages/cli/cli/src/commands/register/registerWorkspacesV2.ts index 31282d03668..268c13b8b08 100644 --- a/packages/cli/cli/src/commands/register/registerWorkspacesV2.ts +++ b/packages/cli/cli/src/commands/register/registerWorkspacesV2.ts @@ -16,7 +16,7 @@ export async function registerWorkspacesV2({ await Promise.all( project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { context.failWithoutThrowing("Cannot register OpenAPI workspace"); } else { await registerApi({ diff --git a/packages/cli/cli/src/commands/validate/validateWorkspaces.ts b/packages/cli/cli/src/commands/validate/validateWorkspaces.ts index 2604e704867..8f465ee858b 100644 --- a/packages/cli/cli/src/commands/validate/validateWorkspaces.ts +++ b/packages/cli/cli/src/commands/validate/validateWorkspaces.ts @@ -23,7 +23,7 @@ export async function validateWorkspaces({ await Promise.all( project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { const fernWorkspace = await convertOpenApiWorkspaceToFernWorkspace(workspace, context); await validateAPIWorkspaceAndLogIssues({ workspace: fernWorkspace, context, logWarnings }); } else { diff --git a/packages/cli/cli/src/commands/write-definition/writeDefinitionForWorkspaces.ts b/packages/cli/cli/src/commands/write-definition/writeDefinitionForWorkspaces.ts index 96df597c1e1..870e866807f 100644 --- a/packages/cli/cli/src/commands/write-definition/writeDefinitionForWorkspaces.ts +++ b/packages/cli/cli/src/commands/write-definition/writeDefinitionForWorkspaces.ts @@ -6,7 +6,7 @@ import { convertOpenApiWorkspaceToFernWorkspace, FernDefinition, FernWorkspace, - OpenAPIWorkspace + OSSWorkspace } from "@fern-api/workspace-loader"; import chalk from "chalk"; import { mkdir, rmdir, writeFile } from "fs/promises"; @@ -24,7 +24,7 @@ export async function writeDefinitionForWorkspaces({ await Promise.all( project.apiWorkspaces.map(async (workspace) => { await cliContext.runTaskForWorkspace(workspace, async (context) => { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { await writeDefinitionForOpenAPIWorkspace({ workspace, context }); } else { await writeDefinitionForFernWorkspace({ workspace, context }); @@ -62,7 +62,7 @@ async function writeDefinitionForOpenAPIWorkspace({ workspace, context }: { - workspace: OpenAPIWorkspace; + workspace: OSSWorkspace; context: TaskContext; }): Promise { const fernWorkspace = await convertOpenApiWorkspaceToFernWorkspace(workspace, context); diff --git a/packages/cli/cli/tsconfig.json b/packages/cli/cli/tsconfig.json index e95662229bf..3ae3e193248 100644 --- a/packages/cli/cli/tsconfig.json +++ b/packages/cli/cli/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../mock" }, { "path": "../openapi-ir-sdk" }, { "path": "../openapi-ir-to-fern" }, + { "path": "../openapi-parser" }, { "path": "../posthog-manager" }, { "path": "../project-loader" }, { "path": "../register" }, diff --git a/packages/cli/config-management/generators-configuration/src/GeneratorsConfiguration.ts b/packages/cli/config-management/generators-configuration/src/GeneratorsConfiguration.ts index 2b8e2c11cfd..aa462d697cb 100644 --- a/packages/cli/config-management/generators-configuration/src/GeneratorsConfiguration.ts +++ b/packages/cli/config-management/generators-configuration/src/GeneratorsConfiguration.ts @@ -5,15 +5,25 @@ import { FernFiddle } from "@fern-fern/fiddle-sdk"; import { GeneratorsConfigurationSchema } from "./schemas/GeneratorsConfigurationSchema"; export interface GeneratorsConfiguration { - absolutePathToConfiguration: AbsoluteFilePath; - absolutePathToOpenAPI: AbsoluteFilePath | undefined; - absolutePathToOpenAPIOverrides: AbsoluteFilePath | undefined; - absolutePathToAsyncAPI: AbsoluteFilePath | undefined; - disableOpenAPIExamples: boolean | undefined; - rawConfiguration: GeneratorsConfigurationSchema; + api?: APIDefinition; defaultGroup: string | undefined; groups: GeneratorGroup[]; whitelabel: FernFiddle.WhitelabelConfig | undefined; + + rawConfiguration: GeneratorsConfigurationSchema; + absolutePathToConfiguration: AbsoluteFilePath; +} + +export type APIDefinition = SingleNamespaceAPIDefinition; + +export interface SingleNamespaceAPIDefinition { + type: "singleNamespace"; + definitions: APIDefinitionLocation[]; +} + +export interface APIDefinitionLocation { + path: string; + overrides: string | undefined; } export interface GeneratorGroup { diff --git a/packages/cli/config-management/generators-configuration/src/__test__/convertGeneratorsConfiguration.test.ts b/packages/cli/config-management/generators-configuration/src/__test__/convertGeneratorsConfiguration.test.ts index f3e7076dd1f..67f2a46b27a 100644 --- a/packages/cli/config-management/generators-configuration/src/__test__/convertGeneratorsConfiguration.test.ts +++ b/packages/cli/config-management/generators-configuration/src/__test__/convertGeneratorsConfiguration.test.ts @@ -123,31 +123,4 @@ describe("convertGeneratorsConfiguration", () => { expect(converted.groups[0]?.generators[0]?.outputMode?.type).toEqual("githubV2"); }); - - it("OpenAPI legacy", async () => { - const converted = await convertGeneratorsConfiguration({ - absolutePathToGeneratorsConfiguration: AbsoluteFilePath.of(__filename), - rawGeneratorsConfiguration: { - openapi: "path/to/openapi.yml", - ["openapi-overrides"]: "path/to/overrides.yml" - } - }); - - expect(converted.disableOpenAPIExamples).toEqual(undefined); - }); - - it("OpenAPI object", async () => { - const converted = await convertGeneratorsConfiguration({ - absolutePathToGeneratorsConfiguration: AbsoluteFilePath.of(__filename), - rawGeneratorsConfiguration: { - openapi: { - path: "path/to/openapi.yml", - overrides: "path/to/overrides.yml", - ["disable-examples"]: true - } - } - }); - - expect(converted.disableOpenAPIExamples).toEqual(true); - }); }); diff --git a/packages/cli/config-management/generators-configuration/src/convertGeneratorsConfiguration.ts b/packages/cli/config-management/generators-configuration/src/convertGeneratorsConfiguration.ts index 7bccbb13ad5..fe3d934be91 100644 --- a/packages/cli/config-management/generators-configuration/src/convertGeneratorsConfiguration.ts +++ b/packages/cli/config-management/generators-configuration/src/convertGeneratorsConfiguration.ts @@ -4,6 +4,8 @@ import { FernFiddle } from "@fern-fern/fiddle-sdk"; import { readFile } from "fs/promises"; import path from "path"; import { + APIDefinition, + APIDefinitionLocation, GenerationLanguage, GeneratorGroup, GeneratorInvocation, @@ -18,7 +20,6 @@ import { OPENAPI_LOCATION_KEY, OPENAPI_OVERRIDES_LOCATION_KEY } from "./schemas/GeneratorsConfigurationSchema"; -import { OPENAPI_DISABLE_EXAMPLES_KEY } from "./schemas/GeneratorsOpenAPIObjectSchema"; import { GithubLicenseSchema } from "./schemas/GithubLicenseSchema"; export async function convertGeneratorsConfiguration({ @@ -28,23 +29,9 @@ export async function convertGeneratorsConfiguration({ absolutePathToGeneratorsConfiguration: AbsoluteFilePath; rawGeneratorsConfiguration: GeneratorsConfigurationSchema; }): Promise { - const openAPI = await parseOpenAPIConfiguration(rawGeneratorsConfiguration); - const pathToAsyncAPI = rawGeneratorsConfiguration[ASYNC_API_LOCATION_KEY]; return { absolutePathToConfiguration: absolutePathToGeneratorsConfiguration, - absolutePathToAsyncAPI: - pathToAsyncAPI != null - ? join(dirname(absolutePathToGeneratorsConfiguration), RelativeFilePath.of(pathToAsyncAPI)) - : undefined, - absolutePathToOpenAPI: - openAPI.path != null - ? join(dirname(absolutePathToGeneratorsConfiguration), RelativeFilePath.of(openAPI.path)) - : undefined, - absolutePathToOpenAPIOverrides: - openAPI.overrides != null - ? join(dirname(absolutePathToGeneratorsConfiguration), RelativeFilePath.of(openAPI.overrides)) - : undefined, - disableOpenAPIExamples: openAPI.disableExamples, + api: await parseAPIConfiguration(rawGeneratorsConfiguration), rawConfiguration: rawGeneratorsConfiguration, defaultGroup: rawGeneratorsConfiguration["default-group"], groups: @@ -68,27 +55,65 @@ export async function convertGeneratorsConfiguration({ }; } -interface OpenAPIConfiguration { - path: string | undefined; - overrides: string | undefined; - disableExamples: boolean | undefined; -} - -async function parseOpenAPIConfiguration( +async function parseAPIConfiguration( rawGeneratorsConfiguration: GeneratorsConfigurationSchema -): Promise { - const openAPI = rawGeneratorsConfiguration[OPENAPI_LOCATION_KEY]; - if (typeof openAPI === "string") { - return { - path: openAPI, - overrides: rawGeneratorsConfiguration[OPENAPI_OVERRIDES_LOCATION_KEY], - disableExamples: undefined - }; +): Promise { + const apiConfiguration = rawGeneratorsConfiguration.api; + const apiDefinitions: APIDefinitionLocation[] = []; + if (apiConfiguration != null) { + if (typeof apiConfiguration === "string") { + apiDefinitions.push({ + path: apiConfiguration, + overrides: undefined + }); + } else if (Array.isArray(apiConfiguration)) { + for (const definition of apiConfiguration) { + if (typeof definition === "string") { + apiDefinitions.push({ + path: definition, + overrides: undefined + }); + } else { + apiDefinitions.push({ + path: definition.path, + overrides: definition.overrides + }); + } + } + } else { + apiDefinitions.push({ + path: apiConfiguration.path, + overrides: apiConfiguration.overrides + }); + } + } else { + const openapi = rawGeneratorsConfiguration[OPENAPI_LOCATION_KEY]; + const openapiOverrides = rawGeneratorsConfiguration[OPENAPI_OVERRIDES_LOCATION_KEY]; + const asyncapi = rawGeneratorsConfiguration[ASYNC_API_LOCATION_KEY]; + + if (openapi != null && typeof openapi === "string") { + apiDefinitions.push({ + path: openapi, + overrides: openapiOverrides + }); + } else if (openapi != null) { + apiDefinitions.push({ + path: openapi.path, + overrides: openapi.overrides + }); + } + + if (asyncapi != null) { + apiDefinitions.push({ + path: asyncapi, + overrides: undefined + }); + } } + return { - path: openAPI?.path, - overrides: openAPI?.overrides ?? rawGeneratorsConfiguration[OPENAPI_OVERRIDES_LOCATION_KEY], - disableExamples: openAPI?.[OPENAPI_DISABLE_EXAMPLES_KEY] + type: "singleNamespace", + definitions: apiDefinitions }; } diff --git a/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsConfigurationSchema.ts b/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsConfigurationSchema.ts index 6dec9c63860..5fe1b837a6b 100644 --- a/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsConfigurationSchema.ts +++ b/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsConfigurationSchema.ts @@ -3,18 +3,64 @@ import { GeneratorGroupSchema } from "./GeneratorGroupSchema"; import { GeneratorsOpenAPISchema } from "./GeneratorsOpenAPISchema"; import { WhitelabelConfigurationSchema } from "./WhitelabelConfigurationSchema"; +/** + * @example + * api: openapi.yml + * + * @example + * api: asyncapi.yml + */ +export const APIDefinitionPathSchema = z.string().describe("Path to the OpenAPI, AsyncAPI or Fern Definition"); + +/** + * @example + * api: + * path: openapi.yml + * overrides: overrides.yml + * + * @example + * api: + * path: asyncapi.yml + * overrides: overrides.yml + */ +export const APIDefintionWithOverridesSchema = z.object({ + path: APIDefinitionPathSchema, + overrides: z.optional(z.string()).describe("Path to the OpenAPI or AsyncAPI overrides") +}); + +/** + * @example + * api: + * - path: openapi.yml + * overrides: overrides.yml + * - openapi.yml + */ +export const APIDefinitionList = z.array(z.union([APIDefinitionPathSchema, APIDefintionWithOverridesSchema])); + +// TODO: Introduce merging configuration with namespaces +export const APIDefinitionSchema = z.union([ + APIDefinitionPathSchema, + APIDefintionWithOverridesSchema, + APIDefinitionList +]); + export const DEFAULT_GROUP_GENERATORS_CONFIG_KEY = "default-group"; export const OPENAPI_LOCATION_KEY = "openapi"; export const OPENAPI_OVERRIDES_LOCATION_KEY = "openapi-overrides"; export const ASYNC_API_LOCATION_KEY = "async-api"; export const GeneratorsConfigurationSchema = z.strictObject({ + api: z.optional(APIDefinitionSchema), + + whitelabel: z.optional(WhitelabelConfigurationSchema), + [DEFAULT_GROUP_GENERATORS_CONFIG_KEY]: z.optional(z.string()), + groups: z.optional(z.record(GeneratorGroupSchema)), + + // deprecated, use the `api` key instead [OPENAPI_LOCATION_KEY]: z.optional(GeneratorsOpenAPISchema), - [OPENAPI_OVERRIDES_LOCATION_KEY]: z.optional(z.string()).describe("Deprecated; use openapi.overrides instead."), - [ASYNC_API_LOCATION_KEY]: z.optional(z.string()), - whitelabel: z.optional(WhitelabelConfigurationSchema), - groups: z.optional(z.record(GeneratorGroupSchema)) + [OPENAPI_OVERRIDES_LOCATION_KEY]: z.optional(z.string()), + [ASYNC_API_LOCATION_KEY]: z.optional(z.string()) }); export type GeneratorsConfigurationSchema = z.infer; diff --git a/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsOpenAPIObjectSchema.ts b/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsOpenAPIObjectSchema.ts index db4c1380f31..56379d9fd3b 100644 --- a/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsOpenAPIObjectSchema.ts +++ b/packages/cli/config-management/generators-configuration/src/schemas/GeneratorsOpenAPIObjectSchema.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const OPENAPI_DISABLE_EXAMPLES_KEY = "disable-examples"; export const GeneratorsOpenAPIObjectSchema = z.strictObject({ - path: z.optional(z.string()), + path: z.string(), overrides: z.optional(z.string()), [OPENAPI_DISABLE_EXAMPLES_KEY]: z.optional(z.boolean()) }); diff --git a/packages/cli/docs-preview/src/previewDocs.ts b/packages/cli/docs-preview/src/previewDocs.ts index 37faf3737d2..132d863003c 100644 --- a/packages/cli/docs-preview/src/previewDocs.ts +++ b/packages/cli/docs-preview/src/previewDocs.ts @@ -91,7 +91,7 @@ class ReferencedAPICollector { continue; } const fernWorkspace = - workspace.type === "openapi" + workspace.type === "oss" ? await convertOpenApiWorkspaceToFernWorkspace(workspace, this.context) : workspace; const ir = await generateIntermediateRepresentation({ diff --git a/packages/cli/generation/ir-generator/src/__test__/generateIntermediateRepresentation.test.ts b/packages/cli/generation/ir-generator/src/__test__/generateIntermediateRepresentation.test.ts index ba9754472e2..5c3e7bd72b3 100644 --- a/packages/cli/generation/ir-generator/src/__test__/generateIntermediateRepresentation.test.ts +++ b/packages/cli/generation/ir-generator/src/__test__/generateIntermediateRepresentation.test.ts @@ -1,9 +1,9 @@ import { Audiences } from "@fern-api/config-management-commons"; import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/fs-utils"; +import { serialization as IrSerialization } from "@fern-api/ir-sdk"; import { loadApis } from "@fern-api/project-loader"; import { createMockTaskContext } from "@fern-api/task-context"; import { APIWorkspace, loadAPIWorkspace } from "@fern-api/workspace-loader"; -import { serialization as IrSerialization } from "@fern-api/ir-sdk"; import path from "path"; // import * as prettier from "prettier"; import { generateIntermediateRepresentation } from "../generateIntermediateRepresentation"; @@ -51,7 +51,7 @@ it("generate IR", async () => { } for (const workspace of apiWorkspaces) { - if (workspace.type === "openapi") { + if (workspace.type === "oss") { throw new Error("Convert OpenAPI to Fern workspace before generating IR"); } diff --git a/packages/cli/generation/ir-generator/src/resolvers/__test__/type-resolver/TypeResolver.test.ts b/packages/cli/generation/ir-generator/src/resolvers/__test__/type-resolver/TypeResolver.test.ts index a56c60fbaaf..2c8eac9a293 100644 --- a/packages/cli/generation/ir-generator/src/resolvers/__test__/type-resolver/TypeResolver.test.ts +++ b/packages/cli/generation/ir-generator/src/resolvers/__test__/type-resolver/TypeResolver.test.ts @@ -19,7 +19,7 @@ describe("TypeResolver", () => { if (!parseResult.didSucceed) { throw new Error("Failed to parse workspace: " + JSON.stringify(parseResult)); } - if (parseResult.workspace.type === "openapi") { + if (parseResult.workspace.type === "oss") { throw new Error("Expected fern workspace, but received openapi"); } diff --git a/packages/cli/generation/ir-migrations/src/__test__/utils/getIrForApi.ts b/packages/cli/generation/ir-migrations/src/__test__/utils/getIrForApi.ts index de5b3e4bab5..db9bdce24af 100644 --- a/packages/cli/generation/ir-migrations/src/__test__/utils/getIrForApi.ts +++ b/packages/cli/generation/ir-migrations/src/__test__/utils/getIrForApi.ts @@ -14,7 +14,7 @@ export async function getIrForApi(absolutePathToWorkspace: AbsoluteFilePath): Pr }); if (!workspace.didSucceed) { return context.failAndThrow("Failed to load workspace", workspace.failures); - } else if (workspace.workspace.type === "openapi") { + } else if (workspace.workspace.type === "oss") { return context.failAndThrow("Expected fern workspace but received openapi."); } return generateIntermediateRepresentation({ diff --git a/packages/cli/openapi-ir-to-fern/src/__test__/testConvertOpenApi.ts b/packages/cli/openapi-ir-to-fern/src/__test__/testConvertOpenApi.ts index a43e6d663f3..c91f853dc1d 100644 --- a/packages/cli/openapi-ir-to-fern/src/__test__/testConvertOpenApi.ts +++ b/packages/cli/openapi-ir-to-fern/src/__test__/testConvertOpenApi.ts @@ -20,12 +20,23 @@ export function testConvertOpenAPI(fixtureName: string, filename: string, asyncA ? join(FIXTURES_PATH, RelativeFilePath.of(fixtureName), RelativeFilePath.of(asyncApiFilename)) : undefined; + const specs = []; + specs.push({ + absoluteFilepath: AbsoluteFilePath.of(openApiPath), + absoluteFilepathToOverrides: undefined + }); + if (absolutePathToAsyncAPI != null) { + specs.push({ + absoluteFilepath: absolutePathToAsyncAPI, + absoluteFilepathToOverrides: undefined + }); + } + const openApiIr = await parse({ - absolutePathToOpenAPI: AbsoluteFilePath.of(openApiPath), - absolutePathToAsyncAPI, - absolutePathToOpenAPIOverrides: undefined, - taskContext: mockTaskContext, - disableExamples: undefined + workspace: { + specs + }, + taskContext: createMockTaskContext({ logger: CONSOLE_LOGGER }) }); const fernDefinition = convert({ openApiIr, taskContext: mockTaskContext }); expect(fernDefinition).toMatchSnapshot(); diff --git a/packages/cli/openapi-parser/src/__test__/__snapshots__/x-fern-overrides-file.test.ts.snap b/packages/cli/openapi-parser/src/__test__/__snapshots__/x-fern-overrides-file.test.ts.snap deleted file mode 100644 index 506ad05aa16..00000000000 --- a/packages/cli/openapi-parser/src/__test__/__snapshots__/x-fern-overrides-file.test.ts.snap +++ /dev/null @@ -1,202 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`x-fern-overrides-filepath x-fern-overrides-filepath parse open api 1`] = ` -{ - "channel": [], - "description": null, - "endpoints": [ - { - "audiences": [], - "authed": false, - "availability": null, - "description": "Retrieve a list of users from the system.", - "errorStatusCode": [ - 500, - ], - "examples": [ - { - "codeSamples": [], - "description": null, - "headers": [], - "name": null, - "pathParameters": [], - "queryParameters": [], - "request": null, - "response": null, - }, - ], - "generatedRequestName": "UsersGetRequest", - "headers": [], - "internal": null, - "method": "GET", - "operationId": null, - "path": "/users", - "pathParameters": [], - "queryParameters": [], - "request": null, - "requestNameOverride": null, - "response": null, - "sdkName": { - "groupName": [ - "users", - ], - "methodName": "get", - }, - "server": [], - "summary": "List users (Overriden description)", - "tags": [], - }, - ], - "errors": { - "500": { - "description": null, - "generatedName": "InternalServerError", - "nameOverride": null, - "schema": { - "generatedName": "InternalServerErrorBody", - "nameOverride": null, - "type": "unknown", - }, - }, - }, - "globalHeaders": [], - "hasEndpointsMarkedInternal": false, - "nonRequestReferencedSchemas": [], - "schemas": { - "User": { - "allOf": [], - "allOfPropertyConflicts": [], - "description": null, - "generatedName": "User", - "groupName": null, - "nameOverride": null, - "properties": [ - { - "audiences": [], - "conflict": {}, - "generatedName": "userId", - "key": "id", - "schema": { - "description": null, - "generatedName": "UserId", - "groupName": null, - "nameOverride": null, - "schema": { - "maxLength": null, - "minLength": null, - "type": "string", - }, - "type": "primitive", - }, - }, - { - "audiences": [], - "conflict": {}, - "generatedName": "userName", - "key": "name", - "schema": { - "description": null, - "generatedName": "userName", - "groupName": null, - "nameOverride": null, - "type": "optional", - "value": { - "description": null, - "generatedName": "UserName", - "groupName": null, - "nameOverride": null, - "schema": { - "maxLength": null, - "minLength": null, - "type": "string", - }, - "type": "primitive", - }, - }, - }, - { - "audiences": [], - "conflict": {}, - "generatedName": "userEmail", - "key": "email", - "schema": { - "description": null, - "generatedName": "userEmail", - "groupName": null, - "nameOverride": null, - "type": "optional", - "value": { - "description": null, - "generatedName": "UserEmail", - "groupName": null, - "nameOverride": null, - "schema": { - "maxLength": null, - "minLength": null, - "type": "string", - }, - "type": "primitive", - }, - }, - }, - { - "audiences": [], - "conflict": {}, - "generatedName": "userRole", - "key": "role", - "schema": { - "description": null, - "generatedName": "userRole", - "groupName": null, - "nameOverride": null, - "type": "optional", - "value": { - "description": null, - "generatedName": "UserRole", - "groupName": null, - "nameOverride": null, - "type": "enum", - "values": [ - { - "casing": { - "camel": null, - "pascal": null, - "screamingSnake": null, - "snake": null, - }, - "description": null, - "generatedName": "admin", - "nameOverride": null, - "value": "admin", - }, - { - "casing": { - "camel": null, - "pascal": null, - "screamingSnake": null, - "snake": null, - }, - "description": null, - "generatedName": "user", - "nameOverride": null, - "value": "user", - }, - ], - }, - }, - }, - ], - "type": "object", - }, - }, - "securitySchemes": {}, - "servers": [], - "tags": { - "orderedTagIds": null, - "tagsById": {}, - }, - "title": "User API", - "variables": {}, - "webhooks": [], -} -`; diff --git a/packages/cli/openapi-parser/src/__test__/testParseOpenApi.ts b/packages/cli/openapi-parser/src/__test__/testParseOpenApi.ts index 8f844761252..5aec4197d81 100644 --- a/packages/cli/openapi-parser/src/__test__/testParseOpenApi.ts +++ b/packages/cli/openapi-parser/src/__test__/testParseOpenApi.ts @@ -20,13 +20,25 @@ export function testParseOpenAPI(fixtureName: string, openApiFilename: string, a asyncApiFilename != null ? join(FIXTURES_PATH, RelativeFilePath.of(fixtureName), RelativeFilePath.of(asyncApiFilename)) : undefined; + const specs = []; + if (absolutePathToOpenAPI != null) { + specs.push({ + absoluteFilepath: absolutePathToOpenAPI, + absoluteFilepathToOverrides: undefined + }); + } + if (absolutePathToAsyncAPI != null) { + specs.push({ + absoluteFilepath: absolutePathToAsyncAPI, + absoluteFilepathToOverrides: undefined + }); + } const openApiIr = await parse({ - absolutePathToAsyncAPI, - absolutePathToOpenAPI, - absolutePathToOpenAPIOverrides: undefined, - taskContext: createMockTaskContext({ logger: CONSOLE_LOGGER }), - disableExamples: undefined + workspace: { + specs + }, + taskContext: createMockTaskContext({ logger: CONSOLE_LOGGER }) }); const openApiIrJson = await serialization.OpenApiIntermediateRepresentation.jsonOrThrow(openApiIr, { skipValidation: true diff --git a/packages/cli/openapi-parser/src/__test__/x-fern-overrides-file.test.ts b/packages/cli/openapi-parser/src/__test__/x-fern-overrides-file.test.ts deleted file mode 100644 index e20aa4967fd..00000000000 --- a/packages/cli/openapi-parser/src/__test__/x-fern-overrides-file.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { testParseOpenAPI } from "./testParseOpenApi"; - -describe("x-fern-overrides-filepath", () => { - testParseOpenAPI("x-fern-overrides-filepath", "openapi.yml"); -}); diff --git a/packages/cli/openapi-parser/src/loadOpenAPI.ts b/packages/cli/openapi-parser/src/loadOpenAPI.ts index 8b8b9f651e7..8fb76f6ea34 100644 --- a/packages/cli/openapi-parser/src/loadOpenAPI.ts +++ b/packages/cli/openapi-parser/src/loadOpenAPI.ts @@ -4,10 +4,8 @@ import { TaskContext } from "@fern-api/task-context"; import { bundle, Config } from "@redocly/openapi-core"; import { Plugin } from "@redocly/openapi-core/lib/config"; import { NodeType } from "@redocly/openapi-core/lib/types"; -import { readFile } from "fs/promises"; -import yaml from "js-yaml"; -import { mergeWith } from "lodash-es"; import { OpenAPI } from "openapi-types"; +import { mergeWithOverrides } from "./mergeWithOverrides"; import { FernOpenAPIExtension } from "./openapi/v3/extensions/fernExtensions"; const XFernStreaming: NodeType = { @@ -81,29 +79,11 @@ export async function loadOpenAPI({ } if (overridesFilepath != null) { - let parsedOverrides = null; - try { - const contents = (await readFile(overridesFilepath, "utf8")).toString(); - try { - parsedOverrides = JSON.parse(contents); - } catch (err) { - parsedOverrides = yaml.load(contents, { json: true }); - } - } catch (err) { - return context.failAndThrow(`Failed to read OpenAPI overrides from file ${overridesFilepath}`); - } - - const merged = mergeWith({}, parsed, parsedOverrides, (obj, src) => - Array.isArray(obj) && Array.isArray(src) - ? src.every((element) => typeof element === "object") && - obj.every((element) => typeof element === "object") - ? // nested arrays of objects are merged - undefined - : // nested arrays of primitives are replaced - [...src] - : undefined - ) as OpenAPI.Document; - return merged; + return await mergeWithOverrides({ + absoluteFilepathToOverrides: overridesFilepath, + context, + data: parsed + }); } return parsed; } diff --git a/packages/cli/openapi-parser/src/mergeWithOverrides.ts b/packages/cli/openapi-parser/src/mergeWithOverrides.ts new file mode 100644 index 00000000000..62cab93f9fd --- /dev/null +++ b/packages/cli/openapi-parser/src/mergeWithOverrides.ts @@ -0,0 +1,38 @@ +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { TaskContext } from "@fern-api/task-context"; +import { readFile } from "fs/promises"; +import yaml from "js-yaml"; +import { mergeWith } from "lodash-es"; + +export async function mergeWithOverrides({ + absoluteFilepathToOverrides, + data, + context +}: { + absoluteFilepathToOverrides: AbsoluteFilePath; + data: T; + context: TaskContext; +}): Promise { + let parsedOverrides = null; + try { + const contents = (await readFile(absoluteFilepathToOverrides, "utf8")).toString(); + try { + parsedOverrides = JSON.parse(contents); + } catch (err) { + parsedOverrides = yaml.load(contents, { json: true }); + } + } catch (err) { + return context.failAndThrow(`Failed to read overrides from file ${absoluteFilepathToOverrides}`); + } + + const merged = mergeWith({}, mergeWith, parsedOverrides, (obj, src) => + Array.isArray(obj) && Array.isArray(src) + ? src.every((element) => typeof element === "object") && obj.every((element) => typeof element === "object") + ? // nested arrays of objects are merged + undefined + : // nested arrays of primitives are replaced + [...src] + : undefined + ) as T; + return merged; +} diff --git a/packages/cli/openapi-parser/src/openapi/parse.ts b/packages/cli/openapi-parser/src/openapi/parse.ts index 67f91a6c1cf..fc1e3a57565 100644 --- a/packages/cli/openapi-parser/src/openapi/parse.ts +++ b/packages/cli/openapi-parser/src/openapi/parse.ts @@ -4,12 +4,18 @@ import { TaskContext } from "@fern-api/task-context"; import { readFile } from "fs/promises"; import yaml from "js-yaml"; import { OpenAPI, OpenAPIV2, OpenAPIV3 } from "openapi-types"; -import { AsyncAPIIntermediateRepresentation, parseAsyncAPI } from "../asyncapi/parse"; +import { parseAsyncAPI } from "../asyncapi/parse"; import { AsyncAPIV2 } from "../asyncapi/v2"; import { loadOpenAPI } from "../loadOpenAPI"; +import { mergeWithOverrides } from "../mergeWithOverrides"; import { generateIr as generateIrFromV2 } from "./v2/generateIr"; import { generateIr as generateIrFromV3 } from "./v3/generateIr"; +export interface Spec { + absoluteFilepath: AbsoluteFilePath; + absoluteFilepathToOverrides: AbsoluteFilePath | undefined; +} + export interface RawOpenAPIFile { absoluteFilepath: AbsoluteFilePath; contents: string; @@ -21,64 +27,144 @@ export interface RawAsyncAPIFile { } export async function parse({ - absolutePathToAsyncAPI, - absolutePathToOpenAPI, - absolutePathToOpenAPIOverrides, - disableExamples, + workspace, taskContext }: { - absolutePathToAsyncAPI: AbsoluteFilePath | undefined; - absolutePathToOpenAPI: AbsoluteFilePath; - absolutePathToOpenAPIOverrides: AbsoluteFilePath | undefined; - disableExamples: boolean | undefined; + workspace: { + specs: Spec[]; + }; taskContext: TaskContext; }): Promise { - let parsedAsyncAPI: AsyncAPIIntermediateRepresentation = { + let ir: OpenApiIntermediateRepresentation = { + title: undefined, + description: undefined, + servers: [], + tags: { + tagsById: {}, + orderedTagIds: undefined + }, + hasEndpointsMarkedInternal: false, + endpoints: [], + webhooks: [], + channel: [], schemas: {}, - channel: undefined + errors: {}, + variables: {}, + nonRequestReferencedSchemas: new Set(), + securitySchemes: {}, + globalHeaders: [] }; - if (absolutePathToAsyncAPI != null) { - const asyncAPI = await loadAsyncAPI(absolutePathToAsyncAPI); - parsedAsyncAPI = parseAsyncAPI({ document: asyncAPI, taskContext }); - } - - const openApiDocument = await loadOpenAPI({ - absolutePathToOpenAPI, - context: taskContext, - absolutePathToOpenAPIOverrides - }); - let openApiIr: OpenApiIntermediateRepresentation | undefined = undefined; - if (isOpenApiV3(openApiDocument)) { - openApiIr = generateIrFromV3({ - openApi: openApiDocument, - taskContext, - disableExamples - }); - } else if (isOpenApiV2(openApiDocument)) { - openApiIr = await generateIrFromV2({ - openApi: openApiDocument, - taskContext, - disableExamples - }); - } - if (openApiIr != null) { - return { - ...openApiIr, - channel: parsedAsyncAPI.channel != null ? [parsedAsyncAPI.channel] : [], - schemas: { - ...openApiIr.schemas, - ...parsedAsyncAPI.schemas + for (const spec of workspace.specs) { + const contents = (await readFile(spec.absoluteFilepath)).toString(); + if (contents.includes("openapi") || contents.includes("swagger")) { + const openApiDocument = await loadOpenAPI({ + absolutePathToOpenAPI: spec.absoluteFilepath, + context: taskContext, + absolutePathToOpenAPIOverrides: spec.absoluteFilepathToOverrides + }); + if (isOpenApiV3(openApiDocument)) { + const openapiIr = generateIrFromV3({ + openApi: openApiDocument, + taskContext, + disableExamples: false + }); + ir = merge(ir, openapiIr); + } else if (isOpenApiV2(openApiDocument)) { + const openapiIr = await generateIrFromV2({ + openApi: openApiDocument, + taskContext, + disableExamples: false + }); + ir = merge(ir, openapiIr); } - }; + // is openapi file + } else if (contents.includes("asyncapi")) { + const asyncAPI = await loadAsyncAPI({ + absoluteFilePathToAsyncAPI: spec.absoluteFilepath, + context: taskContext, + absoluteFilePathToAsyncAPIOverrides: spec.absoluteFilepathToOverrides + }); + const parsedAsyncAPI = parseAsyncAPI({ document: asyncAPI, taskContext }); + if (parsedAsyncAPI.channel != null) { + ir.channel.push(parsedAsyncAPI.channel); + } + if (parsedAsyncAPI.schemas != null) { + ir.schemas = { + ...ir.schemas, + ...parsedAsyncAPI.schemas + }; + } + } else { + taskContext.failAndThrow(`${spec.absoluteFilepath} is not a valid OpenAPI or AsyncAPI file`); + } } - return taskContext.failAndThrow("Only OpenAPI V3 and V2 Documents are supported."); + return ir; +} + +function merge( + ir1: OpenApiIntermediateRepresentation, + ir2: OpenApiIntermediateRepresentation +): OpenApiIntermediateRepresentation { + return { + title: ir1.title ?? ir2.title, + description: ir1.description ?? ir2.description, + servers: [...ir1.servers, ...ir2.servers], + tags: { + tagsById: { + ...ir1.tags.tagsById, + ...ir2.tags.tagsById + }, + orderedTagIds: + ir1.tags.orderedTagIds == null && ir2.tags.orderedTagIds == null + ? undefined + : [...(ir1.tags.orderedTagIds ?? []), ...(ir2.tags.orderedTagIds ?? [])] + }, + hasEndpointsMarkedInternal: ir1.hasEndpointsMarkedInternal || ir2.hasEndpointsMarkedInternal, + endpoints: [...ir1.endpoints, ...ir2.endpoints], + webhooks: [...ir1.webhooks, ...ir2.webhooks], + channel: [...ir1.channel, ...ir2.channel], + schemas: { + ...ir1.schemas, + ...ir2.schemas + }, + errors: { + ...ir1.errors, + ...ir2.errors + }, + variables: { + ...ir1.variables, + ...ir2.variables + }, + nonRequestReferencedSchemas: new Set([...ir1.nonRequestReferencedSchemas, ...ir2.nonRequestReferencedSchemas]), + securitySchemes: { + ...ir1.securitySchemes, + ...ir2.securitySchemes + }, + globalHeaders: ir1.globalHeaders != null ? [...ir1.globalHeaders, ...(ir2.globalHeaders ?? [])] : undefined + }; } -async function loadAsyncAPI(absoluteFilePathToAsyncAPI: AbsoluteFilePath): Promise { +async function loadAsyncAPI({ + absoluteFilePathToAsyncAPI, + absoluteFilePathToAsyncAPIOverrides, + context +}: { + absoluteFilePathToAsyncAPI: AbsoluteFilePath; + absoluteFilePathToAsyncAPIOverrides: AbsoluteFilePath | undefined; + context: TaskContext; +}): Promise { const contents = (await readFile(absoluteFilePathToAsyncAPI)).toString(); - return (await yaml.load(contents)) as AsyncAPIV2.Document; + const parsed = (await yaml.load(contents)) as AsyncAPIV2.Document; + if (absoluteFilePathToAsyncAPIOverrides != null) { + return await mergeWithOverrides({ + absoluteFilepathToOverrides: absoluteFilePathToAsyncAPIOverrides, + context, + data: parsed + }); + } + return parsed; } function isOpenApiV3(openApi: OpenAPI.Document): openApi is OpenAPIV3.Document { diff --git a/packages/cli/workspace-loader/package.json b/packages/cli/workspace-loader/package.json index 6e8be1c92e8..fe98575aaee 100644 --- a/packages/cli/workspace-loader/package.json +++ b/packages/cli/workspace-loader/package.json @@ -33,7 +33,6 @@ "@fern-api/docs-config-sdk": "workspace:*", "@fern-api/fs-utils": "workspace:*", "@fern-api/generators-configuration": "workspace:*", - "@fern-api/openapi-ir-sdk": "workspace:*", "@fern-api/openapi-ir-to-fern": "workspace:*", "@fern-api/openapi-parser": "workspace:*", "@fern-api/project-configuration": "workspace:*", diff --git a/packages/cli/workspace-loader/src/__test__/loadWorkspace.test.ts b/packages/cli/workspace-loader/src/__test__/loadWorkspace.test.ts index 8abeee91267..7262a7f8d31 100644 --- a/packages/cli/workspace-loader/src/__test__/loadWorkspace.test.ts +++ b/packages/cli/workspace-loader/src/__test__/loadWorkspace.test.ts @@ -31,6 +31,6 @@ describe("loadWorkspace", () => { }); expect(workspace.didSucceed).toBe(true); assert(workspace.didSucceed); - assert(workspace.workspace.type === "openapi"); + assert(workspace.workspace.type === "oss"); }); }); diff --git a/packages/cli/workspace-loader/src/index.ts b/packages/cli/workspace-loader/src/index.ts index 5d2f05dce16..21dec7e98fa 100644 --- a/packages/cli/workspace-loader/src/index.ts +++ b/packages/cli/workspace-loader/src/index.ts @@ -10,11 +10,8 @@ export { type FernDefinition, type FernWorkspace, type OnDiskNamedDefinitionFile, - type OpenAPIWorkspace, + type OSSWorkspace, type Workspace } from "./types/Workspace"; export * from "./utils"; -export { - convertOpenApiWorkspaceToFernWorkspace, - getOpenAPIIRFromOpenAPIWorkspace -} from "./utils/convertOpenApiWorkspaceToFernWorkspace"; +export { convertToFernWorkspace as convertOpenApiWorkspaceToFernWorkspace } from "./utils/convertOpenApiWorkspaceToFernWorkspace"; diff --git a/packages/cli/workspace-loader/src/loadAPIWorkspace.ts b/packages/cli/workspace-loader/src/loadAPIWorkspace.ts index d78a17842ee..7796c79e76c 100644 --- a/packages/cli/workspace-loader/src/loadAPIWorkspace.ts +++ b/packages/cli/workspace-loader/src/loadAPIWorkspace.ts @@ -5,12 +5,12 @@ import { ASYNCAPI_DIRECTORY, DEFINITION_DIRECTORY, OPENAPI_DIRECTORY } from "@fe import { TaskContext } from "@fern-api/task-context"; import { listFiles } from "./listFiles"; import { loadAPIChangelog } from "./loadAPIChangelog"; -import { getValidAbsolutePathToAsyncAPI, getValidAbsolutePathToAsyncAPIFromFolder } from "./loadAsyncAPIFile"; -import { getValidAbsolutePathToOpenAPI, getValidAbsolutePathToOpenAPIFromFolder } from "./loadOpenAPIFile"; +import { getValidAbsolutePathToAsyncAPIFromFolder } from "./loadAsyncAPIFile"; +import { getValidAbsolutePathToOpenAPIFromFolder } from "./loadOpenAPIFile"; import { parseYamlFiles } from "./parseYamlFiles"; import { processPackageMarkers } from "./processPackageMarkers"; import { WorkspaceLoader } from "./types/Result"; -import { APIChangelog } from "./types/Workspace"; +import { APIChangelog, FernWorkspace, Spec } from "./types/Workspace"; import { validateStructureOfYamlFiles } from "./validateStructureOfYamlFiles"; export async function loadAPIWorkspace({ @@ -40,29 +40,31 @@ export async function loadAPIWorkspace({ const absolutePathToAsyncAPIFolder = join(absolutePathToWorkspace, RelativeFilePath.of(ASYNCAPI_DIRECTORY)); const asyncApiDirectoryExists = await doesPathExist(absolutePathToAsyncAPIFolder); - if (generatorsConfiguration?.absolutePathToOpenAPI != null) { - const absolutePathToAsyncAPI = - generatorsConfiguration.absolutePathToAsyncAPI != null - ? await getValidAbsolutePathToAsyncAPI(context, generatorsConfiguration.absolutePathToAsyncAPI) - : undefined; - const absolutePathToOpenAPI = await getValidAbsolutePathToOpenAPI( - context, - generatorsConfiguration.absolutePathToOpenAPI - ); + if (generatorsConfiguration?.api != null && generatorsConfiguration.api.definitions.length > 0) { + const specs: Spec[] = []; + for (const definition of generatorsConfiguration.api.definitions) { + specs.push({ + absoluteFilepath: join(absolutePathToWorkspace, RelativeFilePath.of(definition.path)), + absoluteFilepathToOverrides: + definition.overrides != null + ? join(absolutePathToWorkspace, RelativeFilePath.of(definition.overrides)) + : undefined + }); + } return { didSucceed: true, workspace: { - type: "openapi", + type: "oss", name: "api", + specs, workspaceName, absoluteFilepath: absolutePathToWorkspace, generatorsConfiguration, - absolutePathToOpenAPI, - absolutePathToAsyncAPI, changelog } }; } + if (openApiDirectoryExists) { const absolutePathToAsyncAPI = asyncApiDirectoryExists ? await getValidAbsolutePathToAsyncAPIFromFolder(context, absolutePathToAsyncAPIFolder) @@ -71,16 +73,28 @@ export async function loadAPIWorkspace({ context, absolutePathToOpenAPIFolder ); + const specs: Spec[] = []; + if (absolutePathToOpenAPI != null) { + specs.push({ + absoluteFilepath: absolutePathToOpenAPI, + absoluteFilepathToOverrides: undefined + }); + } + if (absolutePathToAsyncAPI != null) { + specs.push({ + absoluteFilepath: absolutePathToAsyncAPI, + absoluteFilepathToOverrides: undefined + }); + } return { didSucceed: true, workspace: { - type: "openapi", + type: "oss", name: "api", + specs, workspaceName, absoluteFilepath: absolutePathToWorkspace, generatorsConfiguration, - absolutePathToOpenAPI, - absolutePathToAsyncAPI, changelog } }; @@ -114,23 +128,25 @@ export async function loadAPIWorkspace({ return processPackageMarkersResult; } + const fernWorkspace: FernWorkspace = { + type: "fern", + name: structuralValidationResult.rootApiFile.contents.name, + absoluteFilepath: absolutePathToWorkspace, + generatorsConfiguration, + dependenciesConfiguration, + workspaceName, + definition: { + absoluteFilepath: absolutePathToDefinition, + rootApiFile: structuralValidationResult.rootApiFile, + namedDefinitionFiles: structuralValidationResult.namedDefinitionFiles, + packageMarkers: processPackageMarkersResult.packageMarkers, + importedDefinitions: processPackageMarkersResult.importedDefinitions + }, + changelog + }; + return { didSucceed: true, - workspace: { - type: "fern", - name: structuralValidationResult.rootApiFile.contents.name, - absoluteFilepath: absolutePathToWorkspace, - generatorsConfiguration, - dependenciesConfiguration, - workspaceName, - definition: { - absoluteFilepath: absolutePathToDefinition, - rootApiFile: structuralValidationResult.rootApiFile, - namedDefinitionFiles: structuralValidationResult.namedDefinitionFiles, - packageMarkers: processPackageMarkersResult.packageMarkers, - importedDefinitions: processPackageMarkersResult.importedDefinitions - }, - changelog - } + workspace: fernWorkspace }; } diff --git a/packages/cli/workspace-loader/src/loadDependency.ts b/packages/cli/workspace-loader/src/loadDependency.ts index 82835e19f1f..ccfb6b5b59f 100644 --- a/packages/cli/workspace-loader/src/loadDependency.ts +++ b/packages/cli/workspace-loader/src/loadDependency.ts @@ -21,7 +21,7 @@ import tmp from "tmp-promise"; import { loadAPIWorkspace } from "./loadAPIWorkspace"; import { WorkspaceLoader, WorkspaceLoaderFailureType } from "./types/Result"; import { FernDefinition, FernWorkspace } from "./types/Workspace"; -import { convertOpenApiWorkspaceToFernWorkspace } from "./utils/convertOpenApiWorkspaceToFernWorkspace"; +import { convertToFernWorkspace } from "./utils/convertOpenApiWorkspaceToFernWorkspace"; const FIDDLE = createFiddleService(); @@ -123,7 +123,7 @@ async function validateLocalDependencyAndGetDefinition({ const workspaceOfDependency = loadDependencyWorkspaceResult.workspace.type === "fern" ? loadDependencyWorkspaceResult.workspace - : await convertOpenApiWorkspaceToFernWorkspace(loadDependencyWorkspaceResult.workspace, context); + : await convertToFernWorkspace(loadDependencyWorkspaceResult.workspace, context); return workspaceOfDependency.definition; } @@ -223,7 +223,7 @@ async function validateVersionedDependencyAndGetDefinition({ } const workspaceOfDependency = loadDependencyWorkspaceResult.workspace; - if (workspaceOfDependency.type === "openapi") { + if (workspaceOfDependency.type === "oss") { context.failWithoutThrowing("Dependency must be a fern workspace."); return undefined; } diff --git a/packages/cli/workspace-loader/src/types/Workspace.ts b/packages/cli/workspace-loader/src/types/Workspace.ts index f83786a7255..8990b2304ee 100644 --- a/packages/cli/workspace-loader/src/types/Workspace.ts +++ b/packages/cli/workspace-loader/src/types/Workspace.ts @@ -15,17 +15,24 @@ export interface DocsWorkspace { config: DocsConfiguration; } -export type APIWorkspace = FernWorkspace | OpenAPIWorkspace; +export type APIWorkspace = FernWorkspace | OSSWorkspace; -export interface OpenAPIWorkspace { - type: "openapi"; +/** + * An OSS workspace is a workspace that contains an OpenAPI or AsyncAPI document. + */ +export interface OSSWorkspace { + type: "oss"; + absoluteFilepath: AbsoluteFilePath; workspaceName: string | undefined; name: string; - absoluteFilepath: AbsoluteFilePath; - generatorsConfiguration: GeneratorsConfiguration | undefined; - absolutePathToOpenAPI: AbsoluteFilePath; - absolutePathToAsyncAPI: AbsoluteFilePath | undefined; + specs: Spec[]; changelog: APIChangelog | undefined; + generatorsConfiguration: GeneratorsConfiguration | undefined; +} + +export interface Spec { + absoluteFilepath: AbsoluteFilePath; + absoluteFilepathToOverrides: AbsoluteFilePath | undefined; } export interface APIChangelog { diff --git a/packages/cli/workspace-loader/src/utils/convertOpenApiWorkspaceToFernWorkspace.ts b/packages/cli/workspace-loader/src/utils/convertOpenApiWorkspaceToFernWorkspace.ts index a39840805d7..a29366809c7 100644 --- a/packages/cli/workspace-loader/src/utils/convertOpenApiWorkspaceToFernWorkspace.ts +++ b/packages/cli/workspace-loader/src/utils/convertOpenApiWorkspaceToFernWorkspace.ts @@ -1,31 +1,20 @@ import { AbsoluteFilePath, RelativeFilePath } from "@fern-api/fs-utils"; -import { OpenApiIntermediateRepresentation } from "@fern-api/openapi-ir-sdk"; import { convert } from "@fern-api/openapi-ir-to-fern"; import { parse } from "@fern-api/openapi-parser"; import { FERN_PACKAGE_MARKER_FILENAME } from "@fern-api/project-configuration"; import { TaskContext } from "@fern-api/task-context"; import yaml from "js-yaml"; import { mapValues as mapValuesLodash } from "lodash-es"; -import { FernWorkspace, OpenAPIWorkspace } from "../types/Workspace"; +import { FernWorkspace, OSSWorkspace } from "../types/Workspace"; -export async function getOpenAPIIRFromOpenAPIWorkspace( - openapiWorkspace: OpenAPIWorkspace, +export async function convertToFernWorkspace( + openapiWorkspace: OSSWorkspace, context: TaskContext -): Promise { - return await parse({ - absolutePathToAsyncAPI: openapiWorkspace.absolutePathToAsyncAPI, - absolutePathToOpenAPI: openapiWorkspace.absolutePathToOpenAPI, - absolutePathToOpenAPIOverrides: openapiWorkspace.generatorsConfiguration?.absolutePathToOpenAPIOverrides, - disableExamples: openapiWorkspace.generatorsConfiguration?.disableOpenAPIExamples, +): Promise { + const openApiIr = await parse({ + workspace: openapiWorkspace, taskContext: context }); -} - -export async function convertOpenApiWorkspaceToFernWorkspace( - openapiWorkspace: OpenAPIWorkspace, - context: TaskContext -): Promise { - const openApiIr = await getOpenAPIIRFromOpenAPIWorkspace(openapiWorkspace, context); const definition = convert({ taskContext: context, openApiIr diff --git a/packages/cli/workspace-loader/tsconfig.json b/packages/cli/workspace-loader/tsconfig.json index 23fd4445dfa..0b9bc8fd0be 100644 --- a/packages/cli/workspace-loader/tsconfig.json +++ b/packages/cli/workspace-loader/tsconfig.json @@ -10,7 +10,6 @@ { "path": "../config-management/dependencies-configuration" }, { "path": "../config-management/generators-configuration" }, { "path": "../config-management/project-configuration" }, - { "path": "../openapi-ir-sdk" }, { "path": "../openapi-ir-to-fern" }, { "path": "../openapi-parser" }, { "path": "../semver-utils" }, diff --git a/packages/cli/yaml/validator/src/__test__/validator/validateFernWorkspace.test.ts b/packages/cli/yaml/validator/src/__test__/validator/validateFernWorkspace.test.ts index 491e5671cba..8db96e01fe2 100644 --- a/packages/cli/yaml/validator/src/__test__/validator/validateFernWorkspace.test.ts +++ b/packages/cli/yaml/validator/src/__test__/validator/validateFernWorkspace.test.ts @@ -33,7 +33,7 @@ describe("validateFernWorkspace", () => { if (!parseResult.didSucceed) { throw new Error("Failed to parse workspace: " + JSON.stringify(parseResult)); } - if (parseResult.workspace.type === "openapi") { + if (parseResult.workspace.type === "oss") { throw new Error("Expected fern workspace, but received openapi"); } diff --git a/packages/cli/yaml/validator/src/testing-utils/getViolationsForRule.ts b/packages/cli/yaml/validator/src/testing-utils/getViolationsForRule.ts index 00db95a63b7..509a5443553 100644 --- a/packages/cli/yaml/validator/src/testing-utils/getViolationsForRule.ts +++ b/packages/cli/yaml/validator/src/testing-utils/getViolationsForRule.ts @@ -28,7 +28,7 @@ export async function getViolationsForRule({ throw new Error("Failed to parse workspace: " + JSON.stringify(parseResult)); } - if (parseResult.workspace.type === "openapi") { + if (parseResult.workspace.type === "oss") { throw new Error("Expected fern workspace, but received openapi"); } diff --git a/yarn.lock b/yarn.lock index 3cca6ed2ae7..5a8a11e1e6b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2353,6 +2353,7 @@ __metadata: "@fern-api/mock": "workspace:*" "@fern-api/openapi-ir-sdk": "workspace:*" "@fern-api/openapi-ir-to-fern": "workspace:*" + "@fern-api/openapi-parser": "workspace:*" "@fern-api/posthog-manager": "workspace:*" "@fern-api/project-configuration": "workspace:*" "@fern-api/project-loader": "workspace:*" @@ -3636,7 +3637,6 @@ __metadata: "@fern-api/docs-config-sdk": "workspace:*" "@fern-api/fs-utils": "workspace:*" "@fern-api/generators-configuration": "workspace:*" - "@fern-api/openapi-ir-sdk": "workspace:*" "@fern-api/openapi-ir-to-fern": "workspace:*" "@fern-api/openapi-parser": "workspace:*" "@fern-api/project-configuration": "workspace:*"