Skip to content

Commit 0c93dfa

Browse files
authored
Add valid locations annotation (ExpediaGroup#1132)
* Add valid locations annotation * Update annotation input * Mark function as interna;
1 parent b1284b4 commit 0c93dfa

File tree

10 files changed

+265
-6
lines changed

10 files changed

+265
-6
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2021 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.generator.annotations
18+
19+
/**
20+
* Kotlin classes can be used as GraphQL input types, output types, or both.
21+
* If you want to enforce that a Kotlin class only be used as an input or output type,
22+
* include this annotation with the preferred location parameter.
23+
*
24+
* By default, classes will be allowed as both input and output types.
25+
*/
26+
annotation class GraphQLValidObjectLocations(val locations: Array<Locations>) {
27+
enum class Locations {
28+
OBJECT,
29+
INPUT_OBJECT
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* Copyright 2021 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.generator.exceptions
18+
19+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
20+
import kotlin.reflect.KClass
21+
22+
/**
23+
* Thrown when the schema is using a Kotlin class in an invalid location marked by [GraphQLValidObjectLocations].
24+
*/
25+
class InvalidObjectLocationException(kClass: KClass<*>, validLocations: Array<GraphQLValidObjectLocations.Locations>) :
26+
GraphQLKotlinException("The class $kClass was used in an invalid location. Only $validLocations are allowed")

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateInputObject.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,19 @@
1717
package com.expediagroup.graphql.generator.internal.types
1818

1919
import com.expediagroup.graphql.generator.SchemaGenerator
20+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
2021
import com.expediagroup.graphql.generator.internal.extensions.getGraphQLDescription
2122
import com.expediagroup.graphql.generator.internal.extensions.getSimpleName
2223
import com.expediagroup.graphql.generator.internal.extensions.getValidProperties
2324
import com.expediagroup.graphql.generator.internal.extensions.safeCast
25+
import com.expediagroup.graphql.generator.internal.types.utils.validateObjectLocation
2426
import graphql.introspection.Introspection.DirectiveLocation
2527
import graphql.schema.GraphQLInputObjectType
2628
import kotlin.reflect.KClass
2729

2830
internal fun generateInputObject(generator: SchemaGenerator, kClass: KClass<*>): GraphQLInputObjectType {
31+
validateObjectLocation(kClass, GraphQLValidObjectLocations.Locations.INPUT_OBJECT)
32+
2933
val builder = GraphQLInputObjectType.newInputObject()
3034

3135
builder.name(kClass.getSimpleName(isInputClass = true))

generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateObject.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
package com.expediagroup.graphql.generator.internal.types
1818

1919
import com.expediagroup.graphql.generator.SchemaGenerator
20+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
2021
import com.expediagroup.graphql.generator.extensions.unwrapType
2122
import com.expediagroup.graphql.generator.internal.extensions.getGraphQLDescription
2223
import com.expediagroup.graphql.generator.internal.extensions.getSimpleName
2324
import com.expediagroup.graphql.generator.internal.extensions.getValidFunctions
2425
import com.expediagroup.graphql.generator.internal.extensions.getValidProperties
2526
import com.expediagroup.graphql.generator.internal.extensions.getValidSuperclasses
2627
import com.expediagroup.graphql.generator.internal.extensions.safeCast
28+
import com.expediagroup.graphql.generator.internal.types.utils.validateObjectLocation
2729
import graphql.introspection.Introspection.DirectiveLocation
2830
import graphql.schema.GraphQLInterfaceType
2931
import graphql.schema.GraphQLObjectType
@@ -32,6 +34,8 @@ import kotlin.reflect.KClass
3234
import kotlin.reflect.full.createType
3335

3436
internal fun generateObject(generator: SchemaGenerator, kClass: KClass<*>): GraphQLObjectType {
37+
validateObjectLocation(kClass, GraphQLValidObjectLocations.Locations.OBJECT)
38+
3539
val builder = GraphQLObjectType.newObject()
3640

3741
val name = kClass.getSimpleName()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2021 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.generator.internal.types.utils
18+
19+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
20+
import com.expediagroup.graphql.generator.exceptions.InvalidObjectLocationException
21+
import kotlin.reflect.KClass
22+
import kotlin.reflect.full.findAnnotation
23+
24+
/**
25+
* Throws an exception if this KClass was used in an invalid location
26+
*/
27+
internal fun validateObjectLocation(kClass: KClass<*>, location: GraphQLValidObjectLocations.Locations) {
28+
kClass.findAnnotation<GraphQLValidObjectLocations>()?.let { annotation ->
29+
val validLocations = annotation.locations
30+
if (!validLocations.contains(location)) {
31+
throw InvalidObjectLocationException(kClass, validLocations)
32+
}
33+
}
34+
}

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateInputObjectTest.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,41 @@ package com.expediagroup.graphql.generator.internal.types
1818

1919
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
2020
import com.expediagroup.graphql.generator.annotations.GraphQLName
21+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
22+
import com.expediagroup.graphql.generator.exceptions.InvalidObjectLocationException
2123
import com.expediagroup.graphql.generator.test.utils.SimpleDirective
2224
import org.junit.jupiter.api.Test
25+
import org.junit.jupiter.api.assertDoesNotThrow
2326
import kotlin.test.assertEquals
27+
import kotlin.test.assertFailsWith
2428

25-
internal class GenerateInputObjectTest : TypeTestHelper() {
29+
class GenerateInputObjectTest : TypeTestHelper() {
2630

2731
@Suppress("Detekt.UnusedPrivateClass")
2832
@GraphQLDescription("The truth")
2933
@SimpleDirective
30-
private class InputClass {
34+
class InputClass {
3135
@SimpleDirective
3236
val myField: String = "car"
3337
}
3438

3539
@Suppress("Detekt.UnusedPrivateClass")
3640
@GraphQLName("InputClassRenamed")
37-
private class InputClassCustomName {
41+
class InputClassCustomName {
3842
@GraphQLName("myFieldRenamed")
3943
val myField: String = "car"
4044
}
4145

46+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
47+
class InputOnly {
48+
val myField: String = "car"
49+
}
50+
51+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
52+
class OutputOnly {
53+
val myField: String = "car"
54+
}
55+
4256
@Test
4357
fun `Test naming`() {
4458
val result = generateInputObject(generator, InputClass::class)
@@ -77,4 +91,18 @@ internal class GenerateInputObjectTest : TypeTestHelper() {
7791
assertEquals(1, result.fields.first().directives.size)
7892
assertEquals("simpleDirective", result.fields.first().directives.first().name)
7993
}
94+
95+
@Test
96+
fun `input only objects are generated`() {
97+
assertDoesNotThrow {
98+
generateInputObject(generator, InputOnly::class)
99+
}
100+
}
101+
102+
@Test
103+
fun `output only objects throw an exception`() {
104+
assertFailsWith(InvalidObjectLocationException::class) {
105+
generateInputObject(generator, OutputOnly::class)
106+
}
107+
}
80108
}

generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateObjectTest.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,33 +19,47 @@ package com.expediagroup.graphql.generator.internal.types
1919
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
2020
import com.expediagroup.graphql.generator.annotations.GraphQLDirective
2121
import com.expediagroup.graphql.generator.annotations.GraphQLName
22+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
23+
import com.expediagroup.graphql.generator.exceptions.InvalidObjectLocationException
2224
import graphql.Scalars
2325
import graphql.introspection.Introspection
2426
import graphql.schema.GraphQLNonNull
2527
import graphql.schema.GraphQLObjectType
2628
import org.junit.jupiter.api.Test
29+
import org.junit.jupiter.api.assertDoesNotThrow
2730
import kotlin.test.assertEquals
31+
import kotlin.test.assertFailsWith
2832
import kotlin.test.assertNotNull
2933
import kotlin.test.assertTrue
3034

3135
class GenerateObjectTest : TypeTestHelper() {
3236

3337
@GraphQLDirective(locations = [Introspection.DirectiveLocation.OBJECT])
34-
internal annotation class ObjectDirective(val arg: String)
38+
annotation class ObjectDirective(val arg: String)
3539

3640
@GraphQLDescription("The truth")
3741
@ObjectDirective("Don't worry")
38-
private class BeHappy
42+
class BeHappy
3943

4044
@GraphQLName("BeHappyRenamed")
41-
private class BeHappyCustomName
45+
class BeHappyCustomName
4246

4347
interface MyInterface {
4448
val foo: String
4549
}
4650

4751
class ClassWithInterface(override val foo: String) : MyInterface
4852

53+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.INPUT_OBJECT])
54+
class InputOnly {
55+
val myField: String = "car"
56+
}
57+
58+
@GraphQLValidObjectLocations(locations = [GraphQLValidObjectLocations.Locations.OBJECT])
59+
class OutputOnly {
60+
val myField: String = "car"
61+
}
62+
4963
@Test
5064
fun `Test naming`() {
5165
val result = generateObject(generator, BeHappy::class) as? GraphQLObjectType
@@ -91,4 +105,18 @@ class GenerateObjectTest : TypeTestHelper() {
91105
assertEquals(1, result.interfaces.size)
92106
assertEquals(1, result.fieldDefinitions.size)
93107
}
108+
109+
@Test
110+
fun `input only objects are generated`() {
111+
assertDoesNotThrow {
112+
generateInputObject(generator, InputOnly::class)
113+
}
114+
}
115+
116+
@Test
117+
fun `output only objects throw an exception`() {
118+
assertFailsWith(InvalidObjectLocationException::class) {
119+
generateInputObject(generator, OutputOnly::class)
120+
}
121+
}
94122
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2021 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.generator.internal.types.utils
18+
19+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations
20+
import com.expediagroup.graphql.generator.annotations.GraphQLValidObjectLocations.Locations
21+
import com.expediagroup.graphql.generator.exceptions.InvalidObjectLocationException
22+
import org.junit.jupiter.api.Test
23+
import org.junit.jupiter.api.assertDoesNotThrow
24+
import kotlin.test.assertFailsWith
25+
26+
class ValidateObjectLocationKtTest {
27+
28+
class SimpleClass
29+
30+
@GraphQLValidObjectLocations([Locations.INPUT_OBJECT, Locations.OBJECT])
31+
class InputAndOutput
32+
33+
@GraphQLValidObjectLocations([Locations.INPUT_OBJECT])
34+
class InputOnly
35+
36+
@GraphQLValidObjectLocations([Locations.OBJECT])
37+
class OutputOnly
38+
39+
@Test
40+
fun `does nothing on class missing annotation`() {
41+
assertDoesNotThrow {
42+
validateObjectLocation(SimpleClass::class, Locations.INPUT_OBJECT)
43+
validateObjectLocation(SimpleClass::class, Locations.OBJECT)
44+
}
45+
}
46+
47+
@Test
48+
fun `allows all locations on class with all locations`() {
49+
assertDoesNotThrow {
50+
validateObjectLocation(InputAndOutput::class, Locations.INPUT_OBJECT)
51+
validateObjectLocation(InputAndOutput::class, Locations.OBJECT)
52+
}
53+
}
54+
55+
@Test
56+
fun `validates input only classes`() {
57+
assertDoesNotThrow {
58+
validateObjectLocation(InputOnly::class, Locations.INPUT_OBJECT)
59+
}
60+
assertFailsWith(InvalidObjectLocationException::class) {
61+
validateObjectLocation(InputOnly::class, Locations.OBJECT)
62+
}
63+
}
64+
65+
@Test
66+
fun `validates output only classes`() {
67+
assertDoesNotThrow {
68+
validateObjectLocation(OutputOnly::class, Locations.OBJECT)
69+
}
70+
assertFailsWith(InvalidObjectLocationException::class) {
71+
validateObjectLocation(OutputOnly::class, Locations.INPUT_OBJECT)
72+
}
73+
}
74+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
id: restricting-input-output
3+
title: Restricting Input and Output Types
4+
---
5+
6+
Since we are using Kotlin classes to represent both GraphQL input and output objects we can use the same class for both and the generator will handle type conflicts.
7+
8+
If you want to enforce that a type should never be used as an input or output you can use the `@GraphQLValidObjectLocations` annotation.
9+
If the class was used in the schema in an invalid location an exception will be thrown.
10+
11+
```kotlin
12+
class SimpleClass(val value: String)
13+
14+
@GraphQLValidObjectLocations([Locations.INPUT_OBJECT])
15+
class InputOnly(val value: String)
16+
17+
@GraphQLValidObjectLocations([Locations.OBJECT])
18+
class OutputOnly(val value: String)
19+
20+
// Valid Usage
21+
fun output1() = SimpleClass("foo")
22+
fun output2() = OutputOnly("foo")
23+
fun input1(input: SimpleClass) = "value was ${input.value}"
24+
fun input2(input: InputOnly) = "value was ${input.value}"
25+
26+
// Throws Exception
27+
fun output3() = InputOnly("foo")
28+
fun input3(input: OutputOnly) = "value was ${input.value}"
29+
```

website/sidebars.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
"schema-generator/customizing-schemas/renaming-fields",
3434
"schema-generator/customizing-schemas/directives",
3535
"schema-generator/customizing-schemas/deprecating-schema",
36+
"schema-generator/customizing-schemas/restricting-input-output",
3637
"schema-generator/customizing-schemas/advanced-features"
3738
]
3839
},

0 commit comments

Comments
 (0)