in graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/kotlin2/GenerateKotlin2ClientTypes.kt [49:250]
fun generateKotlin2ClientTypes(
config: CodeGenConfig,
document: Document,
): List<FileSpec> {
if (!config.generateClientApi) {
return emptyList()
}
val typeLookup = Kotlin2TypeLookup(config, document)
val ivsParameter =
ParameterSpec
.builder("inputValueSerializer", InputValueSerializerInterface::class.asTypeName().copy(nullable = true))
.defaultValue("null")
.build()
// create a projection class for every interface & data type
val dataProjections =
document
.getDefinitionsOfType(ObjectTypeDefinition::class.java)
.plus(document.getDefinitionsOfType(InterfaceTypeDefinition::class.java))
.excludeSchemaTypeExtension()
.filter { type -> type.directives.none { it.name == "skipcodegen" } && !typeLookup.isScalar(type.name) }
.map { typeDefinition ->
// get any fields defined via schema extensions
val extensionTypes = SchemaExtensionsUtils.findTypeExtensions(typeDefinition.name, document.definitions)
// the name of the type is used in every parameter & return value
val typeName = ClassName(config.packageNameClient, "${typeDefinition.name}Projection")
// get all fields defined on the type itself or any extension types
val fields =
listOf(typeDefinition)
.plus(extensionTypes)
.flatMap { it.fieldDefinitions }
.filterSkipped()
.filter(ReservedKeywordFilter.filterInvalidNames)
.map { field ->
val isScalar = typeLookup.isScalar(field.type)
val hasArgs = field.inputValueDefinitions.isNotEmpty()
when {
// scalars without args are just parameters that note the field is requested
isScalar && !hasArgs -> {
PropertySpec
.builder(
name = field.name,
type = typeName,
).getter(
FunSpec
.getterBuilder()
.addStatement("field(%S)", field.name)
.addStatement("return this")
.build(),
).build()
}
// scalars with args are functions to take the args with no projection
isScalar && hasArgs -> {
FunSpec
.builder(field.name)
.addInputArgs(config, typeLookup, typeName, field.inputValueDefinitions)
.returns(typeName)
.addCode(
field.inputValueDefinitions.let { iv ->
val builder = CodeBlock.builder().add("field(%S", field.name)
iv.forEach { d -> builder.add(", %S to %N", d.name, d.name) }
builder.add(")\nreturn this").build()
},
).build()
}
// otherwise it's a projection with optional args
// !isScalar && hasArgs
else -> {
val projectionTypeName = projectionTypeName(field.type)
val (projectionType, projection) = projectionType(config.packageNameClient, projectionTypeName)
FunSpec
.builder(field.name)
.addInputArgs(config, typeLookup, typeName, field.inputValueDefinitions)
.addParameter(
ParameterSpec
.builder(
"_alias",
String::class.asTypeName().copy(nullable = true),
).defaultValue("null")
.build(),
).addParameter(projection)
.returns(typeName)
.addCode(
field.inputValueDefinitions.let { iv ->
val builder =
CodeBlock.builder().add(
"field(_alias, %S, %T(inputValueSerializer), _projection",
field.name,
projectionType,
)
iv.forEach { d -> builder.add(", %S to %N", d.name, d.name) }
builder.add(")\nreturn this").build()
},
).build()
}
}
}
// add the `... on XXX` projection for implementors of this interface
val implementors =
typeLookup
.interfaceImplementors(typeDefinition.name)
.map { subclassName -> onSubclassProjection(config.packageNameClient, typeName, subclassName) }
// create the projection class
val typeSpec =
TypeSpec
.classBuilder(typeName)
.addOptionalGeneratedAnnotation(config)
.primaryConstructor(FunSpec.constructorBuilder().addParameter(ivsParameter).build())
.superclass(GraphQLProjection::class)
.addSuperclassConstructorParameter("inputValueSerializer")
// we can't ask for `__typename` on a `Subscription` object
.apply {
if (typeDefinition.name == "Subscription") {
addSuperclassConstructorParameter("defaultFields = emptySet()")
}
}.addProperties(fields.filterIsInstance<PropertySpec>())
.addFunctions(fields.filterIsInstance<FunSpec>())
.addFunctions(implementors)
.build()
// return a file per type
FileSpec.get(config.packageNameClient, typeSpec)
}
// create a projection for each union
val unionProjections =
document
.getDefinitionsOfType(UnionTypeDefinition::class.java)
.excludeSchemaTypeExtension()
.filter { !it.shouldSkip(config) }
.map { unionDefinition ->
// the name of the type is used in every parameter & return value
val typeName = ClassName(config.packageNameClient, "${unionDefinition.name}Projection")
// get any members defined via schema extensions
val extensionTypes = SchemaExtensionsUtils.findUnionExtensions(unionDefinition.name, document.definitions)
val implementations =
unionDefinition.memberTypes
.plus(extensionTypes.flatMap { it.memberTypes })
val typeSpec =
TypeSpec
.classBuilder(typeName)
.addOptionalGeneratedAnnotation(config)
.primaryConstructor(FunSpec.constructorBuilder().addParameter(ivsParameter).build())
.superclass(GraphQLProjection::class)
.addSuperclassConstructorParameter("inputValueSerializer")
.addFunctions(
implementations.map { subclass ->
onSubclassProjection(config.packageNameClient, typeName, (subclass as NamedNode<*>).name)
},
).build()
// return a file per type
FileSpec.get(config.packageNameClient, typeSpec)
}
// create a top-level client class
val topLevelTypes = typeLookup.operations.filterKeys { typeLookup.objectTypeNames.contains(it) }
val clientSpec =
TypeSpec
.objectBuilder("DgsClient")
.addOptionalGeneratedAnnotation(config)
.addFunctions(
topLevelTypes.map { (type, op) ->
val (projectionType, projection) = projectionType(config.packageNameClient, type)
FunSpec
.builder("build$type")
.addParameter(ivsParameter)
.addParameter(projection)
.returns(String::class)
.addStatement(
"""return %T.asQuery(%T.%L, %T(inputValueSerializer), _projection)""",
GraphQLProjection::class.asTypeName(),
op::class.asTypeName(),
op.name,
projectionType,
).build()
},
).build()
val clientFile = FileSpec.get(config.packageName, clientSpec)
return dataProjections.plus(unionProjections).plus(clientFile)
}