Skip to content

Commit aa07c9a

Browse files
author
sjin
committed
Add fed version validator
1 parent 6c42aad commit aa07c9a

File tree

6 files changed

+119
-17
lines changed

6 files changed

+119
-17
lines changed

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt

+23-5
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,14 @@ open class FederatedSchemaGeneratorHooks(
9797
private val resolvers: List<FederatedTypeResolver>
9898
) : FlowSubscriptionSchemaGeneratorHooks() {
9999
private val validator: FederatedSchemaValidator = FederatedSchemaValidator()
100+
100101
data class LinkSpec(val namespace: String, val imports: Map<String, String>, val url: String? = FEDERATION_SPEC_LATEST_URL)
102+
101103
public val linkSpecs: MutableMap<String, LinkSpec> = HashMap()
102104

105+
private val federationUrl: String
106+
get() = linkSpecs[FEDERATION_SPEC]?.url ?: FEDERATION_SPEC_LATEST_URL
107+
103108
// workaround to https://github.com/ExpediaGroup/graphql-kotlin/issues/1815
104109
// since those scalars can be renamed, we need to ensure we only generate those scalars just once
105110
private val fieldSetScalar: GraphQLScalarType by lazy {
@@ -218,19 +223,23 @@ open class FederatedSchemaGeneratorHooks(
218223
}
219224

220225
override fun willGenerateDirective(directiveInfo: DirectiveMetaInformation): GraphQLDirective? {
221-
val federationSpec = linkSpecs[FEDERATION_SPEC]
222-
val federationUrl = federationSpec?.url ?: FEDERATION_SPEC_LATEST_URL
226+
// Directive requires minimum fed version.
227+
when (directiveInfo.effectiveName) {
228+
POLICY_DIRECTIVE_NAME -> checkDirectiveVersionCompatibility(directiveInfo.effectiveName, Pair(2, 6))
229+
REQUIRES_SCOPE_DIRECTIVE_NAME -> checkDirectiveVersionCompatibility(directiveInfo.effectiveName, Pair(2, 7))
230+
COMPOSE_DIRECTIVE_NAME -> checkDirectiveVersionCompatibility(directiveInfo.effectiveName, Pair(2, 1))
231+
}
223232

224233
return when (directiveInfo.effectiveName) {
225234
CONTACT_DIRECTIVE_NAME -> CONTACT_DIRECTIVE_TYPE
226235
EXTERNAL_DIRECTIVE_NAME -> EXTERNAL_DIRECTIVE_TYPE
227236
KEY_DIRECTIVE_NAME -> keyDirectiveDefinition(fieldSetScalar)
228237
LINK_DIRECTIVE_NAME -> linkDirectiveDefinition(linkImportScalar)
229-
OVERRIDE_DIRECTIVE_NAME -> overrideDirectiveDefinition(federationUrl)
230-
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar, federationUrl)
238+
POLICY_DIRECTIVE_NAME -> policyDirectiveDefinition(policiesScalar)
231239
PROVIDES_DIRECTIVE_NAME -> providesDirectiveDefinition(fieldSetScalar)
232240
REQUIRES_DIRECTIVE_NAME -> requiresDirectiveDefinition(fieldSetScalar)
233241
REQUIRES_SCOPE_DIRECTIVE_NAME -> requiresScopesDirectiveType(scopesScalar)
242+
OVERRIDE_DIRECTIVE_NAME -> overrideDirectiveDefinition(federationUrl)
234243
else -> super.willGenerateDirective(directiveInfo)
235244
}
236245
}
@@ -240,12 +249,15 @@ open class FederatedSchemaGeneratorHooks(
240249
REQUIRES_SCOPE_DIRECTIVE_NAME -> {
241250
directive.toAppliedRequiresScopesDirective(directiveInfo)
242251
}
252+
243253
POLICY_DIRECTIVE_NAME -> {
244254
directive.toAppliedPolicyDirective(directiveInfo)
245255
}
256+
246257
OVERRIDE_DIRECTIVE_NAME -> {
247258
directive.toAppliedOverrideDirective(directiveInfo)
248259
}
260+
249261
else -> {
250262
super.willApplyDirective(directiveInfo, directive)
251263
}
@@ -303,7 +315,6 @@ open class FederatedSchemaGeneratorHooks(
303315
// only add @link directive definition if it doesn't exist yet
304316
builder.additionalDirective(linkDirective)
305317
}
306-
val federationUrl = linkSpecs[FEDERATION_SPEC]?.url ?: FEDERATION_SPEC_LATEST_URL
307318
builder.withSchemaAppliedDirective(linkDirective.toAppliedLinkDirective(federationUrl, null, fed2Imports))
308319
}
309320

@@ -380,4 +391,11 @@ open class FederatedSchemaGeneratorHooks(
380391
return kClass.findAnnotation<GraphQLName>()?.value
381392
?: kClass.simpleName
382393
}
394+
395+
private fun checkDirectiveVersionCompatibility(directiveName: String, requiredVersion: Pair<Int, Int>) {
396+
val federationUrl = linkSpecs[FEDERATION_SPEC]?.url ?: FEDERATION_SPEC_LATEST_URL
397+
if (!isFederationVersionAtLeast(federationUrl, requiredVersion.first, requiredVersion.second)) {
398+
throw IllegalArgumentException("@$directiveName directive requires Federation ${requiredVersion.first}.${requiredVersion.second} or later, but version $federationUrl was specified")
399+
}
400+
}
383401
}

generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/OverrideDirective.kt

+14-10
Original file line numberDiff line numberDiff line change
@@ -85,31 +85,35 @@ internal fun overrideDirectiveDefinition(federationVersion: String = FEDERATION_
8585
* Converts a GraphQL directive to an applied override directive with proper validation
8686
* and handling of optional label argument.
8787
*/
88-
internal fun graphql.schema.GraphQLDirective.toAppliedOverrideDirective(directiveInfo: DirectiveMetaInformation): GraphQLAppliedDirective {
88+
internal fun graphql.schema.GraphQLDirective.toAppliedOverrideDirective(directiveInfo: DirectiveMetaInformation, federationVersion: String = FEDERATION_SPEC_LATEST_URL): GraphQLAppliedDirective {
8989
val overrideDirective = directiveInfo.directive as OverrideDirective
9090
val label = overrideDirective.label.takeIf { it.isNotEmpty() }
9191

92+
if (!label.isNullOrEmpty() && !isFederationVersionAtLeast(federationVersion, 2, 7)) {
93+
throw IllegalArgumentException("@override directive 'label' parameter requires Federation 2.7+")
94+
}
95+
9296
if (!label.isNullOrEmpty() && !validateLabel(label)) {
93-
throw Exception("@override label must follow the format 'percent(number)', got: $label")
97+
throw IllegalArgumentException("@override label must follow the format 'percent(number)', got: $label")
9498
}
9599

96100
val builder = GraphQLAppliedDirective.newDirective()
97101
.name(this.name)
98102
.argument(
99103
GraphQLAppliedDirectiveArgument.newArgument()
100-
.name(OVERRIDE_DIRECTIVE_FROM_PARAM)
101-
.type(GraphQLNonNull(Scalars.GraphQLString))
102-
.valueProgrammatic(overrideDirective.from)
103-
.build()
104+
.name(OVERRIDE_DIRECTIVE_FROM_PARAM)
105+
.type(GraphQLNonNull(Scalars.GraphQLString))
106+
.valueProgrammatic(overrideDirective.from)
107+
.build()
104108
)
105109

106110
if (!label.isNullOrEmpty()) {
107111
builder.argument(
108112
GraphQLAppliedDirectiveArgument.newArgument()
109-
.name(OVERRIDE_DIRECTIVE_LABEL_PARAM)
110-
.type(Scalars.GraphQLString)
111-
.valueProgrammatic(label)
112-
.build()
113+
.name(OVERRIDE_DIRECTIVE_LABEL_PARAM)
114+
.type(Scalars.GraphQLString)
115+
.valueProgrammatic(label)
116+
.build()
113117
)
114118
}
115119

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/compose/ComposeDirectiveTest.kt

+27
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import com.expediagroup.graphql.generator.extensions.print
2121
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig
2222
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
2323
import com.expediagroup.graphql.generator.federation.directives.ComposeDirective
24+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC
25+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL_PREFIX
2426
import com.expediagroup.graphql.generator.federation.directives.FieldSet
2527
import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME
2628
import com.expediagroup.graphql.generator.federation.directives.KeyDirective
@@ -128,6 +130,31 @@ class ComposeDirectiveTest {
128130
assertTrue(entityUnion.types.contains(fooType))
129131
}
130132

133+
@Test
134+
fun `verify ComposeDirective is not created for federation v2_0`() {
135+
val config = FederatedSchemaGeneratorConfig(
136+
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.compose"),
137+
hooks = FederatedSchemaGeneratorHooks(emptyList()).apply {
138+
this.linkSpecs[FEDERATION_SPEC] = FederatedSchemaGeneratorHooks.LinkSpec(
139+
namespace = FEDERATION_SPEC,
140+
imports = emptyMap(),
141+
url = "$FEDERATION_SPEC_URL_PREFIX/v2.0"
142+
)
143+
}
144+
)
145+
val exception = Assertions.assertThrows(IllegalArgumentException::class.java) {
146+
toFederatedSchema(
147+
queries = listOf(TopLevelObject(FooQuery())),
148+
schemaObject = TopLevelObject(CustomSchema()),
149+
config = config
150+
)
151+
}
152+
Assertions.assertEquals(
153+
"@composeDirective directive requires Federation 2.1 or later, but version https://specs.apollo.dev/federation/v2.0 was specified",
154+
exception.message
155+
)
156+
}
157+
131158
@LinkDirective(url = "https://www.myspecs.dev/myspec/v1.0", `as` = "myspec", import = [LinkImport("@custom")])
132159
@ComposeDirective(name = "custom")
133160
class CustomSchema

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/override/OverrideDirectiveTest.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import com.expediagroup.graphql.generator.extensions.print
2121
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig
2222
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
2323
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC
24+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL_PREFIX
2425
import com.expediagroup.graphql.generator.federation.directives.OVERRIDE_DIRECTIVE_NAME
2526
import com.expediagroup.graphql.generator.federation.toFederatedSchema
2627
import com.expediagroup.graphql.generator.federation.directives.OverrideDirective
@@ -166,9 +167,9 @@ class OverrideDirectiveTest {
166167
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.override"),
167168
hooks = FederatedSchemaGeneratorHooks(emptyList()).apply {
168169
this.linkSpecs[FEDERATION_SPEC] = FederatedSchemaGeneratorHooks.LinkSpec(
169-
namespace = "federation",
170+
namespace = FEDERATION_SPEC,
170171
imports = mapOf("override" to "override"),
171-
url = "https://specs.apollo.dev/federation/v2.0"
172+
url = "$FEDERATION_SPEC_URL_PREFIX/v2.0"
172173
)
173174
}
174175
)

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/policy/PolicyDirectiveTest.kt

+26
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.expediagroup.graphql.generator.TopLevelObject
2020
import com.expediagroup.graphql.generator.extensions.print
2121
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig
2222
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
23+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC
24+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL_PREFIX
2325
import com.expediagroup.graphql.generator.federation.directives.POLICY_DIRECTIVE_NAME
2426
import com.expediagroup.graphql.generator.federation.directives.Policies
2527
import com.expediagroup.graphql.generator.federation.directives.Policy
@@ -101,6 +103,30 @@ class PolicyDirectiveTest {
101103
assertNotNull(fooQuery.hasAppliedDirective(POLICY_DIRECTIVE_NAME))
102104
}
103105

106+
@Test
107+
fun `verify PolicyDirective is not created for federation v2_5`() {
108+
val config = FederatedSchemaGeneratorConfig(
109+
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.policy"),
110+
hooks = FederatedSchemaGeneratorHooks(emptyList()).apply {
111+
this.linkSpecs[FEDERATION_SPEC] = FederatedSchemaGeneratorHooks.LinkSpec(
112+
namespace = FEDERATION_SPEC,
113+
imports = emptyMap(),
114+
url = "$FEDERATION_SPEC_URL_PREFIX/v2.5"
115+
)
116+
}
117+
)
118+
val exception = Assertions.assertThrows(IllegalArgumentException::class.java) {
119+
toFederatedSchema(
120+
queries = listOf(TopLevelObject(com.expediagroup.graphql.generator.federation.directives.policy.PolicyDirectiveTest.FooQuery())),
121+
config = config
122+
)
123+
}
124+
Assertions.assertEquals(
125+
"@policy directive requires Federation 2.6 or later, but version https://specs.apollo.dev/federation/v2.5 was specified",
126+
exception.message
127+
)
128+
}
129+
104130
class FooQuery {
105131
@PolicyDirective(policies = [Policies([Policy("policy1"), Policy("policy2")]), Policies([Policy("policy3")])])
106132
fun foo(): String = TODO()

generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/directives/requiresscope/RequiresScopesDirectiveTest.kt

+26
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.expediagroup.graphql.generator.TopLevelObject
2020
import com.expediagroup.graphql.generator.extensions.print
2121
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorConfig
2222
import com.expediagroup.graphql.generator.federation.FederatedSchemaGeneratorHooks
23+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC
24+
import com.expediagroup.graphql.generator.federation.directives.FEDERATION_SPEC_URL_PREFIX
2325
import com.expediagroup.graphql.generator.federation.directives.REQUIRES_SCOPE_DIRECTIVE_NAME
2426
import com.expediagroup.graphql.generator.federation.directives.RequiresScopesDirective
2527
import com.expediagroup.graphql.generator.federation.directives.Scope
@@ -101,6 +103,30 @@ class RequiresScopesDirectiveTest {
101103
assertNotNull(fooQuery.hasAppliedDirective(REQUIRES_SCOPE_DIRECTIVE_NAME))
102104
}
103105

106+
@Test
107+
fun `verify ComposeDirective is not created for federation v2_0`() {
108+
val config = FederatedSchemaGeneratorConfig(
109+
supportedPackages = listOf("com.expediagroup.graphql.generator.federation.directives.requiresscope"),
110+
hooks = FederatedSchemaGeneratorHooks(emptyList()).apply {
111+
this.linkSpecs[FEDERATION_SPEC] = FederatedSchemaGeneratorHooks.LinkSpec(
112+
namespace = FEDERATION_SPEC,
113+
imports = emptyMap(),
114+
url = "$FEDERATION_SPEC_URL_PREFIX/v2.0"
115+
)
116+
}
117+
)
118+
val exception = Assertions.assertThrows(IllegalArgumentException::class.java) {
119+
toFederatedSchema(
120+
queries = listOf(TopLevelObject(com.expediagroup.graphql.generator.federation.directives.requiresscope.RequiresScopesDirectiveTest.FooQuery())),
121+
config = config
122+
)
123+
}
124+
Assertions.assertEquals(
125+
"@requiresScopes directive requires Federation 2.7 or later, but version https://specs.apollo.dev/federation/v2.0 was specified",
126+
exception.message
127+
)
128+
}
129+
104130
class FooQuery {
105131
@RequiresScopesDirective(scopes = [Scopes([Scope("scope1"), Scope("scope2")]), Scopes([Scope("scope3")])])
106132
fun foo(): String = TODO()

0 commit comments

Comments
 (0)