Skip to content

Commit

Permalink
BREAKING CHANGE: generate marker interfaces for union types by default (
Browse files Browse the repository at this point in the history
  • Loading branch information
danadajian authored Apr 24, 2024
1 parent 52f6b49 commit cee01d4
Show file tree
Hide file tree
Showing 43 changed files with 403 additions and 328 deletions.
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion docs/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ sidebar_position: 3
# Configuration

```ts reference title="Config Schema"
https://github.com/ExpediaGroup/graphql-kotlin-codegen/blob/main/src/config.ts#L17-L100000
https://github.com/ExpediaGroup/graphql-kotlin-codegen/blob/main/src/config.ts#L1-L100000
```
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export default [
},
},
rules: {
"no-console": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "error",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"@graphql-codegen/cli": "5.0.2",
"@total-typescript/ts-reset": "0.5.1",
"bun-types": "1.1.4",
"eslint": "9.1.0",
"eslint": "9.1.1",
"husky": "9.0.11",
"prettier": "3.2.5",
"tsup": "8.0.2",
Expand All @@ -39,7 +39,7 @@
"build": "tsup src/plugin.ts --clean --dts --external graphql",
"format": "prettier --write .",
"format-check": "prettier --check .",
"integration": "bun run build && graphql-codegen && ./gradlew compileTestKotlin",
"integration": "bun run build && graphql-codegen && ./gradlew build",
"lint": "eslint .",
"prepack": "bun run build",
"prepare": "husky",
Expand Down
13 changes: 11 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
array,
boolean,
enum_,
literal,
object,
optional,
string,
Expand Down Expand Up @@ -44,11 +45,11 @@ export const configSchema = object({
dependentTypesInScope: optional(array(string())),
/**
* Denotes Kotlin classes representing union types to be treated as interfaces rather than annotation classes.
* This should be used for types outside `dependentTypesInScope` that are not generated by the plugin.
* @description This should be used for types outside `dependentTypesInScope` that are not generated by the plugin. Only use when unionGeneration is set to `ANNOTATION_CLASS`.
*/
externalUnionsAsInterfaces: optional(array(string())),
/**
* Additional imports to add to the generated file.
* Additional imports to add to the generated file. GraphQL Kotlin annotations are always imported.
* @example ["com.example.additional.import.*"]
*/
extraImports: optional(array(string())),
Expand Down Expand Up @@ -129,4 +130,12 @@ export const configSchema = object({
}),
),
),
/**
* Denotes the generation strategy for union types. Defaults to `MARKER_INTERFACE`.
* @description The `MARKER_INTERFACE` option is highly recommended, since it is more type-safe than using annotation classes.
* @link https://opensource.expediagroup.com/graphql-kotlin/docs/schema-generator/writing-schemas/unions/
*/
unionGeneration: optional(
union([literal("ANNOTATION_CLASS"), literal("MARKER_INTERFACE")]),
),
});
8 changes: 4 additions & 4 deletions src/definitions/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ import { EnumTypeDefinitionNode, EnumValueDefinitionNode } from "graphql";
import { indentMultiline } from "@graphql-codegen/visitor-plugin-common";
import { buildAnnotations } from "../helpers/build-annotations";
import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition";
import { CodegenConfig } from "../plugin";
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";

