Skip to content

Commit 01a1345

Browse files
committed
GraalVM example integration
Adds new `graalVM` composite build that ensures servers (ktor for now) can be build with GraphQL Kotlin plugin and produce valid GraalVM native image.
1 parent f4ba9b4 commit 01a1345

File tree

32 files changed

+1053
-1
lines changed

32 files changed

+1053
-1
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: GraaLVM Integration Tests
2+
3+
on:
4+
workflow_call:
5+
pull_request:
6+
branches:
7+
- master
8+
paths:
9+
- 'generator/**'
10+
- 'servers/**'
11+
- 'plugins/**'
12+
- 'integration/graalvm/**'
13+
14+
jobs:
15+
graalvm-integration:
16+
timeout-minutes: 20
17+
runs-on: ubuntu-latest
18+
defaults:
19+
run:
20+
working-directory: integration/graalvm
21+
strategy:
22+
matrix:
23+
server: ['ktor-graalvm-server']
24+
25+
steps:
26+
- name: Checkout Repository
27+
uses: actions/checkout@v3
28+
29+
- name: Validate Gradle wrapper
30+
uses: gradle/wrapper-validation-action@v1
31+
32+
- name: Setup GraalVM
33+
uses: graalvm/setup-graalvm@v1
34+
with:
35+
version: '22.3.1'
36+
java-version: '17'
37+
components: 'native-image'
38+
native-image-job-reports: 'true'
39+
40+
- name: Set up Gradle cache
41+
uses: gradle/gradle-build-action@v2
42+
43+
- name: Build server
44+
run: ./gradlew :${server}:build :${server}:nativeCompile
45+
env:
46+
server: ${{ matrix.server }}
47+
48+
- name: Build and start the native image
49+
id: start_server
50+
run: |
51+
set -x
52+
echo "starting server"
53+
./${server}/build/native/nativeCompile/${server} &
54+
echo "SERVER_PID=$(echo $!)" >> $GITHUB_OUTPUT
55+
env:
56+
server: ${{ matrix.server }}
57+
58+
- name: Integration Test
59+
run: |
60+
echo "sending a test query"
61+
curl --request POST \
62+
--verbose \
63+
--header 'content-type: application/json' \
64+
--url http://localhost:8080/graphql \
65+
--data '{"query":"query($inputArg: InputOnlyInput){ inputTypeQuery(arg: $inputArg) }","variables":{"inputArg": { "id": 123 }}}' \
66+
> response.json
67+
68+
echo "received GraphQL response"
69+
cat response.json
70+
71+
echo "verifying response"
72+
jq -e '.data.inputTypeQuery == "InputOnly(id=123)"' response.json
73+
74+
- name: Stop server
75+
if: ${{ always() }}
76+
run: |
77+
echo "shutting down server"
78+
kill -9 ${{ steps.start_server.outputs.SERVER_PID }}

integration/graalvm/build.gradle.kts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import java.util.Properties
2+
3+
allprojects {
4+
repositories {
5+
mavenCentral()
6+
mavenLocal {
7+
content {
8+
includeGroup("com.expediagroup")
9+
}
10+
}
11+
}
12+
13+
val properties = Properties()
14+
properties.load(File(rootDir.parentFile.parent, "gradle.properties").inputStream())
15+
for ((key, value) in properties) {
16+
if (!project.ext.has(key.toString())) {
17+
project.ext[key.toString()] = value
18+
}
19+
}
20+
}

integration/graalvm/gradle.properties

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
group = com.expediagroup.graalvm
2+
3+
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
4+
org.gradle.caching=true
5+
org.gradle.parallel=true
6+

integration/graalvm/gradle/wrapper

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../gradle/wrapper

