Skip to content

Commit

Permalink
(feat, cli): support customizing api settings per generator (#3913)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsinghvi authored Jun 24, 2024
1 parent f0875f8 commit c49432a
Show file tree
Hide file tree
Showing 15 changed files with 133 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export async function generateDocsWorkspace({

const fernWorkspaces = await Promise.all(
project.apiWorkspaces.map(async (workspace) => {
return workspace.toFernWorkspace({ context });
return workspace.toFernWorkspace({ context }, { enableUniqueErrorsPerEndpoint: true });
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FernFiddle } from "@fern-fern/fiddle-sdk";
import { Audiences } from "../commons";
import { GeneratorsConfigurationSchema } from "./schemas/GeneratorsConfigurationSchema";
import { ReadmeSchema } from "./schemas/ReadmeSchema";
import { APIDefinitionSettingsSchema } from "./schemas/APIConfigurationSchema";

export interface GeneratorsConfiguration {
api?: APIDefinition;
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface GeneratorInvocation {
language: GenerationLanguage | undefined;
publishMetadata: FernFiddle.remoteGen.PublishingMetadata | undefined;
readme: ReadmeSchema | undefined;
settings: APIDefinitionSettingsSchema | undefined;
}

export const GenerationLanguage = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,8 @@ async function convertGenerator({
language: getLanguageFromGeneratorName(generator.name),
irVersionOverride: generator["ir-version"] ?? undefined,
publishMetadata: getPublishMetadata({ generatorInvocation: generator }),
readme
readme,
settings: generator.api?.settings ?? undefined
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const APIDefinitionSettingsSchema = z.object({
.describe("What version of union generation to use, this will grow over time. Defaults to v0.")
});

export type APIDefinitionSettingsSchema = z.infer<typeof APIDefinitionSettingsSchema>;

/**
* @example
* api:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { z } from "zod";
import { APIDefinitionSettingsSchema } from "./APIConfigurationSchema";
import { GeneratorOutputSchema } from "./GeneratorOutputSchema";
import { GeneratorPublishMetadataSchema } from "./GeneratorPublishMetadataSchema";
import { GeneratorSnippetsSchema } from "./GeneratorSnippetsSchema";
Expand All @@ -19,6 +20,8 @@ export const GeneratorInvocationSchema = z.strictObject({
"ir-version": z.optional(z.string()),
// Feature flag used to enable better IR naming.
"smart-casing": z.optional(z.boolean()),
// Override API import settings (this is applied across all specs)
api: z.optional(z.object({ settings: z.optional(APIDefinitionSettingsSchema) })),
// Temporary way to unblock example serialization.
"disable-examples": z.optional(z.boolean()),
// Deprecated, use `metadata` on the output block instead.
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/docs-preview/src/previewDocs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export async function getPreviewDocsDefinition({
}

const fernWorkspaces = await Promise.all(
apiWorkspaces.map(async (workspace) => await workspace.toFernWorkspace({ context }))
apiWorkspaces.map(
async (workspace) => await workspace.toFernWorkspace({ context }, { enableUniqueErrorsPerEndpoint: true })
)
);

const apiCollector = new ReferencedAPICollector();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { fernConfigJson, generatorsYml } from "@fern-api/configuration";
import { TaskContext } from "@fern-api/task-context";
import { APIWorkspace } from "@fern-api/workspace-loader";
import {
APIWorkspace,
FernWorkspace,
getOSSWorkspaceSettingsFromGeneratorInvocation,
OSSWorkspace
} from "@fern-api/workspace-loader";
import chalk from "chalk";
import os from "os";
import path from "path";
Expand Down Expand Up @@ -30,9 +35,15 @@ export async function runLocalGenerationForWorkspace({
"Cannot generate because output location is not local-file-system"
);
} else {
// TODO(dsinghvi): pass in feature flags here
// for union v2 generation and streaming v2 generation
const fernWorkspace = await workspace.toFernWorkspace({ context });
let fernWorkspace: FernWorkspace;
if (workspace instanceof OSSWorkspace) {
fernWorkspace = await workspace.toFernWorkspace(
{ context },
getOSSWorkspaceSettingsFromGeneratorInvocation(generatorInvocation)
);
} else {
fernWorkspace = workspace;
}

await writeFilesToDiskAndRunGenerator({
organization: projectConfig.organization,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { FernToken } from "@fern-api/auth";
import { fernConfigJson, generatorsYml } from "@fern-api/configuration";
import { AbsoluteFilePath } from "@fern-api/fs-utils";
import { TaskContext } from "@fern-api/task-context";
import { APIWorkspace } from "@fern-api/workspace-loader";
import {
APIWorkspace,
FernWorkspace,
getOSSWorkspaceSettingsFromGeneratorInvocation,
OSSWorkspace
} from "@fern-api/workspace-loader";
import { FernFiddle } from "@fern-fern/fiddle-sdk";
import { downloadSnippetsForTask } from "./downloadSnippetsForTask";
import { runRemoteGenerationForGenerator } from "./runRemoteGenerationForGenerator";
Expand Down Expand Up @@ -45,9 +50,15 @@ export async function runRemoteGenerationForAPIWorkspace({
interactiveTasks.push(
...generatorGroup.generators.map((generatorInvocation) =>
context.runInteractiveTask({ name: generatorInvocation.name }, async (interactiveTaskContext) => {
// TODO(dsinghvi): pass in feature flags here
// for union v2 generation and streaming v2 generation
const fernWorkspace = await workspace.toFernWorkspace({ context });
let fernWorkspace: FernWorkspace;
if (workspace instanceof OSSWorkspace) {
fernWorkspace = await workspace.toFernWorkspace(
{ context },
getOSSWorkspaceSettingsFromGeneratorInvocation(generatorInvocation)
);
} else {
fernWorkspace = workspace;
}

const remoteTaskHandlerResponse = await runRemoteGenerationForGenerator({
projectConfig,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/openapi-parser/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { type ParseOpenAPIOptions } from "./options";
export { parse } from "./parse";
export { generateEnumNameFromValue, VALID_ENUM_NAME_REGEX } from "./schema/convertEnum";
24 changes: 18 additions & 6 deletions packages/cli/openapi-parser/src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ export interface RawAsyncAPIFile {

export async function parse({
specs,
taskContext
taskContext,
optionOverrides
}: {
specs: Spec[];
taskContext: TaskContext;
optionOverrides?: Partial<ParseOpenAPIOptions>;
}): Promise<OpenApiIntermediateRepresentation> {
let ir: OpenApiIntermediateRepresentation = {
title: undefined,
Expand Down Expand Up @@ -75,7 +77,7 @@ export async function parse({
const openapiIr = generateIrFromV3({
openApi: openApiDocument,
taskContext,
options: getParseOptions({ specSettings: spec.settings })
options: getParseOptions({ specSettings: spec.settings, overrides: optionOverrides })
});
ir = merge(ir, openapiIr);
} else if (isOpenApiV2(openApiDocument)) {
Expand Down Expand Up @@ -115,14 +117,24 @@ export async function parse({
return ir;
}

function getParseOptions({ specSettings }: { specSettings?: SpecImportSettings }): ParseOpenAPIOptions {
function getParseOptions({
specSettings,
overrides
}: {
specSettings?: SpecImportSettings;
overrides?: Partial<ParseOpenAPIOptions>;
}): ParseOpenAPIOptions {
return {
disableExamples: DEFAULT_PARSE_OPENAPI_SETTINGS.disableExamples,
disableExamples: overrides?.disableExamples ?? DEFAULT_PARSE_OPENAPI_SETTINGS.disableExamples,
discriminatedUnionV2:
overrides?.discriminatedUnionV2 ??
specSettings?.shouldUseUndiscriminatedUnionsWithLiterals ??
DEFAULT_PARSE_OPENAPI_SETTINGS.discriminatedUnionV2,
useTitlesAsName: specSettings?.shouldUseTitleAsName ?? DEFAULT_PARSE_OPENAPI_SETTINGS.useTitlesAsName,
audiences: specSettings?.audiences ?? DEFAULT_PARSE_OPENAPI_SETTINGS.audiences
useTitlesAsName:
overrides?.useTitlesAsName ??
specSettings?.shouldUseTitleAsName ??
DEFAULT_PARSE_OPENAPI_SETTINGS.useTitlesAsName,
audiences: overrides?.audiences ?? specSettings?.audiences ?? DEFAULT_PARSE_OPENAPI_SETTINGS.audiences
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { TaskContext } from "@fern-api/task-context";
import { FernDefinition } from "../types/Workspace";
import { FernWorkspace } from "./FernWorkspace";

export abstract class AbstractAPIWorkspace {
/**
* Represents an arbitrary API Definition within the Fern folder. Each API Definition
* will eventually have a set of canonical operations such as `validate`, `lint`, etc.
*
* Each API Definition will also be able to convert themselves into a `FernWorkspace`
* to be interoperable with the rest of the codebase.
*/
export abstract class AbstractAPIWorkspace<Settings> {
/**
* @returns The Fern Definition that corresponds to this workspace
*/
public abstract getDefinition({ context }: { context: TaskContext }): Promise<FernDefinition>;
public abstract getDefinition({ context }: { context: TaskContext }, settings?: Settings): Promise<FernDefinition>;

public abstract toFernWorkspace({ context }: { context: TaskContext }): Promise<FernWorkspace>;
public abstract toFernWorkspace({ context }: { context: TaskContext }, settings?: Settings): Promise<FernWorkspace>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export declare namespace FernWorkspace {
}
}

export class FernWorkspace extends AbstractAPIWorkspace {
export class FernWorkspace extends AbstractAPIWorkspace<void> {
public type: "fern" | "oss" = "fern";
public workspaceName: string | undefined;
public absoluteFilepath: AbsoluteFilePath;
Expand Down
63 changes: 56 additions & 7 deletions packages/cli/workspace-loader/src/workspaces/OSSWorkspace.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FERN_PACKAGE_MARKER_FILENAME, generatorsYml } from "@fern-api/configuration";
import { AbsoluteFilePath, RelativeFilePath } from "@fern-api/fs-utils";
import { convert } from "@fern-api/openapi-ir-to-fern";
import { parse } from "@fern-api/openapi-parser";
import { parse, ParseOpenAPIOptions } from "@fern-api/openapi-parser";
import { TaskContext } from "@fern-api/task-context";
import yaml from "js-yaml";
import { mapValues as mapValuesLodash } from "lodash-es";
Expand All @@ -17,9 +17,22 @@ export declare namespace OSSWorkspace {
changelog: APIChangelog | undefined;
generatorsConfiguration: generatorsYml.GeneratorsConfiguration | undefined;
}

export interface Settings {
/*
* Whether or not to parse unique errors for OpenAPI operation. This is
* an option that is typically enabled for docs generation.
*/
enableUniqueErrorsPerEndpoint?: boolean;
/*
* Whether or not to parse discriminated unions as undiscriminated unions with literals.
* Typically enabled for duck typed languages like Python / TypeScript.
*/
enableDiscriminatedUnionV2?: boolean;
}
}

export class OSSWorkspace extends AbstractAPIWorkspace {
export class OSSWorkspace extends AbstractAPIWorkspace<OSSWorkspace.Settings> {
public type: "fern" | "oss" = "oss";
public absoluteFilepath: AbsoluteFilePath;
public workspaceName: string | undefined;
Expand All @@ -36,16 +49,24 @@ export class OSSWorkspace extends AbstractAPIWorkspace {
this.generatorsConfiguration = generatorsConfiguration;
}

public async getDefinition({ context }: { context: TaskContext }): Promise<FernDefinition> {
public async getDefinition(
{
context
}: {
context: TaskContext;
},
settings?: OSSWorkspace.Settings
): Promise<FernDefinition> {
const openApiIr = await parse({
specs: this.specs,
taskContext: context
taskContext: context,
optionOverrides: getOptionsOverridesFromSettings(settings)
});

const definition = convert({
taskContext: context,
ir: openApiIr,
enableUniqueErrorsPerEndpoint: false // Come back to this
enableUniqueErrorsPerEndpoint: settings?.enableUniqueErrorsPerEndpoint ?? false
});

return {
Expand Down Expand Up @@ -74,8 +95,11 @@ export class OSSWorkspace extends AbstractAPIWorkspace {
};
}

public async toFernWorkspace({ context }: { context: TaskContext }): Promise<FernWorkspace> {
const definition = await this.getDefinition({ context });
public async toFernWorkspace(
{ context }: { context: TaskContext },
settings?: OSSWorkspace.Settings
): Promise<FernWorkspace> {
const definition = await this.getDefinition({ context }, settings);
return new FernWorkspace({
absoluteFilepath: this.absoluteFilepath,
workspaceName: this.workspaceName,
Expand All @@ -89,6 +113,31 @@ export class OSSWorkspace extends AbstractAPIWorkspace {
}
}

export function getOSSWorkspaceSettingsFromGeneratorInvocation(
generatorInvocation: generatorsYml.GeneratorInvocation
): OSSWorkspace.Settings | undefined {
if (generatorInvocation.settings == null) {
return undefined;
}
const result: OSSWorkspace.Settings = {};
if (generatorInvocation.settings.unions === "v1") {
result.enableDiscriminatedUnionV2 = true;
}

return result;
}

function getOptionsOverridesFromSettings(settings?: OSSWorkspace.Settings): Partial<ParseOpenAPIOptions> | undefined {
if (settings == null) {
return undefined;
}
const result: Partial<ParseOpenAPIOptions> = {};
if (settings.enableDiscriminatedUnionV2) {
result.discriminatedUnionV2 = true;
}
return result;
}

function mapValues<T extends object, U>(items: T, mapper: (item: T[keyof T]) => U): Record<keyof T, U> {
return mapValuesLodash(items, mapper) as Record<keyof T, U>;
}
2 changes: 1 addition & 1 deletion packages/cli/workspace-loader/src/workspaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { type APIWorkspace } from "./APIWorkspace";
export { FernWorkspace } from "./FernWorkspace";
export { OSSWorkspace } from "./OSSWorkspace";
export { getOSSWorkspaceSettingsFromGeneratorInvocation, OSSWorkspace } from "./OSSWorkspace";
6 changes: 3 additions & 3 deletions packages/seed/src/utils/getGeneratorInvocation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { generatorsYml } from "@fern-api/configuration";
import { ReadmeSchema } from "@fern-api/configuration/src/generators-yml";
import { assertNever } from "@fern-api/core-utils";
import { AbsoluteFilePath } from "@fern-api/fs-utils";
import { FernFiddle } from "@fern-fern/fiddle-sdk";
Expand Down Expand Up @@ -28,7 +27,7 @@ export function getGeneratorInvocation({
fixtureName: string;
irVersion: string;
publishMetadata: unknown;
readme: ReadmeSchema | undefined;
readme: generatorsYml.ReadmeSchema | undefined;
}): generatorsYml.GeneratorInvocation {
return {
name: docker.name,
Expand All @@ -43,7 +42,8 @@ export function getGeneratorInvocation({
disableExamples: false,
irVersionOverride: irVersion,
publishMetadata: publishMetadata != null ? (publishMetadata as FernFiddle.PublishingMetadata) : undefined,
readme
readme,
settings: undefined
};
}

Expand Down

0 comments on commit c49432a

Please sign in to comment.