Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,17 @@ interface SchemaGeneratorHooksProvider {
* Create a new instance of a SchemaGeneratorHooks that will be used to generate GraphQL schema in SDL format.
*/
fun hooks(): SchemaGeneratorHooks

/**
* Priority used to break ties when multiple `SchemaGeneratorHooksProvider`s are discovered on the classpath.
*
* When more than one provider is found, the one with the strictly highest priority is selected. If multiple
* providers share the highest priority the generator plugins still fail with an error so the conflict is surfaced
* to the user. Providers that do not override this method fall back to the default priority of `0`, which
* preserves the pre-existing fail-fast behaviour for users that have not opted in.
*
* Library authors that ship a `SchemaGeneratorHooksProvider` which must override a transitively-imported
* provider should return a positive value. Larger values win.
*/
fun priority(): Int = 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,11 @@ private val logger: Logger = LoggerFactory.getLogger("generateSDL")
*/
fun generateSDL(supportedPackages: List<String>): String {
val hooksProviders = ServiceLoader.load(SchemaGeneratorHooksProvider::class.java).toList()
val hooks = when {
hooksProviders.isEmpty() -> {
logger.warn("No SchemaGeneratorHooksProvider were found, defaulting to NoopSchemaGeneratorHooks")
NoopSchemaGeneratorHooks
}
hooksProviders.size > 1 -> {
throw RuntimeException("Cannot generate SDL as multiple SchemaGeneratorHooksProviders were found on the classpath")
}
else -> {
val provider = hooksProviders.first()
logger.debug("SchemaGeneratorHooksProvider found, ${provider.javaClass.simpleName} will be used to generate the hooks")
provider.hooks()
}
val hooks = if (hooksProviders.isEmpty()) {
logger.warn("No SchemaGeneratorHooksProvider were found, defaulting to NoopSchemaGeneratorHooks")
NoopSchemaGeneratorHooks
} else {
selectHooksProvider(hooksProviders, logger).hooks()
}
val scanResult = ClassGraph()
.enableAllInfo()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2026 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.plugin.schema

import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
import org.slf4j.Logger

/**
* Pick the `SchemaGeneratorHooksProvider` that should be used when more than one provider is registered through
* `ServiceLoader`.
*
* Selection rules:
* * When the list contains a single provider it is returned unchanged.
* * When the list contains multiple providers the one with the strictly highest `priority()` wins.
* * When multiple providers tie for the highest priority a `RuntimeException` is thrown so the conflict is surfaced
* to the user, preserving the fail-fast behaviour that existed before priorities were supported.
*
* The caller is expected to handle the empty-list case before invoking this function.
*/
internal fun selectHooksProvider(
providers: List<SchemaGeneratorHooksProvider>,
logger: Logger
): SchemaGeneratorHooksProvider {
require(providers.isNotEmpty()) { "selectHooksProvider should not be called with an empty list of providers" }

if (providers.size == 1) {
val provider = providers.single()
logger.debug("SchemaGeneratorHooksProvider found, ${provider.javaClass.simpleName} will be used to generate the hooks")
return provider
}

val maxPriority = providers.maxOf { it.priority() }
val topProviders = providers.filter { it.priority() == maxPriority }
if (topProviders.size > 1) {
val tiedNames = topProviders.joinToString(", ") { it.javaClass.name }
throw RuntimeException(
"Cannot generate SDL as multiple SchemaGeneratorHooksProviders were found on the classpath with " +
"the same priority ($maxPriority): [$tiedNames]. Override SchemaGeneratorHooksProvider.priority() " +
"on the provider that should win to disambiguate."
)
}

val winner = topProviders.single()
val losers = providers.filter { it !== winner }
logger.warn(
"Multiple SchemaGeneratorHooksProviders were found on the classpath; selecting ${winner.javaClass.simpleName} " +
"with priority $maxPriority. Ignored providers: " +
losers.joinToString(", ") { "${it.javaClass.simpleName}(priority=${it.priority()})" }
)
return winner
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2026 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.plugin.schema

import com.expediagroup.graphql.generator.hooks.NoopSchemaGeneratorHooks
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.slf4j.LoggerFactory
import kotlin.test.assertEquals
import kotlin.test.assertSame
import kotlin.test.assertTrue

class SelectHooksProviderTest {

private val logger = LoggerFactory.getLogger(SelectHooksProviderTest::class.java)

private class FixedPriorityProvider(private val priority: Int) : SchemaGeneratorHooksProvider {
override fun hooks(): SchemaGeneratorHooks = NoopSchemaGeneratorHooks
override fun priority(): Int = priority
}

private class DefaultPriorityProvider : SchemaGeneratorHooksProvider {
override fun hooks(): SchemaGeneratorHooks = NoopSchemaGeneratorHooks
}

@Test
fun `requires a non-empty provider list`() {
val exception = assertThrows<IllegalArgumentException> {
selectHooksProvider(emptyList(), logger)
}
assertTrue(exception.message!!.contains("empty list of providers"))
}

@Test
fun `returns the single provider when only one is registered`() {
val only = DefaultPriorityProvider()
val selected = selectHooksProvider(listOf(only), logger)
assertSame(only, selected)
}

@Test
fun `selects the provider with the highest priority when multiple are registered`() {
val low = FixedPriorityProvider(priority = 0)
val high = FixedPriorityProvider(priority = 10)
val mid = FixedPriorityProvider(priority = 5)

val selected = selectHooksProvider(listOf(low, mid, high), logger)

assertSame(high, selected)
}

@Test
fun `throws when the highest priority is tied between multiple providers`() {
val first = FixedPriorityProvider(priority = 5)
val second = FixedPriorityProvider(priority = 5)

val exception = assertThrows<RuntimeException> {
selectHooksProvider(listOf(first, second), logger)
}

assertTrue(
exception.message!!.contains("Cannot generate SDL as multiple SchemaGeneratorHooksProviders"),
"expected the legacy error prefix to be preserved but was: ${exception.message}"
)
assertTrue(
exception.message!!.contains("priority (5)"),
"expected the tied priority value to be in the message but was: ${exception.message}"
)
assertTrue(
exception.message!!.contains(FixedPriorityProvider::class.java.name),
"expected the tied provider class name to be in the message but was: ${exception.message}"
)
}

@Test
fun `throws when all providers share the default priority`() {
val first = DefaultPriorityProvider()
val second = DefaultPriorityProvider()

val exception = assertThrows<RuntimeException> {
selectHooksProvider(listOf(first, second), logger)
}

assertTrue(
exception.message!!.contains("priority (0)"),
"expected tie at default priority (0) to be reported, was: ${exception.message}"
)
}

@Test
fun `default priority on the SPI is zero`() {
assertEquals(0, DefaultPriorityProvider().priority())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,11 @@ fun generateGraalVmMetadata(targetDirectory: File, supportedPackages: List<Strin
*/
fun generateGraalVmReflectMetadata(supportedPackages: List<String>): List<ClassMetadata> {
val hooksProviders = ServiceLoader.load(SchemaGeneratorHooksProvider::class.java).toList()
val hooks = when {
hooksProviders.isEmpty() -> {
logger.warn("No SchemaGeneratorHooksProvider were found, defaulting to NoopSchemaGeneratorHooks")
NoopSchemaGeneratorHooks
}

hooksProviders.size > 1 -> {
throw RuntimeException("Cannot generate SDL as multiple SchemaGeneratorHooksProviders were found on the classpath")
}

else -> {
val provider = hooksProviders.first()
logger.debug("SchemaGeneratorHooksProvider found, ${provider.javaClass.simpleName} will be used to generate the hooks")
provider.hooks()
}
val hooks = if (hooksProviders.isEmpty()) {
logger.warn("No SchemaGeneratorHooksProvider were found, defaulting to NoopSchemaGeneratorHooks")
NoopSchemaGeneratorHooks
} else {
selectHooksProvider(hooksProviders, logger).hooks()
}

val scanResult = ClassGraph()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2026 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.plugin.graalvm

import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
import org.slf4j.Logger

/**
* Pick the `SchemaGeneratorHooksProvider` that should be used when more than one provider is registered through
* `ServiceLoader` while generating GraalVM reflect metadata.
*
* Selection rules mirror the SDL generator: the provider with the strictly highest `priority()` wins, and ties
* at the top priority result in a `RuntimeException` so the conflict is visible.
*/
internal fun selectHooksProvider(
providers: List<SchemaGeneratorHooksProvider>,
logger: Logger
): SchemaGeneratorHooksProvider {
require(providers.isNotEmpty()) { "selectHooksProvider should not be called with an empty list of providers" }

if (providers.size == 1) {
val provider = providers.single()
logger.debug("SchemaGeneratorHooksProvider found, ${provider.javaClass.simpleName} will be used to generate the hooks")
return provider
}

val maxPriority = providers.maxOf { it.priority() }
val topProviders = providers.filter { it.priority() == maxPriority }
if (topProviders.size > 1) {
val tiedNames = topProviders.joinToString(", ") { it.javaClass.name }
throw RuntimeException(
"Cannot generate SDL as multiple SchemaGeneratorHooksProviders were found on the classpath with " +
"the same priority ($maxPriority): [$tiedNames]. Override SchemaGeneratorHooksProvider.priority() " +
"on the provider that should win to disambiguate."
)
}

val winner = topProviders.single()
val losers = providers.filter { it !== winner }
logger.warn(
"Multiple SchemaGeneratorHooksProviders were found on the classpath; selecting ${winner.javaClass.simpleName} " +
"with priority $maxPriority. Ignored providers: " +
losers.joinToString(", ") { "${it.javaClass.simpleName}(priority=${it.priority()})" }
)
return winner
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2026 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.plugin.graalvm

import com.expediagroup.graphql.generator.hooks.NoopSchemaGeneratorHooks
import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.slf4j.LoggerFactory
import kotlin.test.assertSame
import kotlin.test.assertTrue

class SelectHooksProviderTest {

private val logger = LoggerFactory.getLogger(SelectHooksProviderTest::class.java)

private class FixedPriorityProvider(private val priority: Int) : SchemaGeneratorHooksProvider {
override fun hooks(): SchemaGeneratorHooks = NoopSchemaGeneratorHooks
override fun priority(): Int = priority
}

private class DefaultPriorityProvider : SchemaGeneratorHooksProvider {
override fun hooks(): SchemaGeneratorHooks = NoopSchemaGeneratorHooks
}

@Test
fun `returns the single provider when only one is registered`() {
val only = DefaultPriorityProvider()
val selected = selectHooksProvider(listOf(only), logger)
assertSame(only, selected)
}

@Test
fun `selects the provider with the highest priority when multiple are registered`() {
val low = FixedPriorityProvider(priority = 0)
val high = FixedPriorityProvider(priority = 10)

val selected = selectHooksProvider(listOf(low, high), logger)

assertSame(high, selected)
}

@Test
fun `throws when the highest priority is tied between multiple providers`() {
val first = FixedPriorityProvider(priority = 5)
val second = FixedPriorityProvider(priority = 5)

val exception = assertThrows<RuntimeException> {
selectHooksProvider(listOf(first, second), logger)
}

assertTrue(
exception.message!!.contains("Cannot generate SDL as multiple SchemaGeneratorHooksProviders"),
"expected the legacy error prefix to be preserved but was: ${exception.message}"
)
assertTrue(
exception.message!!.contains("priority (5)"),
"expected the tied priority value to be in the message but was: ${exception.message}"
)
}
}