From ae7b17d1cbb0f2f84cea63081e2a825af3dae1fe Mon Sep 17 00:00:00 2001 From: Zach Kirsch Date: Sat, 1 Oct 2022 22:45:55 -0700 Subject: [PATCH] Add single-union key to YAML (#774) --- .pnp.cjs | 1 + fern.schema.json | 13 +++- .../convertUnionTypeDeclaration.ts | 57 ++++++++++++++--- packages/cli/migrations/package.json | 1 + .../src/migrations/0.0.203/index.ts | 9 +++ .../union-single-property-key.test.ts.snap | 34 ++++++++++ .../simple/fern/api/definition/api.yml | 1 + .../simple/fern/api/definition/blog/blog.yml | 19 ++++++ .../fixtures/simple/fern/api/generators.yml | 15 +++++ .../fixtures/simple/fern/fern.config.json | 4 ++ .../union-single-property-key.test.ts | 29 +++++++++ .../getAllYamlFiles.ts | 26 ++++++++ .../union-single-property-key/index.ts | 1 + .../union-single-property-key/migration.ts | 63 +++++++++++++++++++ packages/cli/migrations/src/migrations/all.ts | 3 +- .../cli/yaml/validator/src/getAllRules.ts | 2 + .../fixtures/simple/definition/api.yml | 1 + .../fixtures/simple/definition/posts.yml | 22 +++++++ .../__test__/fixtures/simple/generators.yml | 1 + .../no-object-single-property-key.test.ts | 40 ++++++++++++ .../no-object-single-property-key/index.ts | 1 + .../no-object-single-property-key.ts | 44 +++++++++++++ .../src/ast/visitors/visitTypeDeclarations.ts | 1 + .../src/schemas/SingleUnionTypeKeySchema.ts | 8 +++ .../src/schemas/SingleUnionTypeSchema.ts | 2 + .../cli/yaml/yaml-schema/src/schemas/index.ts | 1 + yarn.lock | 1 + 27 files changed, 388 insertions(+), 12 deletions(-) create mode 100644 packages/cli/migrations/src/migrations/0.0.203/index.ts create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/__snapshots__/union-single-property-key.test.ts.snap create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/api.yml create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/blog/blog.yml create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/generators.yml create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/fern.config.json create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/union-single-property-key.test.ts create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/getAllYamlFiles.ts create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/index.ts create mode 100644 packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/migration.ts create mode 100644 packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/api.yml create mode 100644 packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/posts.yml create mode 100644 packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/generators.yml create mode 100644 packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/no-object-single-property-key.test.ts create mode 100644 packages/cli/yaml/validator/src/rules/no-object-single-property-key/index.ts create mode 100644 packages/cli/yaml/validator/src/rules/no-object-single-property-key/no-object-single-property-key.ts create mode 100644 packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeKeySchema.ts diff --git a/.pnp.cjs b/.pnp.cjs index 67f92c3482d..cd04d285636 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -8212,6 +8212,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["prettier", "npm:2.7.1"],\ ["tmp-promise", "npm:3.0.3"],\ ["typescript", "patch:typescript@npm%3A4.6.4#~builtin::version=4.6.4&hash=f456af"],\ + ["yaml", "npm:2.1.1"],\ ["zod", "npm:3.16.0"]\ ],\ "linkType": "SOFT"\ diff --git a/fern.schema.json b/fern.schema.json index 08d7986dbf9..5035ac17a91 100644 --- a/fern.schema.json +++ b/fern.schema.json @@ -93,7 +93,18 @@ "properties": { "docs": { "$ref": "#/properties/ids/items/anyOf/1/properties/docs" }, "name": { "type": "string" }, - "type": { "type": "string" } + "type": { "type": "string" }, + "key": { + "anyOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { "name": { "type": "string" }, "value": { "type": "string" } }, + "required": ["value"], + "additionalProperties": false + } + ] + } }, "additionalProperties": false } diff --git a/packages/cli/generation/ir-generator/src/converters/type-declarations/convertUnionTypeDeclaration.ts b/packages/cli/generation/ir-generator/src/converters/type-declarations/convertUnionTypeDeclaration.ts index 016f7d0cc10..abce26e090c 100644 --- a/packages/cli/generation/ir-generator/src/converters/type-declarations/convertUnionTypeDeclaration.ts +++ b/packages/cli/generation/ir-generator/src/converters/type-declarations/convertUnionTypeDeclaration.ts @@ -5,7 +5,7 @@ import { TypeResolver } from "../../resolvers/TypeResolver"; import { generateWireStringWithAllCasings } from "../../utils/generateCasings"; import { getDocs } from "../../utils/getDocs"; -const UNION_VALUE_PROPERTY_NAME = "value"; +const DEFAULT_UNION_VALUE_PROPERTY_VALUE = "value"; export function convertUnionTypeDeclaration({ union, @@ -48,7 +48,13 @@ export function convertUnionTypeDeclaration({ return { discriminantValue, valueType, - shape: getSingleUnionTypeProperties(rawType, valueType, file, typeResolver), + shape: getSingleUnionTypeProperties({ + rawType, + valueType, + file, + typeResolver, + rawSingleUnionType: typeof unionedType !== "string" ? unionedType.key : undefined, + }), docs: getDocs(unionedType), }; }), @@ -101,12 +107,19 @@ export function getUnionedTypeName({ }; } -function getSingleUnionTypeProperties( - rawType: string, - valueType: TypeReference, - file: FernFileContext, - typeResolver: TypeResolver -): SingleUnionTypeProperties { +function getSingleUnionTypeProperties({ + rawType, + valueType, + file, + typeResolver, + rawSingleUnionType, +}: { + rawType: string; + valueType: TypeReference; + file: FernFileContext; + typeResolver: TypeResolver; + rawSingleUnionType: string | RawSchemas.SingleUnionTypeKeySchema | undefined; +}): SingleUnionTypeProperties { const resolvedType = typeResolver.resolveType({ type: rawType, file }); if (resolvedType._type === "named" && isRawObjectDefinition(resolvedType.declaration)) { @@ -114,10 +127,34 @@ function getSingleUnionTypeProperties( } else { return SingleUnionTypeProperties.singleProperty({ name: generateWireStringWithAllCasings({ - wireValue: UNION_VALUE_PROPERTY_NAME, - name: UNION_VALUE_PROPERTY_NAME, + wireValue: getSinglePropertyKeyValue(rawSingleUnionType), + name: getSinglePropertyKeyName(rawSingleUnionType), }), type: valueType, }); } } + +function getSinglePropertyKeyName( + rawSingleUnionType: string | RawSchemas.SingleUnionTypeKeySchema | undefined +): string { + if (rawSingleUnionType != null) { + if (typeof rawSingleUnionType === "string") { + return rawSingleUnionType; + } + return rawSingleUnionType.name ?? rawSingleUnionType.value; + } + return DEFAULT_UNION_VALUE_PROPERTY_VALUE; +} + +function getSinglePropertyKeyValue( + rawSingleUnionType: string | RawSchemas.SingleUnionTypeKeySchema | undefined +): string { + if (rawSingleUnionType != null) { + if (typeof rawSingleUnionType === "string") { + return rawSingleUnionType; + } + return rawSingleUnionType.value; + } + return DEFAULT_UNION_VALUE_PROPERTY_VALUE; +} diff --git a/packages/cli/migrations/package.json b/packages/cli/migrations/package.json index ab10b72cacd..e54d351e093 100644 --- a/packages/cli/migrations/package.json +++ b/packages/cli/migrations/package.json @@ -35,6 +35,7 @@ "glob-promise": "^4.2.2", "inquirer": "^9.1.0", "js-yaml": "^4.1.0", + "yaml": "^2.1.1", "zod": "^3.14.3" }, "devDependencies": { diff --git a/packages/cli/migrations/src/migrations/0.0.203/index.ts b/packages/cli/migrations/src/migrations/0.0.203/index.ts new file mode 100644 index 00000000000..2d39be87d99 --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/index.ts @@ -0,0 +1,9 @@ +import { VersionMigrations } from "../../types/VersionMigrations"; +import UnionSinglePropertyKeyMigration from "./union-single-property-key"; + +const versionMigrations: VersionMigrations = { + version: "0.0.203", + migrations: [UnionSinglePropertyKeyMigration], +}; + +export default versionMigrations; diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/__snapshots__/union-single-property-key.test.ts.snap b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/__snapshots__/union-single-property-key.test.ts.snap new file mode 100644 index 00000000000..5a5b8c7523c --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/__snapshots__/union-single-property-key.test.ts.snap @@ -0,0 +1,34 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`union-single-property-key simple 1`] = ` +"types: + a: string + b: boolean + c: + properties: + a: integer + d: + union: + a: + type: string + key: a + b: + type: boolean + key: b + d: + type: d + key: d + e: + union: + a: + type: string + key: a + b: + type: boolean + key: b + d: + type: d + docs: hello + key: d +" +`; diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/api.yml b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/api.yml new file mode 100644 index 00000000000..fc152e1bbf7 --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/api.yml @@ -0,0 +1 @@ +name: my-api diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/blog/blog.yml b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/blog/blog.yml new file mode 100644 index 00000000000..f3a8e07c63e --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/definition/blog/blog.yml @@ -0,0 +1,19 @@ +types: + a: string + b: boolean + c: + properties: + a: integer + d: + union: + a: string + b: boolean + d: d + e: + union: + a: + type: string + b: boolean + d: + type: d + docs: hello diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/generators.yml b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/generators.yml new file mode 100644 index 00000000000..d3652498655 --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/api/generators.yml @@ -0,0 +1,15 @@ +generators: + - name: fernapi/fern-typescript + version: 0.0.144 + config: + mode: model + - name: fernapi/fern-java + version: 0.0.84 + generate: true + config: + packagePrefix: "com.fern" + mode: MODEL + - name: fernapi/fern-postman + version: 0.0.18 + - name: fernapi/fern-openapi + version: 0.0.5 diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/fern.config.json b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/fern.config.json new file mode 100644 index 00000000000..9d3e64510cd --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/fixtures/simple/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "fern", + "version": "0.0.0" +} diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/union-single-property-key.test.ts b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/union-single-property-key.test.ts new file mode 100644 index 00000000000..9524a0f3730 --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/__test__/union-single-property-key.test.ts @@ -0,0 +1,29 @@ +import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/core-utils"; +import { createMockTaskContext } from "@fern-api/task-context"; +import { cp, readFile } from "fs/promises"; +import tmp from "tmp-promise"; +import { migration } from "../migration"; + +const FIXTURES_PATH = join(AbsoluteFilePath.of(__dirname), RelativeFilePath.of("fixtures")); + +describe("union-single-property-key", () => { + it("simple", async () => { + const fixturePath = join(FIXTURES_PATH, RelativeFilePath.of("simple")); + const tmpDir = await tmp.dir(); + + await cp(fixturePath, tmpDir.path, { recursive: true }); + process.chdir(tmpDir.path); + + await migration.run({ + context: createMockTaskContext(), + }); + + const newBlogYaml = ( + await readFile( + join(AbsoluteFilePath.of(tmpDir.path), RelativeFilePath.of("fern/api/definition/blog/blog.yml")) + ) + ).toString(); + + expect(newBlogYaml).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/getAllYamlFiles.ts b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/getAllYamlFiles.ts new file mode 100644 index 00000000000..0cbc0848a49 --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/getAllYamlFiles.ts @@ -0,0 +1,26 @@ +import { AbsoluteFilePath } from "@fern-api/core-utils"; +import { TaskContext, TASK_FAILURE } from "@fern-api/task-context"; +import { findUp } from "find-up"; +import glob from "glob-promise"; + +const FERN_DIRECTORY = "fern"; + +export async function getAllYamlFiles(context: TaskContext): Promise { + const fernDirectory = await getFernDirectory(); + if (fernDirectory == null) { + return context.fail(`Directory "${FERN_DIRECTORY}" not found.`); + } + const filepaths = await glob("*/definition/**/*.yml", { + cwd: fernDirectory, + absolute: true, + }); + return filepaths.map(AbsoluteFilePath.of); +} + +async function getFernDirectory(): Promise { + const fernDirectoryStr = await findUp(FERN_DIRECTORY, { type: "directory" }); + if (fernDirectoryStr == null) { + return undefined; + } + return AbsoluteFilePath.of(fernDirectoryStr); +} diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/index.ts b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/index.ts new file mode 100644 index 00000000000..218b9952ac0 --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/index.ts @@ -0,0 +1 @@ +export { migration as default } from "./migration"; diff --git a/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/migration.ts b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/migration.ts new file mode 100644 index 00000000000..a979bfe8bed --- /dev/null +++ b/packages/cli/migrations/src/migrations/0.0.203/union-single-property-key/migration.ts @@ -0,0 +1,63 @@ +import { AbsoluteFilePath } from "@fern-api/core-utils"; +import { TaskContext, TASK_FAILURE } from "@fern-api/task-context"; +import { readFile, writeFile } from "fs/promises"; +import YAML from "yaml"; +import { Migration } from "../../../types/Migration"; +import { getAllYamlFiles } from "./getAllYamlFiles"; + +export const migration: Migration = { + name: "union-single-property-migration", + summary: "migrates union types to set the `key` property on non-object subtypes to the discriminant value.", + run: async ({ context }) => { + const yamlFiles = await getAllYamlFiles(context); + if (yamlFiles === TASK_FAILURE) { + return; + } + for (const filepath of yamlFiles) { + try { + await migrateFile(filepath, context); + } catch (error) { + context.fail(`Failed to add 'key' property to union in ${filepath}`, error); + } + } + }, +}; + +async function migrateFile(filepath: AbsoluteFilePath, context: TaskContext): Promise { + const contents = await readFile(filepath); + const parsedDocument = YAML.parseDocument(contents.toString()); + const types = parsedDocument.get("types"); + if (types == null) { + return; + } + if (!YAML.isMap(types)) { + context.fail(`"types" is not a map in ${filepath}`); + return; + } + + for (const typeDeclaration of types.items) { + if (YAML.isMap(typeDeclaration.value)) { + const union = typeDeclaration.value.get("union"); + console.log(typeDeclaration, union); + if (union == null) { + continue; + } + if (!YAML.isMap(union)) { + context.fail(`"union" is not a map in ${filepath}`); + continue; + } + for (const singleUnionType of union.items) { + if (YAML.isScalar(singleUnionType.value)) { + singleUnionType.value = { + type: singleUnionType.value, + key: singleUnionType.key, + }; + } else if (YAML.isMap(singleUnionType.value)) { + singleUnionType.value.add(new YAML.Pair("key", singleUnionType.key)); + } + } + } + } + + await writeFile(filepath, parsedDocument.toString()); +} diff --git a/packages/cli/migrations/src/migrations/all.ts b/packages/cli/migrations/src/migrations/all.ts index ad8c80e4550..acfc2bcb128 100644 --- a/packages/cli/migrations/src/migrations/all.ts +++ b/packages/cli/migrations/src/migrations/all.ts @@ -1,5 +1,6 @@ import { VersionMigrations } from "../types/VersionMigrations"; import migrations_0_0_188 from "./0.0.188"; import migrations_0_0_191 from "./0.0.191"; +import migrations_0_0_203 from "./0.0.203"; -export const ALL_MIGRATIONS: VersionMigrations[] = [migrations_0_0_188, migrations_0_0_191]; +export const ALL_MIGRATIONS: VersionMigrations[] = [migrations_0_0_188, migrations_0_0_191, migrations_0_0_203]; diff --git a/packages/cli/yaml/validator/src/getAllRules.ts b/packages/cli/yaml/validator/src/getAllRules.ts index 22ad5154ce2..c9a3c15b1ac 100644 --- a/packages/cli/yaml/validator/src/getAllRules.ts +++ b/packages/cli/yaml/validator/src/getAllRules.ts @@ -4,6 +4,7 @@ import NoCircularImportsRule from "./rules/no-circular-imports"; import NoDuplicateDeclarationsRule from "./rules/no-duplicate-declarations"; import NoDuplicateEnumValuesRule from "./rules/no-duplicate-enum-values"; import NoDuplicateFieldNamesRule from "./rules/no-duplicate-field-names"; +import NoObjectSinglePropertyKey from "./rules/no-object-single-property-key"; import NoUndefinedErrorReferenceRule from "./rules/no-undefined-error-reference"; import NoUndefinedPathParametersRule from "./rules/no-undefined-path-parameters"; import NoUndefinedTypeReferenceRule from "./rules/no-undefined-type-reference"; @@ -20,5 +21,6 @@ export function getAllRules(): Rule[] { NoCircularImportsRule, ValidFieldNamesRule, NoDuplicateFieldNamesRule, + NoObjectSinglePropertyKey, ]; } diff --git a/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/api.yml b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/api.yml new file mode 100644 index 00000000000..eee3f038239 --- /dev/null +++ b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/api.yml @@ -0,0 +1 @@ +name: simple-api diff --git a/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/posts.yml b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/posts.yml new file mode 100644 index 00000000000..6944592db01 --- /dev/null +++ b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/definition/posts.yml @@ -0,0 +1,22 @@ +types: + MyPrimitiveAlias: string + MyObjectAlias: MyObject + MyObject: + properties: + a: string + b: number + MyUnion: + union: + a: MyPrimitiveAlias + b: + type: string + key: hello + c: MyObjectAlias + d: + type: MyObjectAlias + key: hello + e: + key: yoyo + f: + key: yoyo + type: void diff --git a/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/generators.yml b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/fixtures/simple/generators.yml @@ -0,0 +1 @@ +{} diff --git a/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/no-object-single-property-key.test.ts b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/no-object-single-property-key.test.ts new file mode 100644 index 00000000000..b748f552417 --- /dev/null +++ b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/__test__/no-object-single-property-key.test.ts @@ -0,0 +1,40 @@ +import { AbsoluteFilePath, join, RelativeFilePath } from "@fern-api/core-utils"; +import { getViolationsForRule } from "../../../testing-utils/getViolationsForRule"; +import { ValidationViolation } from "../../../ValidationViolation"; +import { NoObjectSinglePropertyKeyRule } from "../no-object-single-property-key"; + +describe("valid-field-names", () => { + it("simple", async () => { + const violations = await getViolationsForRule({ + rule: NoObjectSinglePropertyKeyRule, + absolutePathToWorkspace: join( + AbsoluteFilePath.of(__dirname), + RelativeFilePath.of("fixtures"), + RelativeFilePath.of("simple") + ), + }); + + const expectedViolations: ValidationViolation[] = [ + { + message: "Union subtype d extends an object, so key cannot be defined", + nodePath: ["types", "MyUnion"], + relativeFilepath: RelativeFilePath.of("posts.yml"), + severity: "error", + }, + { + message: "Union subtype e has no body, so key cannot be defined", + nodePath: ["types", "MyUnion"], + relativeFilepath: RelativeFilePath.of("posts.yml"), + severity: "error", + }, + { + message: "Union subtype f has no body, so key cannot be defined", + nodePath: ["types", "MyUnion"], + relativeFilepath: RelativeFilePath.of("posts.yml"), + severity: "error", + }, + ]; + + expect(violations).toEqual(expectedViolations); + }); +}); diff --git a/packages/cli/yaml/validator/src/rules/no-object-single-property-key/index.ts b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/index.ts new file mode 100644 index 00000000000..30e44c3cbd0 --- /dev/null +++ b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/index.ts @@ -0,0 +1 @@ +export { NoObjectSinglePropertyKeyRule as default } from "./no-object-single-property-key"; diff --git a/packages/cli/yaml/validator/src/rules/no-object-single-property-key/no-object-single-property-key.ts b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/no-object-single-property-key.ts new file mode 100644 index 00000000000..e5f68d7005e --- /dev/null +++ b/packages/cli/yaml/validator/src/rules/no-object-single-property-key/no-object-single-property-key.ts @@ -0,0 +1,44 @@ +import { constructFernFileContext, TypeResolverImpl } from "@fern-api/ir-generator"; +import { isRawObjectDefinition, isRawUnionDefinition } from "@fern-api/yaml-schema"; +import { Rule, RuleViolation } from "../../Rule"; + +export const NoObjectSinglePropertyKeyRule: Rule = { + name: "no-object-single-property-key", + create: ({ workspace }) => { + const typeResolver = new TypeResolverImpl(workspace); + + return { + typeDeclaration: ({ declaration }, { relativeFilepath, contents }) => { + const violations: RuleViolation[] = []; + if (!isRawUnionDefinition(declaration)) { + return violations; + } + + for (const [discriminantValue, singleUnionType] of Object.entries(declaration.union)) { + if (typeof singleUnionType !== "string" && singleUnionType.key != null) { + const resolvedType = + singleUnionType.type != null + ? typeResolver.resolveType({ + type: singleUnionType.type, + file: constructFernFileContext({ relativeFilepath, serviceFile: contents }), + }) + : undefined; + if (resolvedType == null || resolvedType._type === "void") { + violations.push({ + severity: "error", + message: `Union subtype ${discriminantValue} has no body, so key cannot be defined`, + }); + } else if (resolvedType._type === "named" && isRawObjectDefinition(resolvedType.declaration)) { + violations.push({ + severity: "error", + message: `Union subtype ${discriminantValue} extends an object, so key cannot be defined`, + }); + } + } + } + + return violations; + }, + }; + }, +}; diff --git a/packages/cli/yaml/yaml-schema/src/ast/visitors/visitTypeDeclarations.ts b/packages/cli/yaml/yaml-schema/src/ast/visitors/visitTypeDeclarations.ts index 39a89878b1e..5a8d1d802f3 100644 --- a/packages/cli/yaml/yaml-schema/src/ast/visitors/visitTypeDeclarations.ts +++ b/packages/cli/yaml/yaml-schema/src/ast/visitors/visitTypeDeclarations.ts @@ -93,6 +93,7 @@ export async function visitTypeDeclaration({ await visitObject(unionType, { docs: createDocsVisitor(visitor, nodePathForUnionType), name: noop, + key: noop, type: async (type) => { if (type != null) { await visitor.typeReference?.(type, [...nodePathForType, "type"]); diff --git a/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeKeySchema.ts b/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeKeySchema.ts new file mode 100644 index 00000000000..761e0af8fb0 --- /dev/null +++ b/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeKeySchema.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const SingleUnionTypeKeySchema = z.strictObject({ + name: z.optional(z.string()), + value: z.string(), +}); + +export type SingleUnionTypeKeySchema = z.infer; diff --git a/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeSchema.ts b/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeSchema.ts index 3edef1ddc10..c8bcb248a19 100644 --- a/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeSchema.ts +++ b/packages/cli/yaml/yaml-schema/src/schemas/SingleUnionTypeSchema.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { SingleUnionTypeKeySchema } from "./SingleUnionTypeKeySchema"; import { WithDocsSchema } from "./WithDocsSchema"; export const SingleUnionTypeSchema = z.union([ @@ -6,6 +7,7 @@ export const SingleUnionTypeSchema = z.union([ WithDocsSchema.extend({ name: z.optional(z.string()), type: z.optional(z.string()), + key: z.optional(z.union([z.string(), SingleUnionTypeKeySchema])), }), ]); diff --git a/packages/cli/yaml/yaml-schema/src/schemas/index.ts b/packages/cli/yaml/yaml-schema/src/schemas/index.ts index ebb8a191f61..ed8b4099a05 100644 --- a/packages/cli/yaml/yaml-schema/src/schemas/index.ts +++ b/packages/cli/yaml/yaml-schema/src/schemas/index.ts @@ -24,6 +24,7 @@ export { ObjectPropertySchema } from "./ObjectPropertySchema"; export { ObjectSchema } from "./ObjectSchema"; export { ResponseErrorsSchema } from "./ResponseErrorsSchema"; export { ServicesSchema } from "./ServicesSchema"; +export { SingleUnionTypeKeySchema } from "./SingleUnionTypeKeySchema"; export { SingleUnionTypeSchema } from "./SingleUnionTypeSchema"; export { TypeDeclarationSchema } from "./TypeDeclarationSchema"; export { TypeReferenceSchema } from "./TypeReferenceSchema"; diff --git a/yarn.lock b/yarn.lock index 17213ed63bf..fd4b9ec9ac9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3587,6 +3587,7 @@ __metadata: prettier: ^2.7.1 tmp-promise: ^3.0.3 typescript: 4.6.4 + yaml: ^2.1.1 zod: ^3.14.3 languageName: unknown linkType: soft