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 @@ -177,7 +177,7 @@ class CodeGen private constructor(
codeGenResult.javaInterfaces +
codeGenResult.javaEnumTypes +
codeGenResult.javaQueryTypes +
codeGenResult.clientProjections +
lastProjectionPerPath(codeGenResult.clientProjections) +
codeGenResult.javaConstants,
kotlinFiles =
codeGenResult.kotlinDataTypes +
Expand All @@ -196,6 +196,13 @@ class CodeGen private constructor(
return codeGenResult
}

/**
* Up to 8.7.0 files were written one after another, so of two same-named projections the last one won on disk.
* The parallel writer rejects duplicate destinations instead, so resolve them up front.
*/
private fun lastProjectionPerPath(projections: List<JavaFile>): List<JavaFile> =
projections.associateBy { it.packageName() to it.typeSpec().name() }.values.toList()

/**
* Build a [Document] containing the combined schemas from
* [config].
Expand Down Expand Up @@ -387,13 +394,17 @@ class CodeGen private constructor(
private fun generateJavaClientApi(definitions: Collection<Definition<*>>): CodeGenResult {
val methodNames = mutableSetOf<String>()
return if (config.generateClientApi) {
definitions
val operations =
definitions
.filterIsInstance<ObjectTypeDefinition>()
.filter { it.name == "Query" || it.name == "Mutation" || it.name == "Subscription" }
.sortedBy { it.name.length }
val rootProjectionTypes =
ClientApiGenerator(config, schemaIndex).rootProjectionTypes(operations, federatedDefinitions(definitions))
operations
.asSequence()
.filterIsInstance<ObjectTypeDefinition>()
.filter { it.name == "Query" || it.name == "Mutation" || it.name == "Subscription" }
.sortedBy { it.name.length }
.map {
ClientApiGenerator(config, schemaIndex).generate(it, methodNames)
ClientApiGenerator(config, schemaIndex).generate(it, methodNames, rootProjectionTypes)
}.fold(CodeGenResult.EMPTY) { result, next -> result.merge(next) }
} else {
CodeGenResult.EMPTY
Expand All @@ -402,17 +413,16 @@ class CodeGen private constructor(

private fun generateJavaClientEntitiesApi(definitions: Collection<Definition<*>>): CodeGenResult =
if (config.generateClientApi) {
val federatedDefinitions =
definitions
.asSequence()
.filterIsInstance<ObjectTypeDefinition>()
.filter { it.hasDirective("key") }
.toList()
ClientApiGenerator(config, schemaIndex).generateEntities(federatedDefinitions)
ClientApiGenerator(config, schemaIndex).generateEntities(federatedDefinitions(definitions))
} else {
CodeGenResult.EMPTY
}

private fun federatedDefinitions(definitions: Collection<Definition<*>>): List<ObjectTypeDefinition> =
definitions
.filterIsInstance<ObjectTypeDefinition>()
.filter { it.hasDirective("key") }

private fun generateJavaClientEntitiesRepresentations(definitions: Collection<Definition<*>>): CodeGenResult =
if (config.generateClientApi) {
val generatedRepresentations = mutableMapOf<String, Any>()
Expand Down Expand Up @@ -767,10 +777,7 @@ data class CodeGenResult(
javaEnumTypes = javaEnumTypes.concat(current.javaEnumTypes),
javaDataFetchers = javaDataFetchers.concat(current.javaDataFetchers),
javaQueryTypes = javaQueryTypes.concat(current.javaQueryTypes),
clientProjections =
clientProjections
.concat(current.clientProjections)
.distinctBy { it.packageName() to it.typeSpec().name() },
clientProjections = clientProjections.concat(current.clientProjections).distinctProjections(),
javaConstants = javaConstants.concat(current.javaConstants),
kotlinDataTypes = kotlinDataTypes.concat(current.kotlinDataTypes),
kotlinInputTypes = kotlinInputTypes.concat(current.kotlinInputTypes),
Expand Down Expand Up @@ -804,6 +811,28 @@ data class CodeGenResult(
.plus(kotlinClientTypes)
.toList()

/**
* Drops projections identical to an earlier one, like the `distinct()` used up to 8.7.0, but only renders files
* whose names collide: equality renders the source, and rendering every accumulated file on every merge made
* merging quadratic.
* The package is part of the key, so comparing the rendered type skips `JavaFile`'s import pass.
* Same-named projections that differ are kept; `CodeGen` writes the last one, as 8.7.0 did.
*/
private fun List<JavaFile>.distinctProjections(): List<JavaFile> {
val byName = HashMap<Pair<String, String>, MutableList<JavaFile>>()
val rendered = IdentityHashMap<JavaFile, String>()
val source = { file: JavaFile -> rendered.getOrPut(file) { file.typeSpec().toString() } }
return filter { file ->
val sameName = byName.getOrPut(file.packageName() to file.typeSpec().name()) { mutableListOf() }
if (sameName.any { it === file || source(it) == source(file) }) {
false
} else {
sameName.add(file)
true
}
}
}

private fun <T> List<T>.concat(other: List<T>): List<T> {
if (other.isEmpty()) {
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,24 @@ class ClientApiGenerator internal constructor(
fun generate(
definition: ObjectTypeDefinition,
methodNames: MutableSet<String>,
): CodeGenResult = generate(definition, methodNames, rootProjectionTypes(listOf(definition)))

/**
* Pass [rootProjectionTypes] computed over every operation, or same-named fields returning different types collide.
*/
internal fun generate(
definition: ObjectTypeDefinition,
methodNames: MutableSet<String>,
rootProjectionTypes: Map<String, String>,
): CodeGenResult =
definition.fieldDefinitions
.filterIncludedInConfig(definition.name, config)
.filterSkipped()
rootFields(definition)
.map {
val javaFile = createQueryClass(it, definition.name, methodNames)

val rootProjection =
it.type.findTypeDefinition(schemaIndex, true)?.let { typeDefinition ->
createRootProjection(typeDefinition, it.name.capitalized())
val prefix = rootProjectionPrefix(it.name.capitalized(), definition.name, typeDefinition, rootProjectionTypes)
createRootProjection(typeDefinition, prefix)
}
?: CodeGenResult.EMPTY
CodeGenResult(javaQueryTypes = listOf(javaFile)).merge(rootProjection)
Expand Down Expand Up @@ -434,6 +442,58 @@ class ClientApiGenerator internal constructor(
.addCode("""super(null, null, java.util.Optional.of("$typeName"));""")
.build()

/**
* Maps each unqualified root projection class name to the type it projects, given [operations] in generation order.
*
* Root projections are named after the operation field, so `Query.result: QueryResult` and
* `Mutation.result: MutationResult` both want `ResultProjectionRoot`.
* Up to 8.7.0 each operation generated its own, identical ones were deduplicated keeping the first and the last
* one written replaced the rest on disk.
* The type that won there keeps the unqualified name, so clients compiled against it still compile:
* `distinct()` reproduces the deduplication and `toMap()` the last write.
* [generateEntities] writes `EntitiesProjectionRoot` after every operation, so a Query field named `entities` loses
* it to [federatedTypes].
*/
internal fun rootProjectionTypes(
operations: List<ObjectTypeDefinition>,
federatedTypes: List<ObjectTypeDefinition> = emptyList(),
): Map<String, String> {
val rootProjectionTypes =
operations
.flatMap { operation ->
rootFields(operation).mapNotNull { field ->
field.type.findTypeDefinition(schemaIndex, true)?.let { "${field.name.capitalized()}ProjectionRoot" to it.name }
}
}.distinct()
.toMap()
return if (!config.skipEntityQueries && federatedTypes.isNotEmpty()) {
rootProjectionTypes + (ENTITIES_PROJECTION_ROOT to "_entities")
} else {
rootProjectionTypes
}
}

private fun rootFields(definition: ObjectTypeDefinition): List<FieldDefinition> =
definition.fieldDefinitions
.filterIncludedInConfig(definition.name, config)
.filterSkipped()

/**
* A field returning a different type than the unqualified root gets an operation-qualified one such as
* `ResultGraphQLQueryProjectionRoot`, mirroring the query class name from [generateMethodName].
*/
private fun rootProjectionPrefix(
fieldName: String,
operation: String,
type: TypeDefinition<*>,
rootProjectionTypes: Map<String, String>,
): String =
if (rootProjectionTypes["${fieldName}ProjectionRoot"] == type.name) {
fieldName
} else {
"${fieldName}GraphQL${operation.capitalized()}"
}

private fun createRootProjection(
type: TypeDefinition<*>,
prefix: String,
Expand Down Expand Up @@ -615,7 +675,7 @@ class ClientApiGenerator internal constructor(
}

private fun createEntitiesRootProjection(federatedTypes: List<ObjectTypeDefinition>): CodeGenResult {
val clazzName = "EntitiesProjectionRoot"
val clazzName = ENTITIES_PROJECTION_ROOT
val javaType =
createProjectionClass(clazzName)
.addMethod(createRootProjectionConstructor("_entities"))
Expand Down Expand Up @@ -922,4 +982,8 @@ class ClientApiGenerator internal constructor(
private fun getPackageName(): String = config.packageNameClient

private fun getDatatypesPackageName(): String = config.packageNameTypes

private companion object {
const val ENTITIES_PROJECTION_ROOT = "EntitiesProjectionRoot"
}
}
Loading
Loading