Skip to content

Commit 3f6e1f8

Browse files
authored
fix: generate Kotlin interfaces rather than open classes (#64)
* update tests * pass tests * refactor * rename config * refactor
1 parent 2739b28 commit 3f6e1f8

File tree

14 files changed

+130
-116
lines changed

14 files changed

+130
-116
lines changed

codegen.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export default {
1212
plugins: [
1313
{
1414
"dist/plugin.cjs": {
15-
resolverClasses: [
15+
resolverInterfaces: [
1616
{
1717
typeName: "Query",
1818
},

docs/docs/recommended-usage.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ sidebar_position: 4
66

77
In general, the `resolverClasses` config should be used to generate more performant code. This is especially important
88
when dealing with expensive operations, such as database queries or network requests. When at least one field has
9-
arguments in a type, we generate an open class with function signatures to be inherited in source code.
9+
arguments in a type, we generate an interface with function signatures to be inherited in source code.
1010
However, when fields have no arguments, we generate data classes by default.
1111

1212
## Example
@@ -33,8 +33,8 @@ Generated Kotlin:
3333
```kotlin
3434
package com.types.generated
3535

36-
open class Query {
37-
open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.")
36+
interface Query {
37+
fun resolveMyType(input: String): MyType
3838
}
3939

4040
data class MyType(
@@ -50,7 +50,7 @@ import com.expediagroup.graphql.server.operations.Query
5050
import com.types.generated.MyType
5151
import com.types.generated.Query as QueryInterface
5252

53-
class MyQuery : Query, QueryInterface() {
53+
class MyQuery : Query, QueryInterface {
5454
override fun resolveMyType(input: String): MyType =
5555
MyType(
5656
field1 = myExpensiveCall1(),
@@ -86,13 +86,13 @@ Generated Kotlin:
8686
```kotlin
8787
package com.types.generated
8888

89-
open class Query {
90-
open fun resolveMyType(input: String): MyType = throw NotImplementedError("Query.resolveMyType must be implemented.")
89+
interface Query {
90+
fun resolveMyType(input: String): MyType
9191
}
9292

93-
open class MyType {
94-
open fun field1(): String = throw NotImplementedError("MyType.field1 must be implemented.")
95-
open fun field2(): String? = throw NotImplementedError("MyType.field2 must be implemented.")
93+
interface MyType {
94+
fun field1(): String
95+
fun field2(): String?
9696
}
9797
```
9898

@@ -102,12 +102,12 @@ Source code:
102102
import com.types.generated.MyType as MyTypeInterface
103103
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
104104

105-
class MyQuery : Query, QueryInterface() {
105+
class MyQuery : Query, QueryInterface {
106106
override fun resolveMyType(input: String): MyType = MyType()
107107
}
108108

109109
@GraphQLIgnore
110-
class MyType : MyTypeInterface() {
110+
class MyType : MyTypeInterface {
111111
override fun field1(): String = myExpensiveCall1()
112112
override fun field2(): String? = myExpensiveCall2()
113113
}

src/config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ export const configSchema = object({
108108
*/
109109
packageName: optional(string()),
110110
/**
111-
* Denotes types that should be generated as classes. Resolver classes can inherit from these to enforce a type contract.
111+
* Denotes types that should be generated as interfaces rather than classes. Resolver classes should inherit from these to enforce a type contract.
112112
*
113-
* Type names can be optionally passed with the classMethods config to generate `suspend` functions or
113+
* Type names can be optionally passed with the classMethods config to generate the interface with `suspend` functions or
114114
* `java.util.concurrent.CompletableFuture` functions.
115115
* @example
116116
* [
@@ -128,7 +128,7 @@ export const configSchema = object({
128128
* ]
129129
* @link https://opensource.expediagroup.com/graphql-kotlin-codegen/docs/recommended-usage
130130
*/
131-
resolverClasses: optional(
131+
resolverInterfaces: optional(
132132
array(
133133
object({
134134
typeName: string(),

src/definitions/object.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
import { buildFieldDefinition } from "../helpers/build-field-definition";
2828
import { CodegenConfigWithDefaults } from "../helpers/build-config-with-defaults";
2929
import { inputTypeHasMatchingOutputType } from "../helpers/input-type-has-matching-output-type";
30-
import { findTypeInResolverClassesConfig } from "../helpers/findTypeInResolverClassesConfig";
30+
import { findTypeInResolverInterfacesConfig } from "../helpers/findTypeInResolverInterfacesConfig";
3131

3232
export function buildObjectTypeDefinition(
3333
node: ObjectTypeDefinitionNode,
@@ -60,20 +60,20 @@ export function buildObjectTypeDefinition(
6060
? ""
6161
: "@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])\n";
6262

63-
const typeInResolverClassesConfig = findTypeInResolverClassesConfig(
63+
const typeInResolverInterfacesConfig = findTypeInResolverInterfacesConfig(
6464
node,
6565
config,
6666
);
6767
const shouldGenerateFunctions = Boolean(
68-
typeInResolverClassesConfig ||
68+
typeInResolverInterfacesConfig ||
6969
node.fields?.some((fieldNode) => fieldNode.arguments?.length),
7070
);
7171
if (shouldGenerateFunctions) {
7272
const fieldsWithNoArguments = node.fields?.filter(
7373
(fieldNode) => !fieldNode.arguments?.length,
7474
);
7575
const constructor =
76-
!typeInResolverClassesConfig && fieldsWithNoArguments?.length
76+
!typeInResolverInterfacesConfig && fieldsWithNoArguments?.length
7777
? `(\n${fieldsWithNoArguments
7878
.map((fieldNode) => {
7979
const typeMetadata = buildTypeMetadata(
@@ -95,11 +95,13 @@ export function buildObjectTypeDefinition(
9595
const fieldsWithArguments = node.fields?.filter(
9696
(fieldNode) => fieldNode.arguments?.length,
9797
);
98-
const fieldNodes = typeInResolverClassesConfig
98+
const fieldNodes = typeInResolverInterfacesConfig
9999
? node.fields
100100
: fieldsWithArguments;
101-
return `${annotations}${outputRestrictionAnnotation}open class ${name}${constructor}${interfaceInheritance} {
102-
${getDataClassMembers({ node, fieldNodes, schema, config, shouldGenerateFunctions })}
101+
const abstractModifier = constructor ? "abstract " : "";
102+
const keyWord = constructor ? "abstract class" : "interface";
103+
return `${annotations}${outputRestrictionAnnotation}${keyWord} ${name}${constructor}${interfaceInheritance} {
104+
${getDataClassMembers({ node, fieldNodes, schema, config, shouldGenerateFunctions, abstractModifier })}
103105
}`;
104106
}
105107

@@ -113,12 +115,14 @@ function getDataClassMembers({
113115
fieldNodes,
114116
schema,
115117
config,
118+
abstractModifier,
116119
shouldGenerateFunctions,
117120
}: {
118121
node: ObjectTypeDefinitionNode;
119122
fieldNodes?: readonly FieldDefinitionNode[];
120123
schema: GraphQLSchema;
121124
config: CodegenConfigWithDefaults;
125+
abstractModifier?: string;
122126
shouldGenerateFunctions?: boolean;
123127
}) {
124128
return (fieldNodes ?? node.fields)
@@ -131,6 +135,7 @@ function getDataClassMembers({
131135
config,
132136
typeMetadata,
133137
shouldGenerateFunctions,
138+
abstractModifier,
134139
);
135140
})
136141
.join(`${shouldGenerateFunctions ? "" : ","}\n`);

src/helpers/build-field-definition.ts

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
import { CodegenConfigWithDefaults } from "./build-config-with-defaults";
2424
import { indent } from "@graphql-codegen/visitor-plugin-common";
2525
import { buildAnnotations } from "./build-annotations";
26-
import { findTypeInResolverClassesConfig } from "./findTypeInResolverClassesConfig";
26+
import { findTypeInResolverInterfacesConfig } from "./findTypeInResolverInterfacesConfig";
2727

2828
export function buildFieldDefinition(
2929
node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
@@ -32,8 +32,15 @@ export function buildFieldDefinition(
3232
config: CodegenConfigWithDefaults,
3333
typeMetadata: TypeMetadata,
3434
shouldGenerateFunctions?: boolean,
35+
abstractModifier: string = "",
3536
) {
36-
const modifier = buildFieldModifier(node, fieldNode, schema, config);
37+
const modifier = buildFieldModifier(
38+
node,
39+
fieldNode,
40+
schema,
41+
config,
42+
abstractModifier,
43+
);
3744
const fieldArguments = buildFieldArguments(node, fieldNode, schema, config);
3845
const fieldDefinition = `${modifier} ${fieldNode.name.value}${fieldArguments}`;
3946
const annotations = buildAnnotations({
@@ -49,19 +56,18 @@ export function buildFieldDefinition(
4956
);
5057
}
5158

52-
const notImplementedError = ` = throw NotImplementedError("${node.name.value}.${fieldNode.name.value} must be implemented.")`;
53-
const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""}${notImplementedError}`;
59+
const defaultFunctionValue = `${typeMetadata.isNullable ? "?" : ""}`;
5460
const defaultValue = shouldGenerateFunctions
5561
? defaultFunctionValue
5662
: typeMetadata.defaultValue;
5763
const defaultDefinition = `${typeMetadata.typeName}${defaultValue}`;
58-
const typeInResolverClassesConfig = findTypeInResolverClassesConfig(
64+
const typeInResolverInterfacesConfig = findTypeInResolverInterfacesConfig(
5965
node,
6066
config,
6167
);
6268
const isCompletableFuture =
63-
typeInResolverClassesConfig?.classMethods === "COMPLETABLE_FUTURE";
64-
const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>${notImplementedError}`;
69+
typeInResolverInterfacesConfig?.classMethods === "COMPLETABLE_FUTURE";
70+
const completableFutureDefinition = `java.util.concurrent.CompletableFuture<${typeMetadata.typeName}${typeMetadata.isNullable ? "?" : ""}>`;
6571
const field = indent(
6672
`${fieldDefinition}: ${isCompletableFuture ? completableFutureDefinition : defaultDefinition}`,
6773
2,
@@ -74,8 +80,9 @@ function buildFieldModifier(
7480
fieldNode: FieldDefinitionNode,
7581
schema: GraphQLSchema,
7682
config: CodegenConfigWithDefaults,
83+
abstractModifier: string,
7784
) {
78-
const typeInResolverClassesConfig = findTypeInResolverClassesConfig(
85+
const typeInResolverInterfacesConfig = findTypeInResolverInterfacesConfig(
7986
node,
8087
config,
8188
);
@@ -84,20 +91,19 @@ function buildFieldModifier(
8491
fieldNode,
8592
schema,
8693
);
87-
if (!typeInResolverClassesConfig && !fieldNode.arguments?.length) {
88-
return shouldOverrideField ? "override val" : "val";
94+
const overrideModifier = shouldOverrideField ? "override " : "";
95+
if (!typeInResolverInterfacesConfig && !fieldNode.arguments?.length) {
96+
return `${overrideModifier}val`;
8997
}
9098
const functionModifier =
91-
typeInResolverClassesConfig?.classMethods === "SUSPEND" ? "suspend " : "";
99+
typeInResolverInterfacesConfig?.classMethods === "SUSPEND"
100+
? "suspend "
101+
: "";
92102
if (node.kind === Kind.INTERFACE_TYPE_DEFINITION) {
93103
return `${functionModifier}fun`;
94104
}
95-
const isCompletableFuture =
96-
typeInResolverClassesConfig?.classMethods === "COMPLETABLE_FUTURE";
97-
if (shouldOverrideField && !isCompletableFuture) {
98-
return "override fun";
99-
}
100-
return `open ${functionModifier}fun`;
105+
106+
return `${abstractModifier}${overrideModifier}${functionModifier}fun`;
101107
}
102108

103109
function buildFieldArguments(
@@ -106,8 +112,11 @@ function buildFieldArguments(
106112
schema: GraphQLSchema,
107113
config: CodegenConfigWithDefaults,
108114
) {
109-
const typeIsInResolverClasses = findTypeInResolverClassesConfig(node, config);
110-
if (!typeIsInResolverClasses && !fieldNode.arguments?.length) {
115+
const typeIsInResolverInterfaces = findTypeInResolverInterfacesConfig(
116+
node,
117+
config,
118+
);
119+
if (!typeIsInResolverInterfaces && !fieldNode.arguments?.length) {
111120
return "";
112121
}
113122
const isOverrideFunction = shouldModifyFieldWithOverride(
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { InterfaceTypeDefinitionNode, ObjectTypeDefinitionNode } from "graphql";
22
import { CodegenConfigWithDefaults } from "./build-config-with-defaults";
33

4-
export function findTypeInResolverClassesConfig(
4+
export function findTypeInResolverInterfacesConfig(
55
node: ObjectTypeDefinitionNode | InterfaceTypeDefinitionNode,
66
config: CodegenConfigWithDefaults,
77
) {
8-
return config.resolverClasses?.find(
9-
(resolverClass) => resolverClass.typeName === node.name.value,
8+
return config.resolverInterfaces?.find(
9+
(resolverInterface) => resolverInterface.typeName === node.name.value,
1010
);
1111
}

test/integration/Query.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ import com.expediagroup.graphql.server.operations.Query
44
import graphql.schema.DataFetchingEnvironment
55
import test.integration.Query as QueryInterface
66

7-
class IntegrationTestQuery() : Query, QueryInterface() {
7+
class IntegrationTestQuery : Query, QueryInterface {
88
override fun testQuery(dataFetchingEnvironment: DataFetchingEnvironment): SomeType = SomeType()
99
}

test/unit/should_consolidate_input_and_output_types/expected.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,8 @@ data class MyTypeToConsolidateInputParent(
6969
)
7070

7171
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
72-
open class MyTypeToConsolidateParent2 {
73-
open fun field(input: MyTypeToConsolidate, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("MyTypeToConsolidateParent2.field must be implemented.")
72+
interface MyTypeToConsolidateParent2 {
73+
fun field(input: MyTypeToConsolidate, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String?
7474
}
7575

7676
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])

test/unit/should_generate_classes_for_types_with_field_args/expected.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,18 @@ package com.kotlin.generated
33
import com.expediagroup.graphql.generator.annotations.*
44

55
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
6-
open class TypeWithOnlyFieldArgs {
7-
open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("TypeWithOnlyFieldArgs.nullableResolver must be implemented.")
8-
open fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("TypeWithOnlyFieldArgs.nonNullableResolver must be implemented.")
6+
interface TypeWithOnlyFieldArgs {
7+
fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String?
8+
fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String
99
}
1010

1111
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
12-
open class HybridType(
12+
abstract class HybridType(
1313
val nullableField: String? = null,
1414
val nonNullableField: String
1515
) {
16-
open fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String? = throw NotImplementedError("HybridType.nullableResolver must be implemented.")
17-
open fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String = throw NotImplementedError("HybridType.nonNullableResolver must be implemented.")
16+
abstract fun nullableResolver(arg: String, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String?
17+
abstract fun nonNullableResolver(arg: InputTypeForResolver, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): String
1818
}
1919

2020
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
@@ -30,14 +30,14 @@ interface HybridInterface {
3030
}
3131

3232
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
33-
open class TypeImplementingInterface(
33+
abstract class TypeImplementingInterface(
3434
override val field1: String? = null,
3535
override val field2: String,
3636
val booleanField1: Boolean? = null,
3737
val booleanField2: Boolean = false,
3838
val integerField1: Int? = null,
3939
val integerField2: Int
4040
) : HybridInterface {
41-
override fun nullableListResolver(arg1: Int?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List<String?>? = throw NotImplementedError("TypeImplementingInterface.nullableListResolver must be implemented.")
42-
override fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List<String> = throw NotImplementedError("TypeImplementingInterface.nonNullableListResolver must be implemented.")
41+
abstract override fun nullableListResolver(arg1: Int?, arg2: Int, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List<String?>?
42+
abstract override fun nonNullableListResolver(arg1: Int, arg2: Int?, dataFetchingEnvironment: graphql.schema.DataFetchingEnvironment): List<String>
4343
}

0 commit comments

Comments
 (0)