Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fun generateClient(
schemaPath: String,
queries: List<File>,
useOptionalInputWrapper: Boolean = false,
useSharedResponseTypes: Boolean = false,
parserOptions: ParserOptions.Builder.() -> Unit = {}
): List<FileSpec> {
val customScalars = customScalarsMap.associateBy { it.scalar }
Expand All @@ -44,7 +45,8 @@ fun generateClient(
customScalarMap = customScalars,
serializer = serializer,
useOptionalInputWrapper = useOptionalInputWrapper,
parserOptions = parserOptions
parserOptions = parserOptions,
useSharedResponseTypes = useSharedResponseTypes
)
val generator = GraphQLClientGenerator(schemaPath, config)
return generator.generate(queries)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ class GraphQLClientGenerator(
private val documentParser: Parser = Parser()
private val typeAliases: MutableMap<String, TypeAliasSpec> = mutableMapOf()
private val sharedTypes: MutableMap<ClassName, List<TypeSpec>> = mutableMapOf()
private val sharedResponseTypeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
private val globalClassNameCache: MutableMap<String, MutableList<ClassName>> = mutableMapOf()
private val globalTypeToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()

private var generateOptionalSerializer: Boolean = false
private val graphQLSchema: TypeDefinitionRegistry
private val parserOptions: ParserOptions = ParserOptions.newParserOptions().also { this.config.parserOptions(it) }.build()
Expand Down Expand Up @@ -119,7 +123,11 @@ class GraphQLClientGenerator(
allowDeprecated = config.allowDeprecated,
customScalarMap = config.customScalarMap,
serializer = config.serializer,
useOptionalInputWrapper = config.useOptionalInputWrapper
useOptionalInputWrapper = config.useOptionalInputWrapper,
sharedClassNameCache = if (config.useSharedResponseTypes) globalClassNameCache else null,
sharedTypeToSelectionSetMap = if (config.useSharedResponseTypes) globalTypeToSelectionSetMap else null,
useSharedResponseTypes = config.useSharedResponseTypes,
sharedResponseTypeSpecs = if (config.useSharedResponseTypes) sharedResponseTypeSpecs else null
)
val queryConstName = capitalizedOperationName.toUpperUnderscore()
val queryConstProp = PropertySpec.builder(queryConstName, STRING)
Expand Down Expand Up @@ -205,17 +213,23 @@ class GraphQLClientGenerator(
fileSpecs.add(polymorphicTypeSpec.build())
}
context.typeSpecs.minus(polymorphicTypes).forEach { (className, typeSpec) ->
val outputTypeFileSpec = FileSpec.builder(className.packageName, className.simpleName)
.addType(typeSpec)
.build()
fileSpecs.add(outputTypeFileSpec)
if (!(config.useSharedResponseTypes && context.isSharedResponseType(className))) {
val outputTypeFileSpec = FileSpec.builder(className.packageName, className.simpleName)
.addType(typeSpec)
.build()
fileSpecs.add(outputTypeFileSpec)
}
}
operationFileSpec.addType(operationTypeSpec.build())
fileSpecs.add(operationFileSpec.build())

// shared types
sharedTypes.putAll(context.enumClassToTypeSpecs.mapValues { listOf(it.value) })
sharedTypes.putAll(context.inputClassToTypeSpecs.mapValues { listOf(it.value) })
if (config.useSharedResponseTypes) {
sharedTypes.putAll(sharedResponseTypeSpecs.mapValues { listOf(it.value) })
}

context.scalarClassToConverterTypeSpecs
.values
.forEach {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ data class GraphQLClientGeneratorConfig(
/** Explicit opt-in flag to enable support for optional inputs. */
val useOptionalInputWrapper: Boolean = false,
/** Set parser options for processing GraphQL queries and schema definition language documents */
val parserOptions: ParserOptions.Builder.() -> Unit = {}
val parserOptions: ParserOptions.Builder.() -> Unit = {},
/** Opt-in flag to enable cross-operation shared response types. */
val useSharedResponseTypes: Boolean = false
)

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ data class GraphQLClientGeneratorContext(
val allowDeprecated: Boolean = false,
val customScalarMap: Map<String, GraphQLScalar> = mapOf(),
val serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
val useOptionalInputWrapper: Boolean = false
val useOptionalInputWrapper: Boolean = false,
val sharedClassNameCache: MutableMap<String, MutableList<ClassName>>? = null,
val sharedTypeToSelectionSetMap: MutableMap<String, Set<String>>? = null,
val useSharedResponseTypes: Boolean = false,
val sharedResponseTypeSpecs: MutableMap<ClassName, TypeSpec>? = null
) {
// per operation caches
val typeSpecs: MutableMap<ClassName, TypeSpec> = mutableMapOf()
Expand All @@ -50,13 +54,14 @@ data class GraphQLClientGeneratorContext(
internal fun isTypeAlias(typeName: String) = typeAliases.containsKey(typeName)

// class name and type selection caches
val classNameCache: MutableMap<String, MutableList<ClassName>> = mutableMapOf()
val typeToSelectionSetMap: MutableMap<String, Set<String>> = mutableMapOf()
val classNameCache: MutableMap<String, MutableList<ClassName>> = sharedClassNameCache ?: mutableMapOf()
val typeToSelectionSetMap: MutableMap<String, Set<String>> = sharedTypeToSelectionSetMap ?: mutableMapOf()

private val customScalarClassNames: Set<ClassName> = customScalarMap.values.map { it.className }.toSet()
internal fun isCustomScalar(typeName: TypeName): Boolean = customScalarClassNames.contains(typeName)
var requireOptionalSerializer = false
val optionalSerializers: MutableMap<ClassName, TypeSpec> = mutableMapOf()
fun isSharedResponseType(className: ClassName): Boolean = useSharedResponseTypes && sharedResponseTypeSpecs?.containsKey(className) == true
}

sealed class ScalarConverterInfo {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,12 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra
when (graphQLTypeDefinition) {
is ObjectTypeDefinition -> {
className = generateClassName(context, graphQLTypeDefinition, selectionSet)
context.typeSpecs[className] = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
val typeSpec = generateGraphQLObjectTypeSpec(context, graphQLTypeDefinition, selectionSet)
if (context.useSharedResponseTypes) {
context.sharedResponseTypeSpecs?.set(className, typeSpec)
} else {
context.typeSpecs[className] = typeSpec
}
}
is InputObjectTypeDefinition -> {
className = generateClassName(context, graphQLTypeDefinition, selectionSet, packageName = "${context.packageName}.inputs")
Expand Down Expand Up @@ -174,7 +179,11 @@ internal fun generateCustomClassName(context: GraphQLClientGeneratorContext, gra
// should never happen as we can only generate different object, interface or union type
else -> throw UnknownGraphQLTypeException(graphQLType)
}
context.typeSpecs[className] = typeSpec
if (graphQLTypeDefinition is ObjectTypeDefinition && context.useSharedResponseTypes) {
context.sharedResponseTypeSpecs?.set(className, typeSpec)
} else {
context.typeSpecs[className] = typeSpec
}
className
}
}
Expand All @@ -187,7 +196,11 @@ internal fun generateClassName(
graphQLType: NamedNode<*>,
selectionSet: SelectionSet? = null,
nameOverride: String? = null,
packageName: String = "${context.packageName}.${context.operationName.lowercase()}"
packageName: String = if (graphQLType is ObjectTypeDefinition && context.useSharedResponseTypes) {
"${context.packageName}.responses"
} else {
"${context.packageName}.${context.operationName.lowercase()}"
}
): ClassName {
val typeName = nameOverride ?: graphQLType.name
val className = ClassName(packageName, typeName)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
query Operation1 {
first: complexObjectQuery {
id
name
}
second: complexObjectQuery {
id
name
details {
id
value
}
}
third: complexObjectQuery {
id
name
details {
id
}
}
fourth: complexObjectQuery {
id
name
}
fifth: complexObjectQuery {
id
name
details {
id
value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
query Operation2 {
first: complexObjectQuery {
id
name
}
second: complexObjectQuery {
id
name
details {
id
value
}
}
third: complexObjectQuery {
id
name
details {
id
}
}
fourth: complexObjectQuery {
id
name
}
fifth: complexObjectQuery {
id
name
details {
id
value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.expediagroup.generated

import com.expediagroup.generated.responses.ComplexObject
import com.expediagroup.generated.responses.ComplexObject2
import com.expediagroup.generated.responses.ComplexObject3
import com.expediagroup.graphql.client.Generated
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import kotlin.String
import kotlin.reflect.KClass

@Generated
class Operation1 : GraphQLClientRequest<Operation1.Result> {
override val query: String = OPERATION_1

override val operationName: String = "Operation1"

override fun responseType(): KClass<Result> = Result::class

class Result(
val first: ComplexObject,
val second: ComplexObject2,
val third: ComplexObject3,
val fourth: ComplexObject,
val fifth: ComplexObject2,
)
}

const val OPERATION_1: String =
"query Operation1 {\n first: complexObjectQuery {\n id\n name\n }\n second: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n third: complexObjectQuery {\n id\n name\n details {\n id\n }\n }\n fourth: complexObjectQuery {\n id\n name\n }\n fifth: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.expediagroup.generated

import com.expediagroup.generated.responses.ComplexObject
import com.expediagroup.generated.responses.ComplexObject2
import com.expediagroup.generated.responses.ComplexObject3
import com.expediagroup.graphql.client.Generated
import com.expediagroup.graphql.client.types.GraphQLClientRequest
import kotlin.String
import kotlin.reflect.KClass

@Generated
class Operation2 : GraphQLClientRequest<Operation2.Result> {
override val query: String = OPERATION_2

override val operationName: String = "Operation2"

override fun responseType(): KClass<Result> = Result::class

class Result(
val first: ComplexObject,
val second: ComplexObject2,
val third: ComplexObject3,
val fourth: ComplexObject,
val fifth: ComplexObject2,
)
}

const val OPERATION_2: String =
"query Operation2 {\n first: complexObjectQuery {\n id\n name\n }\n second: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n third: complexObjectQuery {\n id\n name\n details {\n id\n }\n }\n fourth: complexObjectQuery {\n id\n name\n }\n fifth: complexObjectQuery {\n id\n name\n details {\n id\n value\n }\n }\n}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.expediagroup.generated.responses

import com.expediagroup.graphql.client.Generated

@Generated
class ComplexObject(
val id: kotlin.Int,
val name: kotlin.String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.expediagroup.generated.responses

import com.expediagroup.graphql.client.Generated

@Generated
class ComplexObject2(
val id: kotlin.Int,
val name: kotlin.String,
val details: DetailsObject,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.expediagroup.generated.responses

import com.expediagroup.graphql.client.Generated

@Generated
class ComplexObject3(
val id: kotlin.Int,
val name: kotlin.String,
val details: DetailsObject?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.expediagroup.generated.responses

import com.expediagroup.graphql.client.Generated

@Generated
class DetailsObject(
val id: kotlin.Int,
val value: kotlin.String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.expediagroup.generated.responses

import com.expediagroup.graphql.client.Generated

@Generated
class DetailsObject2(
val id: kotlin.Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
schema {
query: Query
}

type Query {
complexObjectQuery: ComplexObject
}

type ComplexObject {
id: ID!
name: String!
details: DetailsObject
}

type DetailsObject {
id: ID!
value: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.expediagroup.graphql.plugin.client.generator

import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
import com.expediagroup.graphql.plugin.client.generateClient
import org.junit.jupiter.api.Test
import java.io.File

class GenerateGraphQLClientSharedResponsesIT {
@Test
fun `generate client with shared response types across operations`() {
val files = generateClient(
packageName = "com.expediagroup.generated",
allowDeprecated = false,
customScalarsMap = emptyList(),
serializer = GraphQLSerializer.JACKSON,
schemaPath = "src/test/data/generator/shared_responses/schema.graphql",
queries = listOf(
File("src/test/data/generator/shared_responses/Operation1.graphql"),
File("src/test/data/generator/shared_responses/Operation2.graphql")
),
useOptionalInputWrapper = false,
useSharedResponseTypes = true
)
val namesByPackage = files.groupBy({ it.packageName }, { it.name }).mapValues { entry -> entry.value.toSet() }
check(namesByPackage["com.expediagroup.generated.responses"]?.containsAll(setOf("ComplexObject", "ComplexObject2", "ComplexObject3")) == true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ internal val defaultConfig = GraphQLClientGeneratorConfig(packageName = "com.exp

internal fun locateTestCaseArguments(directory: String) = File(directory)
.listFiles()
?.filter { it.isDirectory }
?.filter { it.isDirectory && it.name != "shared_responses" }
?.map {
Arguments.of(it)
} ?: emptyList()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ open class GraphQLPluginClientExtension {
var serializer: GraphQLSerializer = GraphQLSerializer.JACKSON
/** Opt-in flag to wrap nullable arguments in OptionalInput that supports both null and undefined. */
var useOptionalInputWrapper: Boolean = false

/** Opt-in flag to enable cross-operation shared response types. */
var useSharedResponseTypes: Boolean = false
/** Connect and read timeout configuration for executing introspection query/download schema */
internal val timeoutConfig: TimeoutConfiguration = TimeoutConfiguration()

Expand Down
Loading