integration/graalvm/gradlew

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../gradlew
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import com.expediagroup.graphql.plugin.gradle.graphql
2+
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3+
4+
@Suppress("DSL_SCOPE_VIOLATION") // TODO: remove once KTIJ-19369 / Gradle#22797 is fixed
5+
plugins {
6+
alias(libs.plugins.kotlin.jvm)
7+
application
8+
alias(libs.plugins.graalvm.native)
9+
id("com.expediagroup.graphql")
10+
}
11+
12+
dependencies {
13+
implementation("com.expediagroup", "graphql-kotlin-ktor-server")
14+
implementation("com.expediagroup", "graphql-kotlin-hooks-provider")
15+
implementation(libs.logback)
16+
implementation(libs.ktor.server.cio)
17+
testImplementation(libs.junit.api)
18+
testImplementation(libs.kotlin.test)
19+
testImplementation(libs.ktor.client.content)
20+
testImplementation(libs.ktor.server.test.host)
21+
}
22+
23+
tasks.test {
24+
useJUnitPlatform()
25+
}
26+
27+
tasks.withType<KotlinCompile> {
28+
kotlinOptions.jvmTarget = "17"
29+
}
30+
31+
application {
32+
mainClass.set("com.expediagroup.graalvm.ktor.ApplicationKt")
33+
}
34+
35+
graalvmNative {
36+
toolchainDetection.set(false)
37+
binaries {
38+
named("main") {
39+
verbose.set(true)
40+
buildArgs.add("--initialize-at-build-time=io.ktor,kotlin,ch.qos.logback,org.slf4j")
41+
buildArgs.add("-H:+ReportExceptionStackTraces")
42+
}
43+
metadataRepository {
44+
enabled.set(true)
45+
}
46+
}
47+
}
48+
49+
graphql {
50+
graalVm {
51+
packages = listOf("com.expediagroup.graalvm.ktor")
52+
}
53+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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.graalvm.ktor
18+
19+
import com.expediagroup.graalvm.ktor.context.CustomContextFactory
20+
import com.expediagroup.graalvm.ktor.hooks.CustomHooks
21+
import com.expediagroup.graalvm.ktor.schema.ArgumentQuery
22+
import com.expediagroup.graalvm.ktor.schema.AsyncQuery
23+
import com.expediagroup.graalvm.ktor.schema.BasicMutation
24+
import com.expediagroup.graalvm.ktor.schema.ContextualQuery
25+
import com.expediagroup.graalvm.ktor.schema.CustomScalarQuery
26+
import com.expediagroup.graalvm.ktor.schema.EnumQuery
27+
import com.expediagroup.graalvm.ktor.schema.ErrorQuery
28+
import com.expediagroup.graalvm.ktor.schema.IdQuery
29+
import com.expediagroup.graalvm.ktor.schema.model.ExampleInterface
30+
import com.expediagroup.graalvm.ktor.schema.model.ExampleUnion
31+
import com.expediagroup.graalvm.ktor.schema.InnerClassQuery
32+
import com.expediagroup.graalvm.ktor.schema.ListQuery
33+
import com.expediagroup.graalvm.ktor.schema.PolymorphicQuery
34+
import com.expediagroup.graalvm.ktor.schema.ScalarQuery
35+
import com.expediagroup.graalvm.ktor.schema.TypesQuery
36+
import com.expediagroup.graalvm.ktor.schema.dataloader.ExampleDataLoader
37+
import com.expediagroup.graalvm.ktor.schema.model.FirstImpl
38+
import com.expediagroup.graalvm.ktor.schema.model.FirstUnionMember
39+
import com.expediagroup.graalvm.ktor.schema.model.SecondImpl
40+
import com.expediagroup.graalvm.ktor.schema.model.SecondUnionMember
41+
import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory
42+
import com.expediagroup.graphql.server.ktor.GraphQL
43+
import com.expediagroup.graphql.server.ktor.graphQLGetRoute
44+
import com.expediagroup.graphql.server.ktor.graphQLPostRoute
45+
import com.expediagroup.graphql.server.ktor.graphQLSDLRoute
46+
import com.expediagroup.graphql.server.ktor.graphiQLRoute
47+
import io.ktor.server.application.install
48+
import io.ktor.server.cio.CIO
49+
import io.ktor.server.engine.embeddedServer
50+
import io.ktor.server.routing.routing
51+
52+
fun main() {
53+
embeddedServer(CIO, port = 8080, host = "0.0.0.0") {
54+
install(GraphQL) {
55+
schema {
56+
packages = listOf("com.expediagroup.graalvm.ktor")
57+
queries = listOf(
58+
ArgumentQuery(),
59+
AsyncQuery(),
60+
ContextualQuery(),
61+
CustomScalarQuery(),
62+
EnumQuery(),
63+
ErrorQuery(),
64+
IdQuery(),
65+
InnerClassQuery(),
66+
ListQuery(),
67+
PolymorphicQuery(),
68+
ScalarQuery(),
69+
TypesQuery()
70+
)
71+
mutations = listOf(
72+
BasicMutation()
73+
)
74+
hooks = CustomHooks()
75+
typeHierarchy = mapOf(
76+
ExampleInterface::class to listOf(FirstImpl::class, SecondImpl::class),
77+
ExampleUnion::class to listOf(FirstUnionMember::class, SecondUnionMember::class)
78+
)
79+
}
80+
engine {
81+
dataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory(
82+
ExampleDataLoader
83+
)
84+
}
85+
server {
86+
contextFactory = CustomContextFactory()
87+
}
88+
}
89+
routing {
90+
graphQLGetRoute()
91+
graphQLPostRoute()
92+
graphQLSDLRoute()
93+
graphiQLRoute()
94+
}
95+
}.start(wait = true)
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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.graalvm.ktor.context
18+
19+
import com.expediagroup.graphql.generator.extensions.plus
20+
import com.expediagroup.graphql.server.ktor.DefaultKtorGraphQLContextFactory
21+
import graphql.GraphQLContext
22+
import io.ktor.server.request.ApplicationRequest
23+
import java.util.UUID
24+
25+
class CustomContextFactory : DefaultKtorGraphQLContextFactory() {
26+
override suspend fun generateContext(request: ApplicationRequest): GraphQLContext {
27+
return super.generateContext(request).plus(mapOf("custom" to (request.headers["X-Custom-Header"] ?: UUID.randomUUID().toString())))
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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.graalvm.ktor.hooks
18+
19+
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
20+
import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
21+
import graphql.language.StringValue
22+
import graphql.schema.Coercing
23+
import graphql.schema.CoercingParseLiteralException
24+
import graphql.schema.CoercingParseValueException
25+
import graphql.schema.CoercingSerializeException
26+
import graphql.schema.GraphQLScalarType
27+
import graphql.schema.GraphQLType
28+
import java.util.UUID
29+
import kotlin.reflect.KClass
30+
import kotlin.reflect.KType
31+
32+
class CustomHooks : SchemaGeneratorHooks {
33+
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) {
34+
UUID::class -> graphqlUUIDType
35+
else -> null
36+
}
37+
}
38+
39+
class CustomHooksProvider : SchemaGeneratorHooksProvider {
40+
override fun hooks(): SchemaGeneratorHooks = CustomHooks()
41+
}
42+
43+
val graphqlUUIDType: GraphQLScalarType = GraphQLScalarType.newScalar()
44+
.name("UUID")
45+
.description("A type representing a formatted java.util.UUID")
46+
.coercing(UUIDCoercing)
47+
.build()
48+
49+
object UUIDCoercing : Coercing<UUID, String> {
50+
override fun parseValue(input: Any): UUID = runCatching {
51+
UUID.fromString(serialize(input))
52+
}.getOrElse {
53+
throw CoercingParseValueException("Expected valid UUID but was $input")
54+
}
55+
56+
override fun parseLiteral(input: Any): UUID {
57+
val uuidString = (input as? StringValue)?.value
58+
return runCatching {
59+
UUID.fromString(uuidString)
60+
}.getOrElse {
61+
throw CoercingParseLiteralException("Expected valid UUID literal but was $uuidString")
62+
}
63+
}
64+
65+
override fun serialize(dataFetcherResult: Any): String = runCatching {
66+
dataFetcherResult.toString()
67+
}.getOrElse {
68+
throw CoercingSerializeException("Data fetcher result $dataFetcherResult cannot be serialized to a String")
69+
}
70+
}

0 commit comments

Comments
 (0)