Skip to content

Commit 403603c

Browse files
authored
feat: new GraalVM reflect metadata generator (#1739)
* feat: new GraalVM reflect metadata generator Creates new `graphql-kotlin-graalvm-metadata-generator` project that will be used by Gradle and Maven plugins to auto-generate GraalVM reachability metadata. In order to keep the size of PRs manageable, plugin PRs will be created separately. [GraalVM reachability metadata](https://www.graalvm.org/22.2/reference-manual/native-image/metadata/) is required by GraalVM to support reflections and resource handling. This feature will be used by plugins as way to simplify running `graphql-kotlin` servers as GraalVM native images. Without this functionality, end users have to start their applications with GraalVM agent and then manually run **ALL** possible execution paths (i.e. queries, loading graphiql, etc). * update generateGraalVmReflectMetadata to be internal
1 parent 8a445d9 commit 403603c

37 files changed

+1893
-0
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
description = "GraalVM metadata generator for GraphQL Kotlin servers"
2+
3+
plugins {
4+
id("com.expediagroup.graphql.conventions")
5+
}
6+
7+
dependencies {
8+
implementation(projects.graphqlKotlinHooksProvider)
9+
implementation(projects.graphqlKotlinServer)
10+
implementation(projects.graphqlKotlinFederation)
11+
implementation(libs.classgraph)
12+
implementation(libs.slf4j)
13+
}
14+
15+
testing {
16+
suites {
17+
val test by getting(JvmTestSuite::class) {
18+
useJUnitJupiter()
19+
}
20+
21+
val integrationTest by registering(JvmTestSuite::class) {
22+
dependencies {
23+
implementation(project())
24+
}
25+
26+
targets {
27+
all {
28+
testTask.configure {
29+
shouldRunAfter(test)
30+
}
31+
}
32+
}
33+
34+
sources {
35+
java {
36+
setSrcDirs(listOf("src/integrationTest/kotlin"))
37+
}
38+
resources {
39+
setSrcDirs(listOf("src/integrationTest/resources"))
40+
}
41+
compileClasspath += sourceSets["test"].compileClasspath
42+
runtimeClasspath += compileClasspath + sourceSets["test"].runtimeClasspath
43+
}
44+
}
45+
}
46+
}
47+
48+
tasks {
49+
jacocoTestReport {
50+
// we need to explicitly add integrationTest coverage info
51+
executionData.setFrom(fileTree(buildDir).include("/jacoco/*.exec"))
52+
}
53+
jacocoTestCoverageVerification {
54+
// we need to explicitly add integrationTest coverage info
55+
executionData.setFrom(fileTree(buildDir).include("/jacoco/*.exec"))
56+
violationRules {
57+
rule {
58+
limit {
59+
counter = "INSTRUCTION"
60+
value = "COVEREDRATIO"
61+
minimum = "0.85".toBigDecimal()
62+
}
63+
limit {
64+
counter = "BRANCH"
65+
value = "COVEREDRATIO"
66+
minimum = "0.60".toBigDecimal()
67+
}
68+
}
69+
}
70+
}
71+
check {
72+
dependsOn(testing.suites.named("integrationTest"))
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm.custom
18+
19+
import com.expediagroup.graphql.server.operations.Query
20+
import java.util.UUID
21+
22+
class CustomScalarQuery : Query {
23+
fun customScalar(): UUID = TODO()
24+
fun customScalarArg(arg: UUID): UUID = TODO()
25+
fun optionalCustomScalarArg(arg: UUID? = null): UUID? = TODO()
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm.custom
18+
19+
import com.expediagroup.graphql.plugin.graalvm.ClassMetadata
20+
import com.expediagroup.graphql.plugin.graalvm.DefaultMetadataLoader.loadDefaultReflectMetadata
21+
import com.expediagroup.graphql.plugin.graalvm.MethodMetadata
22+
import com.expediagroup.graphql.plugin.graalvm.generateGraalVmMetadata
23+
import com.fasterxml.jackson.annotation.JsonInclude
24+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
25+
import org.junit.jupiter.api.Assertions
26+
import org.junit.jupiter.api.Test
27+
28+
class GenerateGraalVmCustomScalarMetadataTest {
29+
30+
@Test
31+
fun `verifies we can generate valid reflect metadata for query with custom scalars`() {
32+
val expected = listOf(
33+
ClassMetadata(
34+
name = "com.expediagroup.graphql.plugin.graalvm.custom.CustomScalarQuery",
35+
methods = listOf(
36+
MethodMetadata(
37+
name = "customScalar",
38+
parameterTypes = listOf()
39+
),
40+
MethodMetadata(
41+
name = "customScalarArg",
42+
parameterTypes = listOf("java.util.UUID")
43+
),
44+
MethodMetadata(
45+
name = "optionalCustomScalarArg",
46+
parameterTypes = listOf("java.util.UUID")
47+
),
48+
MethodMetadata(
49+
name = "optionalCustomScalarArg\$default",
50+
parameterTypes = listOf("com.expediagroup.graphql.plugin.graalvm.custom.CustomScalarQuery", "java.util.UUID", "int", "java.lang.Object")
51+
)
52+
)
53+
)
54+
)
55+
56+
val actual = generateGraalVmMetadata(supportedPackages = listOf("com.expediagroup.graphql.plugin.graalvm.custom"))
57+
val defaults = loadDefaultReflectMetadata()
58+
59+
val mapper = jacksonObjectMapper()
60+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
61+
val writer = mapper.writerWithDefaultPrettyPrinter()
62+
Assertions.assertEquals(writer.writeValueAsString(expected + defaults), writer.writeValueAsString(actual))
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm.federated
18+
19+
import com.expediagroup.graphql.generator.federation.directives.ExternalDirective
20+
import com.expediagroup.graphql.generator.federation.directives.FieldSet
21+
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
22+
import com.expediagroup.graphql.generator.federation.directives.RequiresDirective
23+
import kotlin.properties.Delegates
24+
25+
@KeyDirective(fields = FieldSet("id"))
26+
data class FederatedEntity(val id: Int) {
27+
@ExternalDirective
28+
var externalField: String by Delegates.notNull()
29+
30+
@RequiresDirective(FieldSet("externalField"))
31+
fun federatedFunction(arg: String): String = TODO()
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm.federated
18+
19+
import com.expediagroup.graphql.plugin.graalvm.ClassMetadata
20+
import com.expediagroup.graphql.plugin.graalvm.DefaultMetadataLoader.loadDefaultReflectMetadata
21+
import com.expediagroup.graphql.plugin.graalvm.MethodMetadata
22+
import com.expediagroup.graphql.plugin.graalvm.generateGraalVmMetadata
23+
import com.fasterxml.jackson.annotation.JsonInclude
24+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
25+
import org.junit.jupiter.api.Assertions
26+
import org.junit.jupiter.api.Test
27+
28+
class GenerateGraalVmEntityMetadataTest {
29+
30+
@Test
31+
fun `verifies we can generate valid reflect metadata for federated entities`() {
32+
val expected = listOf(
33+
ClassMetadata(
34+
name = "com.expediagroup.graphql.plugin.graalvm.federated.FederatedEntity",
35+
allDeclaredFields = true,
36+
methods = listOf(
37+
MethodMetadata(
38+
name = "federatedFunction",
39+
parameterTypes = listOf("java.lang.String")
40+
),
41+
MethodMetadata(
42+
name = "getExternalField",
43+
parameterTypes = listOf()
44+
),
45+
MethodMetadata(
46+
name = "getId",
47+
parameterTypes = listOf()
48+
)
49+
)
50+
)
51+
)
52+
53+
val actual = generateGraalVmMetadata(supportedPackages = listOf("com.expediagroup.graphql.plugin.graalvm.federated"))
54+
val defaults = loadDefaultReflectMetadata()
55+
56+
val mapper = jacksonObjectMapper()
57+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
58+
val writer = mapper.writerWithDefaultPrettyPrinter()
59+
Assertions.assertEquals(writer.writeValueAsString(expected + defaults), writer.writeValueAsString(actual))
60+
}
61+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm.hooks
18+
19+
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
20+
import graphql.language.StringValue
21+
import graphql.schema.Coercing
22+
import graphql.schema.CoercingParseLiteralException
23+
import graphql.schema.CoercingParseValueException
24+
import graphql.schema.GraphQLScalarType
25+
import graphql.schema.GraphQLType
26+
import java.util.UUID
27+
import kotlin.reflect.KType
28+
29+
private val graphqlUUIDType = GraphQLScalarType.newScalar()
30+
.name("UUID")
31+
.description("Custom scalar representing UUID")
32+
.coercing(object : Coercing<UUID, String> {
33+
override fun parseValue(input: Any): UUID = try {
34+
UUID.fromString(serialize(input))
35+
} catch (e: Exception) {
36+
throw CoercingParseValueException("Unable to convert value $input to UUID")
37+
}
38+
39+
override fun parseLiteral(input: Any): UUID {
40+
val uuidString = (input as? StringValue)?.value
41+
return if (uuidString != null) {
42+
UUID.fromString(uuidString)
43+
} else {
44+
throw CoercingParseLiteralException("Unable to convert literal $input to UUID")
45+
}
46+
}
47+
48+
override fun serialize(dataFetcherResult: Any): String = dataFetcherResult.toString()
49+
})
50+
.build()
51+
52+
class CustomFederatedHooks : FederatedSchemaGeneratorHooks(emptyList()) {
53+
54+
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier) {
55+
UUID::class -> graphqlUUIDType
56+
else -> super.willGenerateGraphQLType(type)
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm.hooks
18+
19+
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
20+
import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
21+
22+
class TestSchemaGeneratorHooksProvider : SchemaGeneratorHooksProvider {
23+
24+
override fun hooks(): SchemaGeneratorHooks = CustomFederatedHooks()
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.expediagroup.graphql.plugin.graalvm.hooks.TestSchemaGeneratorHooksProvider
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2023 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.plugin.graalvm
18+
19+
import com.fasterxml.jackson.core.type.TypeReference
20+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
21+
import java.io.InputStream
22+
23+
private const val DEFAULT_REFLECT_CONFIG = "default-reflect-config.json"
24+
private const val DEFAULT_RESOURCE_CONFIG = "default-resource-config.json"
25+
26+
object DefaultMetadataLoader {
27+
/**
28+
* Load default GraalVM reflect metadata required to run native graphql-kotlin servers
29+
*/
30+
fun loadDefaultReflectMetadata(): List<ClassMetadata> {
31+
val defaultResources = DefaultMetadataLoader.javaClass.classLoader.getResourceAsStream(DEFAULT_REFLECT_CONFIG)
32+
?: throw IllegalStateException("Unable to load graphql-kotlin GraalVM reflect metadata")
33+
val mapper = jacksonObjectMapper()
34+
return mapper.readValue(defaultResources, object : TypeReference<List<ClassMetadata>>() {})
35+
}
36+
37+
/**
38+
* Open up InputStream to default GraalVM resource config file to be used with graphql-kotlin servers.
39+
*/
40+
fun defaultResourceMetadataStream(): InputStream =
41+
DefaultMetadataLoader.javaClass.classLoader.getResourceAsStream(DEFAULT_RESOURCE_CONFIG)
42+
?: throw IllegalStateException("Unable to load graphql-kotlin GraalVM resources metadata")
43+
}

0 commit comments

Comments
 (0)