From 51072186f2d60f1597ec60b9d7bfa910402fa091 Mon Sep 17 00:00:00 2001 From: Ryan Flegel Date: Fri, 28 Aug 2026 17:15:16 -0600 Subject: [PATCH] Generate fragments for interfaces --- .../generators/java/ClientApiGenerator.kt | 80 +++++++++---------- .../clientapi/ClientApiGenFragmentTest.kt | 60 ++++++++++++++ 2 files changed, 99 insertions(+), 41 deletions(-) diff --git a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGenerator.kt b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGenerator.kt index 15373de62..e145d40e4 100644 --- a/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGenerator.kt +++ b/graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGenerator.kt @@ -523,11 +523,10 @@ class ClientApiGenerator( } } - val concreteTypesResult = createConcreteTypes(type, javaType.build(), javaType, mutableSetOf(), 0) - val unionTypesResult = createUnionTypes(type, javaType, javaType.build(), mutableSetOf(), 0) + val fragmentTypesResult = addFragmentProjectionMethods(type, javaType.build(), javaType, mutableSetOf(), 0) val javaFile = JavaFile.builder(getPackageName(), javaType.build()).build() - return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult).merge(concreteTypesResult).merge(unionTypesResult) + return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult).merge(fragmentTypesResult) } private fun addFieldSelectionMethodWithArguments( @@ -674,44 +673,17 @@ class ClientApiGenerator( return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult) } - private fun createConcreteTypes( + private fun addFragmentProjectionMethods( type: TypeDefinition<*>, root: TypeSpec, javaType: TypeSpec.Builder, processedEdges: Set>, queryDepth: Int, ): CodeGenResult = - if (type is InterfaceTypeDefinition) { - val concreteTypes = - document - .getDefinitionsOfType(ObjectTypeDefinition::class.java) - .filter { - it.implements.filterIsInstance>().any { iface -> iface.name == type.name } - }.distinctBy { it.name } - concreteTypes - .map { - addFragmentProjectionMethod(javaType, root, it, processedEdges, queryDepth) - }.fold(CodeGenResult.EMPTY) { total, current -> total.merge(current) } - } else { - CodeGenResult.EMPTY - } - - private fun createUnionTypes( - type: TypeDefinition<*>, - javaType: TypeSpec.Builder, - rootType: TypeSpec, - processedEdges: Set>, - queryDepth: Int, - ): CodeGenResult = - if (type is UnionTypeDefinition) { - val memberTypes = type.memberTypes.mapNotNull { it.findTypeDefinition(document, true) }.toList() - memberTypes - .map { - addFragmentProjectionMethod(javaType, rootType, it, processedEdges, queryDepth) - }.fold(CodeGenResult.EMPTY) { total, current -> total.merge(current) } - } else { - CodeGenResult.EMPTY - } + getFragmentTypes(type) + .map { + addFragmentProjectionMethod(javaType, root, it, processedEdges, queryDepth) + }.fold(CodeGenResult.EMPTY) { total, current -> total.merge(current) } private fun addFragmentProjectionMethod( javaType: TypeSpec.Builder, @@ -740,18 +712,18 @@ class ClientApiGenerator( ).build(), ) - return createFragment(it as ObjectTypeDefinition, rootType, projectionName, processedEdges, queryDepth) + return createFragment(it, rootType, projectionName, processedEdges, queryDepth) } private fun createFragment( - type: ObjectTypeDefinition, + type: TypeDefinition<*>, root: TypeSpec, prefix: String, processedEdges: Set>, queryDepth: Int, ): CodeGenResult { val subProjection = - createSubProjectionType(type, root, prefix, processedEdges, queryDepth) + createSubProjectionType(type, root, prefix, processedEdges, queryDepth, fragment = true) ?: return CodeGenResult.EMPTY val javaType = subProjection.first val codeGenResult = subProjection.second @@ -794,6 +766,27 @@ class ClientApiGenerator( return CodeGenResult(clientProjections = listOf(javaFile)).merge(codeGenResult) } + private fun getFragmentTypes(type: TypeDefinition<*>): List> = + when (type) { + is InterfaceTypeDefinition -> { + document + .getDefinitionsOfType(InterfaceTypeDefinition::class.java) + .plus(document.getDefinitionsOfType(ObjectTypeDefinition::class.java)) + .filter { + it.implements.filterIsInstance>().any { iface -> iface.name == type.name } + }.distinctBy { it.name } + } + + is UnionTypeDefinition -> { + type.memberTypes + .mapNotNull { it.findTypeDefinition(document, true) } + } + + else -> { + emptyList() + } + } + private fun createSubProjection( type: TypeDefinition<*>, root: TypeSpec, @@ -817,6 +810,7 @@ class ClientApiGenerator( prefix: String, processedEdges: Set>, queryDepth: Int, + fragment: Boolean = false, ): Pair? { val className = ClassName.get(BaseSubProjectionNode::class.java) val clazzName = "${prefix}Projection" @@ -959,10 +953,14 @@ class ClientApiGenerator( } } - val concreteTypesResult = createConcreteTypes(type, root, javaType, processedEdges, queryDepth) - val unionTypesResult = createUnionTypes(type, javaType, root, processedEdges, queryDepth) + val fragmentTypesResult = + if (!fragment) { + addFragmentProjectionMethods(type, root, javaType, processedEdges, queryDepth) + } else { + CodeGenResult.EMPTY + } - return javaType to codeGenResult.merge(concreteTypesResult).merge(unionTypesResult) + return javaType to codeGenResult.merge(fragmentTypesResult) } private fun getDeprecateDirective(node: DirectivesContainer<*>): Directive? { diff --git a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt index 890c24833..71af1637d 100644 --- a/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt +++ b/graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt @@ -145,6 +145,66 @@ class ClientApiGenFragmentTest { ) } + @Test + fun interfaceFragmentWithInterfaceHierarchy() { + val schema = + """ + type Query { + searchForSeries(title: String): [Series] + } + + interface Show { + title: String + relatedShows: [Show] + } + + interface Series implements Show { + title: String + relatedShows: [Show] + } + + interface MiniSeries implements Series & Show { + title: String + relatedShows: [Show] + episodes: Int + } + """.trimIndent() + + val codeGenResult = + CodeGen( + CodeGenConfig( + schemas = setOf(schema), + packageName = BASE_PACKAGE_NAME, + generateClientApi = true, + ), + ).generate() + + assertThat(codeGenResult.clientProjections.size).isEqualTo(4) + assertThat(codeGenResult.clientProjections[0].typeSpec().name()).isEqualTo("SearchForSeriesProjectionRoot") + assertThat(codeGenResult.clientProjections[0].typeSpec().methodSpecs()).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[0].typeSpec().methodSpecs()).extracting("name").contains("__typename") + assertThat(codeGenResult.clientProjections[0].typeSpec().methodSpecs()).extracting("name").contains("onMiniSeries") + assertThat(codeGenResult.clientProjections[0].typeSpec().methodSpecs()).extracting("name").doesNotContain("onSeries") + assertThat(codeGenResult.clientProjections[1].typeSpec().name()).isEqualTo("ShowProjection") + assertThat(codeGenResult.clientProjections[1].typeSpec().methodSpecs()).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[1].typeSpec().methodSpecs()).extracting("name").contains("relatedShows") + assertThat(codeGenResult.clientProjections[1].typeSpec().methodSpecs()).extracting("name").contains("onSeries") + assertThat(codeGenResult.clientProjections[1].typeSpec().methodSpecs()).extracting("name").contains("onMiniSeries") + assertThat(codeGenResult.clientProjections[1].typeSpec().methodSpecs()).extracting("name").doesNotContain("onShow") + assertThat(codeGenResult.clientProjections[2].typeSpec().name()).isEqualTo("SeriesFragmentProjection") + assertThat(codeGenResult.clientProjections[2].typeSpec().methodSpecs()).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[2].typeSpec().methodSpecs()).extracting("name").contains("relatedShows") + assertThat(codeGenResult.clientProjections[2].typeSpec().methodSpecs()).extracting("name").doesNotContain("onMiniSeries") + assertThat(codeGenResult.clientProjections[3].typeSpec().name()).isEqualTo("MiniSeriesFragmentProjection") + assertThat(codeGenResult.clientProjections[3].typeSpec().methodSpecs()).extracting("name").contains("title") + assertThat(codeGenResult.clientProjections[3].typeSpec().methodSpecs()).extracting("name").contains("relatedShows") + + assertCompilesJava( + codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + + codeGenResult.javaInterfaces, + ) + } + @Test fun unionFragment() { val schema =