Skip to content

Commit

Permalink
Merge pull request #446 from commercetools/graphql-module
Browse files Browse the repository at this point in the history
  • Loading branch information
jenschude authored Jun 1, 2023
2 parents 71a36aa + 8e7153a commit e613a40
Show file tree
Hide file tree
Showing 26 changed files with 14,557 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ dependencies {
* `commercetools-sdk-compat-v1`: Compatibility layer for Java v1 SDK
* `commercetools-money`: Provider for JSR-354 money instances
* `commercetools-monitoring-newrelic`: Middleware to integrate NewRelic monitoring
* `commercetools-graphql-api`: type safe support for the commercetools GraphQL API

### Migration Guidelines
To migrate from the 1.x to the 2.x, there is a guideline below:
Expand Down
8 changes: 8 additions & 0 deletions allowed-licenses.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@
"moduleLicense": null,
"moduleName": "com.squareup.okio:okio"
},
{
"moduleLicense": null,
"moduleName": "com.netflix.graphql.dgs:graphql-dgs-platform"
},
{
"moduleLicense": null,
"moduleName": "com.netflix.graphql.dgs:graphql-dgs-platform-dependencies"
},
{
"moduleLicense": "GNU GENERAL PUBLIC LICENSE, Version 2 + Classpath Exception",
"moduleName": "javax.annotation:javax.annotation-api"
Expand Down
1 change: 1 addition & 0 deletions buildSrc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ repositories {
dependencies {
implementation "com.github.javaparser:javaparser-symbol-solver-core:3.25.2"
implementation "com.google.code.gson:gson:2.10.1"
implementation 'com.squareup:javapoet:1.13.0'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.commercetools.sdk.plugins

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.JavaPlugin
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.SourceSet

class GraphQLPlugin : Plugin<Project> {
companion object {
const val GRADLE_GROUP = "DGS GraphQL Codegen"
}

override fun apply(project: Project) {

project.plugins.apply(JavaPlugin::class.java)

val generateJavaTaskProvider = project.tasks.register("generateGraphQLRoots", GraphQLTask::class.java)
generateJavaTaskProvider.configure { it.group = GRADLE_GROUP }

val javaExtension = project.extensions.getByType(JavaPluginExtension::class.java)

val sourceSets = javaExtension.sourceSets
val mainSourceSet = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
val outputDir = generateJavaTaskProvider.map(GraphQLTask::getOutputDir)
mainSourceSet.java.srcDirs(project.files(outputDir).builtBy(generateJavaTaskProvider))

}
}
209 changes: 209 additions & 0 deletions buildSrc/src/main/kotlin/com/commercetools/sdk/plugins/GraphQLTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
package com.commercetools.sdk.plugins

import com.github.javaparser.StaticJavaParser
import com.github.javaparser.ast.CompilationUnit
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
import com.github.javaparser.ast.body.ConstructorDeclaration
import com.github.javaparser.ast.body.MethodDeclaration
import com.github.javaparser.ast.body.TypeDeclaration
import com.squareup.javapoet.*
import org.gradle.api.DefaultTask
import org.gradle.api.plugins.JavaPluginExtension
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.nio.file.Paths
import java.util.*
import java.util.function.UnaryOperator
import javax.lang.model.element.Modifier


open class GraphQLTask: DefaultTask() {
@Input
var generatedSourcesDir: String = project.buildDir.absolutePath

@Input
var packageName: String = "com.commercetools.graphql.api"

@Input
var clientPackageName: String = "$packageName.client"

@Input
var typesPackageName: String = "$packageName.types"

@OutputDirectory
fun getOutputDir(): File {
return Paths.get("$generatedSourcesDir/generated/sources/coco-codegen").toFile()
}

@TaskAction
fun generate() {
val javaExtension = project.extensions.getByType(JavaPluginExtension::class.java)

val mainSourceSet = javaExtension.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
val queryClasses = mainSourceSet.java.asFileTree.matching { it.include("**/*GraphQLQuery.java") }.files

val graphQL = TypeSpec.classBuilder("GraphQL")
.addModifiers(Modifier.PUBLIC)

val graphQLData = TypeSpec.interfaceBuilder("GraphQLData")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(AnnotationSpec.builder(ClassName.get("com.fasterxml.jackson.databind.annotation", "JsonDeserialize"))
.addMember("as", "GraphQLDataImpl.class")
.build())
.addMethod(MethodSpec.methodBuilder("get")
.addModifiers(Modifier.PUBLIC, Modifier.DEFAULT)
.addParameter(ParameterSpec.builder(
ParameterizedTypeName.get(
ClassName.get(packageName, "GraphQLRequest"),
TypeVariableName.get("T")
),
"request").build())
.returns(TypeVariableName.get("T"))
.addTypeVariable(TypeVariableName.get("T"))
.addCode("return request.getDataMapper().apply(this);")
.build())
val graphQLDataImpl = TypeSpec.classBuilder("GraphQLDataImpl")
.addSuperinterface(ClassName.get(packageName, "GraphQLData"))
.addModifiers(Modifier.PUBLIC)


val wildcard: TypeName = WildcardTypeName.subtypeOf(Any::class.java)

queryClasses.forEach {
val queryName = it.nameWithoutExtension
val operation = queryName.replace("GraphQLQuery", "");
val projectionName = "${operation}ProjectionRoot"
val projectionFile = it.parentFile.resolve("$projectionName.java")
val operationName = operationName(it);
val returnType = returnType(projectionFile)

graphQL.addMethod(MethodSpec.methodBuilder(operationName)
.addModifiers(Modifier.PUBLIC)
.addModifiers(Modifier.STATIC)
.addParameter(ParameterSpec.builder(
ParameterizedTypeName.get(
ClassName.get(UnaryOperator::class.java),
ClassName.get(clientPackageName, queryName).nestedClass("Builder")
),
"query").build()
)
.addCode(CodeBlock.builder().addStatement("return new GraphQLQueryRequestBuilder<>(query.apply($queryName.newRequest()).build(), new $projectionName<>(), GraphQLData::get${operationName.firstUpperCase()})").build())
.returns(ParameterizedTypeName.get(
ClassName.get(packageName, "GraphQLQueryRequestBuilder"),
ClassName.get(typesPackageName, returnType),
ParameterizedTypeName.get(
ClassName.get(clientPackageName, projectionName),
wildcard,
wildcard
)
))
.build())

graphQLData.addMethod(MethodSpec.methodBuilder("get${operationName.firstUpperCase()}")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addAnnotation(AnnotationSpec.builder(ClassName.get("com.fasterxml.jackson.annotation", "JsonProperty"))
.addMember("value", "\"$operationName\"")
.build()
)
.returns(ClassName.get(typesPackageName, returnType))
.build())
graphQLData.addMethod(MethodSpec.methodBuilder("set${operationName.firstUpperCase()}")
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
.addParameter(ClassName.get(typesPackageName, returnType), operationName)
.build())

graphQLDataImpl.addField(
FieldSpec.builder(ClassName.get(typesPackageName, returnType), operationName, Modifier.PRIVATE).build()
)
graphQLDataImpl.addMethod(MethodSpec.methodBuilder("get${operationName.firstUpperCase()}")
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(typesPackageName, returnType))
.addCode("return $operationName;")
.build())
graphQLDataImpl.addMethod(MethodSpec.methodBuilder("set${operationName.firstUpperCase()}")
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(typesPackageName, returnType), operationName)
.addCode("this.$operationName = $operationName;")
.build())
}

val graphQLFile = JavaFile.builder("com.commercetools.graphql.api", graphQL.build()).build()
graphQLFile.writeTo(Paths.get("${getOutputDir()}"))

val graphQLDataFile = JavaFile.builder("com.commercetools.graphql.api", graphQLData.build()).build()
graphQLDataFile.writeTo(Paths.get("${getOutputDir()}"))
val graphQLDataImplFile = JavaFile.builder("com.commercetools.graphql.api", graphQLDataImpl.build()).build()
graphQLDataImplFile.writeTo(Paths.get("${getOutputDir()}"))

logger.info("Processing schema files: $graphQLFile")
}

private fun returnType(file: File): String {
val parse: CompilationUnit = StaticJavaParser.parse(file)

val declarations = parse
.types
.filterIsInstance<ClassOrInterfaceDeclaration>()
.map { typeDeclaration: TypeDeclaration<*> -> typeDeclaration as ClassOrInterfaceDeclaration }
.filterNot { it.isInterface }
.map { it.defaultConstructor }

return declarations.filter { it.isPresent }
.map { it.get() }
.map { it.methodBody() }
.first()
.substringAfterLast("Optional.of(\"")
.substringBefore("\")")
}

private fun operationName(file: File): String {
val parse: CompilationUnit = StaticJavaParser.parse(file)

val declarations = parse
.types
.filterIsInstance<ClassOrInterfaceDeclaration>()
.map { typeDeclaration: TypeDeclaration<*> -> typeDeclaration as ClassOrInterfaceDeclaration }
.filterNot { it.isInterface }
.flatMap { it.getMethodsByName("getOperationName") }

return declarations
.map { it.methodBody() }
.first()
.substringAfterLast("return \"")
.substringBefore("\"")
}

fun ConstructorDeclaration.methodBody(): String {
val methodBody = this.body
val bodyRange = methodBody.tokenRange.get().toString()
return bodyRange.substring(1, bodyRange.length - 1).trimIndent()
}

fun MethodDeclaration.methodBody(): String {
val methodBody = this.body
if (!methodBody.isPresent) {
return ""
}
val bodyRange = methodBody.get().tokenRange.get().toString()
return bodyRange.substring(1, bodyRange.length - 1).trimIndent()
}

private fun String.firstLowerCase(): String {
return this.replaceFirstChar {
it.lowercase(
Locale.getDefault()
)
}
}

private fun String.firstUpperCase(): String {
return this.replaceFirstChar {
it.uppercase(
Locale.getDefault()
)
}
}
}
40 changes: 40 additions & 0 deletions commercetools/commercetools-graphql-api/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import com.commercetools.sdk.plugins.GraphQLPlugin

plugins {
id "com.netflix.dgs.codegen" version "5.10.0"
}

apply plugin:'com.netflix.dgs.codegen'
apply plugin: GraphQLPlugin

dependencies {
api project(':commercetools:commercetools-sdk-java-api')
testImplementation project(':commercetools:commercetools-http-client')
implementation google.findbugs
implementation javax.validation
api 'com.netflix.graphql.dgs.codegen:graphql-dgs-codegen-shared-core:5.10.0'
}

generateGraphQLRoots {
dependsOn "generateJava"
}

generateJava {
schemaPaths = ["${projectDir}/src/main/resources/graphql"] // List of directories containing schema files
packageName = 'com.commercetools.graphql.api' // The package name to use to generate sources
generateBoxedTypes = true
generateClientv2 = true
shortProjectionNames = true

typeMapping = [
"Country": "String",
"Locale": "String",
"Long": "Long",
"Json": "com.fasterxml.jackson.databind.JsonNode",
"KeyReferenceInput": "String",
"Set": "Object",
"Time": "String",
"BigDecimal": "Double",
"YearMonth": "String",
]
}
Loading

0 comments on commit e613a40

Please sign in to comment.