Skip to content

Commit

Permalink
Client Generation - Allow schema to be on the classpath (#1136)
Browse files Browse the repository at this point in the history
* Client Generation - Allow schema SDL to be on the classpath

* Suggestions and Refactors

* Minor touchup

* More improvements to the code, remove redundant edits
  • Loading branch information
AlexRiedler authored Apr 30, 2021
1 parent 6a262aa commit edad0b4
Show file tree
Hide file tree
Showing 10 changed files with 60 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import com.expediagroup.graphql.plugin.client.generator.GraphQLClientGeneratorCo
import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
import com.expediagroup.graphql.plugin.client.generator.GraphQLScalar
import com.squareup.kotlinpoet.FileSpec
import graphql.schema.idl.SchemaParser
import java.io.File

/**
Expand All @@ -32,7 +31,7 @@ fun generateClient(
allowDeprecated: Boolean = false,
customScalarsMap: List<GraphQLScalar> = emptyList(),
serializer: GraphQLSerializer = GraphQLSerializer.JACKSON,
schema: File,
schemaPath: String,
queries: List<File>
): List<FileSpec> {
val customScalars = customScalarsMap.associateBy { it.scalar }
Expand All @@ -42,7 +41,6 @@ fun generateClient(
customScalarMap = customScalars,
serializer = serializer
)
val graphQLSchema = SchemaParser().parse(schema)
val generator = GraphQLClientGenerator(graphQLSchema, config)
val generator = GraphQLClientGenerator(schemaPath, config)
return generator.generate(queries)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.expediagroup.graphql.plugin.client.generator

import com.expediagroup.graphql.plugin.client.generator.exceptions.MultipleOperationsInFileException
import com.expediagroup.graphql.plugin.client.generator.exceptions.SchemaUnavailableException
import com.expediagroup.graphql.plugin.client.generator.types.generateGraphQLObjectTypeSpec
import com.expediagroup.graphql.plugin.client.generator.types.generateVariableTypeSpec
import com.squareup.kotlinpoet.ClassName
Expand All @@ -31,6 +32,7 @@ import com.squareup.kotlinpoet.TypeSpec
import graphql.language.ObjectTypeDefinition
import graphql.language.OperationDefinition
import graphql.parser.Parser
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import kotlinx.serialization.Serializable
import java.io.File
Expand All @@ -41,12 +43,17 @@ private const val CORE_TYPES_PACKAGE = "com.expediagroup.graphql.client.types"
* GraphQL client code generator that uses [KotlinPoet](https://github.com/square/kotlinpoet) to generate Kotlin classes based on the specified GraphQL queries.
*/
class GraphQLClientGenerator(
private val graphQLSchema: TypeDefinitionRegistry,
schemaPath: String,
private val config: GraphQLClientGeneratorConfig
) {
private val documentParser: Parser = Parser()
private val typeAliases: MutableMap<String, TypeAliasSpec> = mutableMapOf()
private val sharedTypes: MutableMap<ClassName, List<TypeSpec>> = mutableMapOf()
private val graphQLSchema: TypeDefinitionRegistry

init {
graphQLSchema = parseSchema(schemaPath)
}

/**
* Generate GraphQL clients for the specified queries.
Expand Down Expand Up @@ -196,6 +203,16 @@ class GraphQLClientGenerator(
val rootType = operationNames[operationDefinition.operation.name]
return graphQLSchema.getType(rootType).get() as ObjectTypeDefinition
}

private fun parseSchema(path: String): TypeDefinitionRegistry {
val schemaFile = File(path)
return if (schemaFile.isFile) {
SchemaParser().parse(schemaFile)
} else {
val schemaInputStream = this.javaClass.classLoader.getResourceAsStream(path) ?: throw SchemaUnavailableException(path)
SchemaParser().parse(schemaInputStream)
}
}
}

internal fun String.toUpperUnderscore(): String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.expediagroup.graphql.plugin.client.generator.exceptions

/**
* Exception thrown when specified schema file path is not found or unavailable
*/
internal class SchemaUnavailableException(schemaPath: String) : RuntimeException("Specified schema file=$schemaPath does not exist")
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.expediagroup.graphql.plugin.client.generator

import com.expediagroup.graphql.plugin.client.generator.exceptions.SchemaUnavailableException
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
Expand All @@ -31,13 +33,21 @@ class GenerateInvalidClientIT {
val (queries, _) = locateTestFiles(testDirectory)
val expectedException = File(testDirectory, "exception.txt").readText().trim()

val generator = GraphQLClientGenerator(testSchema(), defaultConfig)
val generator = GraphQLClientGenerator(TEST_SCHEMA_PATH, defaultConfig)
val exception = assertFails {
generator.generate(queries)
}
assertEquals(expectedException, exception::class.simpleName)
}

@Test
fun `verify an invalid schema path will raise an exception`() {
val exception = assertFails {
GraphQLClientGenerator("missingSchema.graphql", defaultConfig)
}
assertEquals(SchemaUnavailableException::class, exception::class)
}

companion object {
@JvmStatic
fun invalidTests(): List<Arguments> = locateTestCaseArguments("src/test/data/invalid")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ package com.expediagroup.graphql.plugin.client.generator
import com.expediagroup.graphql.client.converter.ScalarConverter
import com.tschuchort.compiletesting.KotlinCompilation
import com.tschuchort.compiletesting.SourceFile
import graphql.schema.idl.SchemaParser
import graphql.schema.idl.TypeDefinitionRegistry
import org.junit.jupiter.params.provider.Arguments
import java.io.File
import java.util.UUID
Expand Down Expand Up @@ -52,17 +50,12 @@ internal fun locateTestFiles(directory: File): Pair<List<File>, Map<String, File
return queries to expectedFiles
}

internal fun testSchema(): TypeDefinitionRegistry {
val schemaFileStream = ClassLoader.getSystemClassLoader().getResourceAsStream("testSchema.graphql") ?: throw RuntimeException("unable to locate test schema")
return schemaFileStream.use {
SchemaParser().parse(schemaFileStream)
}
}
internal const val TEST_SCHEMA_PATH = "testSchema.graphql"

internal fun verifyClientGeneration(config: GraphQLClientGeneratorConfig, testDirectory: File) {
val (queries, expectedFiles) = locateTestFiles(testDirectory)

val generator = GraphQLClientGenerator(testSchema(), config)
val generator = GraphQLClientGenerator(TEST_SCHEMA_PATH, config)
val fileSpecs = generator.generate(queries)
assertTrue(fileSpecs.isNotEmpty())
assertEquals(expectedFiles.size, fileSpecs.size)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ abstract class GenerateClientAction : WorkAction<GenerateClientParameters> {
val allowDeprecated = parameters.allowDeprecated.get()
val customScalarMap = parameters.customScalars.get().map { GraphQLScalar(it.scalar, it.type, it.converter) }
val serializer = GraphQLSerializer.valueOf(parameters.serializer.get().name)
val schemaFile = parameters.schemaFile.get()
val schemaPath = parameters.schemaPath.get()
val queryFiles = parameters.queryFiles.get()
val targetDirectory = parameters.targetDirectory.get()

generateClient(targetPackage, allowDeprecated, customScalarMap, serializer, schemaFile, queryFiles).forEach {
generateClient(targetPackage, allowDeprecated, customScalarMap, serializer, schemaPath, queryFiles).forEach {
it.writeTo(targetDirectory)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ interface GenerateClientParameters : WorkParameters {
val customScalars: ListProperty<GraphQLScalar>
/** Type of JSON serializer that will be used to generate the data classes. */
val serializer: Property<GraphQLSerializer>
/** GraphQL schema file that will be used to generate client code. */
val schemaFile: Property<File>
/** GraphQL schema file path that will be used to generate client code. */
val schemaPath: Property<String>
/** List of query files that will be processed to generate HTTP clients. */
val queryFiles: ListProperty<File>
/** Directory where to save the generated source files. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,11 @@ abstract class AbstractGenerateClientTask : DefaultTask() {
fun generateGraphQLClientAction() {
logger.debug("generating GraphQL client")

val graphQLSchema = when {
schemaFile.isPresent -> schemaFile.get().asFile
schemaFileName.isPresent -> File(schemaFileName.get())
val graphQLSchemaPath = when {
schemaFile.isPresent -> schemaFile.get().asFile.path
schemaFileName.isPresent -> schemaFileName.get()
else -> throw RuntimeException("schema not available")
}
if (!graphQLSchema.isFile) {
throw RuntimeException("specified schema file does not exist")
}

val targetPackage = packageName.orNull ?: throw RuntimeException("package not specified")
val targetQueryFiles: List<File> = when {
Expand All @@ -174,7 +171,7 @@ abstract class AbstractGenerateClientTask : DefaultTask() {
throw RuntimeException("failed to generate generated source directory = $targetDirectory")
}

logConfiguration(graphQLSchema, targetQueryFiles)
logConfiguration(graphQLSchemaPath, targetQueryFiles)
val workQueue: WorkQueue = getWorkerExecutor().classLoaderIsolation { workerSpec: ClassLoaderWorkerSpec ->
workerSpec.classpath.from(pluginClasspath)
logger.debug("worker classpath: \n${workerSpec.classpath.files.joinToString("\n")}")
Expand All @@ -185,17 +182,17 @@ abstract class AbstractGenerateClientTask : DefaultTask() {
parameters.allowDeprecated.set(allowDeprecatedFields)
parameters.customScalars.set(customScalars)
parameters.serializer.set(serializer)
parameters.schemaFile.set(graphQLSchema)
parameters.schemaPath.set(graphQLSchemaPath)
parameters.queryFiles.set(targetQueryFiles)
parameters.targetDirectory.set(targetDirectory)
}
workQueue.await()
logger.debug("successfully generated GraphQL HTTP client")
}

private fun logConfiguration(schema: File, queryFiles: List<File>) {
private fun logConfiguration(schemaPath: String, queryFiles: List<File>) {
logger.debug("GraphQL Client generator configuration:")
logger.debug(" schema file = ${schema.path}")
logger.debug(" schema file = $schemaPath")
logger.debug(" queries")
queryFiles.forEach {
logger.debug(" - ${it.name}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ abstract class GraphQLDownloadSDLTask : DefaultTask() {
}

/**
* Download schema in SDL format from the specified endpoint and sve it locally in the target output file.
* Download schema in SDL format from the specified endpoint and save it locally in the target output file.
*/
@TaskAction
fun downloadSDLAction() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
package com.expediagroup.graphql.plugin.maven

import com.expediagroup.graphql.plugin.client.generateClient
import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
import com.expediagroup.graphql.plugin.client.generator.GraphQLScalar
import com.expediagroup.graphql.plugin.client.generator.GraphQLSerializer
import org.apache.maven.plugin.AbstractMojo
import org.apache.maven.plugins.annotations.Parameter
import org.apache.maven.project.MavenProject
Expand All @@ -39,7 +39,7 @@ abstract class GenerateClientAbstractMojo : AbstractMojo() {
* GraphQL schema file that will be used to generate client code.
*/
@Parameter(defaultValue = "\${graphql.schemaFile}", name = "schemaFile")
private var schemaFile: File? = null
private var schemaFile: String? = null

/**
* Target package name for generated code.
Expand Down Expand Up @@ -99,17 +99,18 @@ abstract class GenerateClientAbstractMojo : AbstractMojo() {

override fun execute() {
log.debug("generating GraphQL client")
val graphQLSchemaFile = schemaFile ?: File(project.build.directory, "schema.graphql")
validateGraphQLSchemaExists(graphQLSchemaFile)

val schemaPath = schemaFile ?: File(project.build.directory, "schema.graphql").path

val targetQueryFiles: List<File> = locateQueryFiles(queryFiles, queryFileDirectory)

if (!outputDirectory.isDirectory && !outputDirectory.mkdirs()) {
throw RuntimeException("failed to generate generated source directory")
}

logConfiguration(graphQLSchemaFile, targetQueryFiles)
logConfiguration(schemaPath, targetQueryFiles)
val customGraphQLScalars = customScalars.map { GraphQLScalar(it.scalar, it.type, it.converter) }
generateClient(packageName, allowDeprecatedFields, customGraphQLScalars, serializer, graphQLSchemaFile, targetQueryFiles).forEach {
generateClient(packageName, allowDeprecatedFields, customGraphQLScalars, serializer, schemaPath, targetQueryFiles).forEach {
it.writeTo(outputDirectory)
}

Expand All @@ -125,17 +126,11 @@ abstract class GenerateClientAbstractMojo : AbstractMojo() {
return targetQueryFiles
}

private fun validateGraphQLSchemaExists(graphQLSchemaFile: File) {
if (!graphQLSchemaFile.isFile) {
throw RuntimeException("specified GraphQL schema is not a file, ${graphQLSchemaFile.path}")
}
}

abstract fun configureProjectWithGeneratedSources(mavenProject: MavenProject, generatedSourcesDirectory: File)

private fun logConfiguration(graphQLSchemaFile: File, queryFiles: List<File>) {
private fun logConfiguration(graphQLSchemaFilePath: String, queryFiles: List<File>) {
log.debug("GraphQL Client generator configuration:")
log.debug(" schema file = ${graphQLSchemaFile.path}")
log.debug(" schema file = $graphQLSchemaFilePath")
log.debug(" queries")
queryFiles.forEach {
log.debug(" - ${it.name}")
Expand Down

0 comments on commit edad0b4

Please sign in to comment.