export function buildEnumTypeDefinition(
node: EnumTypeDefinitionNode,
config: CodegenConfig,
config: CodegenConfigWithDefaults,
) {
if (!shouldIncludeTypeDefinition(node, config)) {
return "";
Expand All @@ -46,11 +46,11 @@ ${indentMultiline(enumValues.join(",\n") + ";", 2)}

function buildEnumValueDefinition(
node: EnumValueDefinitionNode,
config: CodegenConfig,
config: CodegenConfigWithDefaults,
) {
const annotations = buildAnnotations({
config,
definitionNode: node,
});
return `${annotations}${config.convert(node)}`;
return `${annotations}${config.convert?.(node)}`;
}
5 changes: 2 additions & 3 deletions src/definitions/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-defi
import { buildTypeMetadata } from "../helpers/build-type-metadata";
import { buildAnnotations } from "../helpers/build-annotations";
import { indent } from "@graphql-codegen/visitor-plugin-common";
import { CodegenConfig } from "../plugin";
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";

export function buildInputObjectDefinition(
node: InputObjectTypeDefinitionNode,
schema: GraphQLSchema,
config: CodegenConfig,
config: CodegenConfigWithDefaults,
) {
if (!shouldIncludeTypeDefinition(node, config)) {
return "";
Expand All @@ -47,7 +47,6 @@ export function buildInputObjectDefinition(

const annotations = buildAnnotations({
config,
inputDescription: node.description?.value,
definitionNode: node,
});

Expand Down
4 changes: 2 additions & 2 deletions src/definitions/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import { indent } from "@graphql-codegen/visitor-plugin-common";
import { buildTypeMetadata } from "../helpers/build-type-metadata";
import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition";
import { buildFieldDefinition } from "../helpers/build-field-definition";
import { CodegenConfig } from "../plugin";
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";

export function buildInterfaceDefinition(
node: InterfaceTypeDefinitionNode,
schema: GraphQLSchema,
config: CodegenConfig,
config: CodegenConfigWithDefaults,
) {
if (!shouldIncludeTypeDefinition(node, config)) {
return "";
Expand Down
26 changes: 17 additions & 9 deletions src/definitions/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,19 @@ import { buildAnnotations } from "../helpers/build-annotations";
import { indent } from "@graphql-codegen/visitor-plugin-common";
import { buildTypeMetadata } from "../helpers/build-type-metadata";
import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition";
import { getDependentInterfaceNames } from "../helpers/dependent-type-utils";
import {
getDependentInterfaceNames,
getDependentUnionsForType,
} from "../helpers/dependent-type-utils";
import { isResolverType } from "../helpers/is-resolver-type";
import { buildFieldDefinition } from "../helpers/build-field-definition";
import { isExternalField } from "../helpers/is-external-field";
import { CodegenConfig } from "../plugin";
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";

export function buildObjectTypeDefinition(
node: ObjectTypeDefinitionNode,
schema: GraphQLSchema,
config: CodegenConfig,
config: CodegenConfigWithDefaults,
) {
if (!shouldIncludeTypeDefinition(node, config)) {
return "";
Expand All @@ -36,33 +39,38 @@ export function buildObjectTypeDefinition(
definitionNode: node,
});
const name = node.name.value;
const interfacesToInherit = getDependentInterfaceNames(node);
const dependentInterfaces = getDependentInterfaceNames(node);
const dependentUnions = getDependentUnionsForType(schema, node);
const interfacesToInherit =
config.unionGeneration === "MARKER_INTERFACE"
? dependentInterfaces.concat(dependentUnions)
: dependentInterfaces;
const interfaceInheritance = `${interfacesToInherit.length ? ` : ${interfacesToInherit.join(", ")}` : ""}`;

if (isResolverType(node, config)) {
return `${annotations}@GraphQLIgnore\ninterface ${name}${interfaceInheritance} {
${getClassMembers({ node, schema, config })}
${getDataClassMembers({ node, schema, config })}
}
${annotations}@GraphQLIgnore\ninterface ${name}CompletableFuture {
${getClassMembers({ node, schema, config, completableFuture: true })}
${getDataClassMembers({ node, schema, config, completableFuture: true })}
}`;
}

return `${annotations}data class ${name}(
${getClassMembers({ node, schema, config })}
${getDataClassMembers({ node, schema, config })}
)${interfaceInheritance}`;
}

function getClassMembers({
function getDataClassMembers({
node,
schema,
config,
completableFuture,
}: {
node: ObjectTypeDefinitionNode;
schema: GraphQLSchema;
config: CodegenConfig;
config: CodegenConfigWithDefaults;
completableFuture?: boolean;
}) {
const resolverType = isResolverType(node, config);
Expand Down
22 changes: 15 additions & 7 deletions src/definitions/union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,33 @@ limitations under the License.

import { UnionTypeDefinitionNode } from "graphql";
import { shouldIncludeTypeDefinition } from "../helpers/should-include-type-definition";
import { buildDirectiveAnnotations } from "../helpers/build-directive-annotations";
import { CodegenConfig } from "../plugin";
import { trimDescription } from "../helpers/build-annotations";
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";
import {
buildAnnotations,
trimDescription,
} from "../helpers/build-annotations";

export function buildUnionTypeDefinition(
node: UnionTypeDefinitionNode,
config: CodegenConfig,
config: CodegenConfigWithDefaults,
) {
if (!shouldIncludeTypeDefinition(node, config)) {
return "";
}
const annotations = buildAnnotations({
config,
definitionNode: node,
});
if (config.unionGeneration === "MARKER_INTERFACE") {
return `${annotations}interface ${node.name.value}`;
}

const directiveAnnotations = buildDirectiveAnnotations(node, config);
const possibleTypes =
node.types?.map((type) => `${type.name.value}::class`).join(", ") || "";
return `${directiveAnnotations}@GraphQLUnion(
return `${annotations}@GraphQLUnion(
name = "${node.name.value}",
possibleTypes = [${possibleTypes}],
description = "${node.description?.value ? trimDescription(node.description.value) : ""}"
description = "${trimDescription(node.description?.value)}"
)
annotation class ${node.name.value}`;
}
36 changes: 36 additions & 0 deletions src/helpers/add-dependent-types-to-only-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Copyright 2024 Expedia, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import { CodegenConfigWithDefaults } from "./build-config-with-defaults";
import { getDependentTypeNames } from "./get-dependent-type-names";
import { GraphQLSchema } from "graphql";

export function addDependentTypesToOnlyTypes(
config: CodegenConfigWithDefaults,
schema: GraphQLSchema,
) {
if (!config.onlyTypes) {
throw new Error(`onlyTypes config is required to add dependent types`);
}
const onlyTypesNodes = config.onlyTypes
.map((typeName) => schema.getType(typeName)?.astNode)
.filter(Boolean);
const dependentTypeNames = onlyTypesNodes.flatMap((node) =>
getDependentTypeNames(schema, node, config),
);
const typesInScope = config.dependentTypesInScope;
const dependentTypesInScope = typesInScope
? dependentTypeNames.filter((typeName) => typesInScope.includes(typeName))
: dependentTypeNames;
config.onlyTypes.push(...dependentTypesInScope);
}
35 changes: 0 additions & 35 deletions src/helpers/add-dependent-types.ts

This file was deleted.

53 changes: 15 additions & 38 deletions src/helpers/build-annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import {
InputValueDefinitionNode,
TypeDefinitionNode,
} from "graphql";
import { buildDescriptionAnnotation } from "./build-description-annotation";
import { buildDirectiveAnnotations } from "./build-directive-annotations";
import { CodegenConfig } from "../plugin";
import { CodegenConfigWithDefaults } from "./build-config-with-defaults";
import { TypeMetadata } from "./build-type-metadata";

export type DefinitionNode =
Expand All @@ -30,39 +31,24 @@ export type DefinitionNode =

export function buildAnnotations({
config,
inputDescription,
definitionNode,
resolvedType,
}: {
config: CodegenConfig;
inputDescription?: string;
definitionNode?: DefinitionNode;
config: CodegenConfigWithDefaults;
definitionNode: DefinitionNode;
resolvedType?: TypeMetadata;
}) {
const description =
inputDescription ?? definitionNode?.description?.value ?? "";
const descriptionAnnotator = isDeprecatedDescription(
const description = definitionNode?.description?.value ?? "";
const descriptionAnnotation = buildDescriptionAnnotation(
description,
definitionNode,
config,
resolvedType,
)
? "@Deprecated"
: "@GraphQLDescription";
const descriptionValue = isDeprecatedDescription(description, resolvedType)
? description.replace("DEPRECATED: ", "")
: description;
const trimmedDescription = trimDescription(descriptionValue);
const descriptionAnnotation = description
? `${descriptionAnnotator}("${trimmedDescription}")\n`
: "";

const directiveAnnotations = definitionNode
? buildDirectiveAnnotations(
definitionNode,
config,
description,
resolvedType,
)
: "";
);
const directiveAnnotations = buildDirectiveAnnotations(
definitionNode,
config,
);
const unionAnnotation = resolvedType?.unionAnnotation
? `@${resolvedType.unionAnnotation}\n`
: "";
Expand All @@ -86,19 +72,10 @@ export function buildAnnotations({
);
}

export function isDeprecatedDescription(
description?: string,
resolvedType?: TypeMetadata,
) {
return (
description?.startsWith("DEPRECATED: ") && !resolvedType?.unionAnnotation
);
}

export function trimDescription(description: string) {
export function trimDescription(description?: string) {
return (
description
.split("\n")
?.split("\n")
.map((str) => str.trim().replaceAll('"', "").replaceAll("\\", ""))
.find((str) => str.match(/^[a-zA-Z]/)) ?? ""
);
Expand Down
Loading

0 comments on commit cee01d4

Please sign in to comment.