diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt index 231ad8b..4c0232c 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt @@ -724,12 +724,13 @@ open class Parser { /** * InputValueDefinition : - * - Description? Name : Type DefaultValue? Directives{Const}? + * - Description? Name ArgumentsDefinition? : Type DefaultValue? Directives{Const}? */ private fun parseInputValueDef(): InputValueDefinitionNode { val start = lexer.token val description = parseDescription() val name = parseName() + val args = parseArgumentDefs() expectToken(COLON) val type = parseTypeReference() val defaultValue = expectOptionalToken(EQUALS)?.let { parseValueLiteral(true) } @@ -737,6 +738,7 @@ open class Parser { return InputValueDefinitionNode( description = description, + arguments = args, name = name, type = type, defaultValue = defaultValue, diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt index e86403e..5d7a153 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt @@ -209,7 +209,7 @@ class SchemaBuilder internal constructor() { fun inputType(kClass: KClass, block: InputTypeDSL.() -> Unit) { val input = InputTypeDSL(kClass).apply(block) - model.addInputObject(TypeDef.Input(input.name, kClass, input.description)) + model.addInputObject(input.toKQLObject()) } inline fun inputType(noinline block: InputTypeDSL.() -> Unit = {}) { diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt index feaaec0..099c5e9 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt @@ -2,9 +2,33 @@ package com.apurebase.kgraphql.schema.dsl.types import com.apurebase.kgraphql.defaultKQLTypeName import com.apurebase.kgraphql.schema.dsl.ItemDSL +import com.apurebase.kgraphql.schema.dsl.KotlinPropertyDSL +import com.apurebase.kgraphql.schema.model.PropertyDef +import com.apurebase.kgraphql.schema.model.TypeDef import kotlin.reflect.KClass +import kotlin.reflect.KProperty1 class InputTypeDSL(val kClass: KClass) : ItemDSL() { var name = kClass.defaultKQLTypeName() + + private val kotlinProperties = mutableMapOf, PropertyDef.Kotlin>() + + fun property(kProperty: KProperty1, block: KotlinPropertyDSL.() -> Unit) { + val dsl = KotlinPropertyDSL(kProperty, block) + kotlinProperties[kProperty] = dsl.toKQLProperty() + } + + fun KProperty1.configure(block: KotlinPropertyDSL.() -> Unit) { + property(this, block) + } + + internal fun toKQLObject(): TypeDef.Input { + return TypeDef.Input( + name = name, + kClass = kClass, + kotlinProperties = kotlinProperties.toMap(), + description = description + ) + } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__InputValue.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__InputValue.kt index a422477..e6246ed 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__InputValue.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__InputValue.kt @@ -1,6 +1,8 @@ package com.apurebase.kgraphql.schema.introspection -interface __InputValue : __Described { +import com.apurebase.kgraphql.schema.model.Depreciable + +interface __InputValue : Depreciable, __Described { val type: __Type diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutableSchemaDefinition.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutableSchemaDefinition.kt index d9a7db8..13fb8ed 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutableSchemaDefinition.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/MutableSchemaDefinition.kt @@ -10,6 +10,7 @@ import com.apurebase.kgraphql.schema.introspection.TypeKind import com.apurebase.kgraphql.schema.introspection.__Directive import com.apurebase.kgraphql.schema.introspection.__EnumValue import com.apurebase.kgraphql.schema.introspection.__Field +import com.apurebase.kgraphql.schema.introspection.__InputValue import com.apurebase.kgraphql.schema.introspection.__Schema import com.apurebase.kgraphql.schema.introspection.__Type import kotlin.reflect.KClass @@ -23,7 +24,8 @@ data class MutableSchemaDefinition( private val objects: ArrayList> = arrayListOf( TypeDef.Object(__Schema::class.defaultKQLTypeName(), __Schema::class), create__TypeDefinition(), - create__DirectiveDefinition() + create__DirectiveDefinition(), + create__FieldDefinition() ), private val queries: ArrayList> = arrayListOf(), private val scalars: ArrayList> = arrayListOf( @@ -161,12 +163,37 @@ data class MutableSchemaDefinition( } } +private fun create__FieldDefinition() = TypeDSL(emptyList(), __Field::class).apply { + transformation(__Field::args) { args: List<__InputValue>, includeDeprecated: Boolean? -> + if (includeDeprecated == true) { + args + } else { + args.filterNot { it.isDeprecated } + } + } +}.toKQLObject() + private fun create__TypeDefinition() = TypeDSL(emptyList(), __Type::class).apply { transformation(__Type::fields) { fields: List<__Field>?, includeDeprecated: Boolean? -> - if (includeDeprecated == true) fields else fields?.filterNot { it.isDeprecated } + if (includeDeprecated == true) { + fields + } else { + fields?.filterNot { it.isDeprecated } + } + } + transformation(__Type::inputFields) { fields: List<__InputValue>?, includeDeprecated: Boolean? -> + if (includeDeprecated == true) { + fields + } else { + fields?.filterNot { it.isDeprecated } + } } transformation(__Type::enumValues) { enumValues: List<__EnumValue>?, includeDeprecated: Boolean? -> - if (includeDeprecated == true) enumValues else enumValues?.filterNot { it.isDeprecated } + if (includeDeprecated == true) { + enumValues + } else { + enumValues?.filterNot { it.isDeprecated } + } } }.toKQLObject() @@ -200,6 +227,13 @@ private fun create__DirectiveDefinition() = TypeDSL( } deprecate("Use `locations`.") } + transformation(__Directive::args) { args: List<__InputValue>, includeDeprecated: Boolean? -> + if (includeDeprecated == true) { + args + } else { + args.filterNot { it.isDeprecated } + } + } }.toKQLObject() private fun List.containsAny(vararg elements: T) = elements.any { this.contains(it) } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt index 01ea1bd..5af57c7 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt @@ -34,6 +34,7 @@ interface TypeDef { class Input( name: String, override val kClass: KClass, + val kotlinProperties: Map, PropertyDef.Kotlin> = emptyMap(), description: String? = null ) : BaseKQLType(name, description), Kotlin diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/InputValueDefinitionNode.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/InputValueDefinitionNode.kt index a972a6d..27c8505 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/InputValueDefinitionNode.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/InputValueDefinitionNode.kt @@ -6,6 +6,7 @@ data class InputValueDefinitionNode( override val loc: Location?, val name: NameNode, val description: StringValueNode?, + val arguments: List?, val type: TypeNode, val defaultValue: ValueNode?, val directives: List? diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/InputValue.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/InputValue.kt index f384725..fc9062d 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/InputValue.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/InputValue.kt @@ -14,5 +14,9 @@ class InputValue( override val description: String? = valueDef.description + override val isDeprecated: Boolean = valueDef.isDeprecated + + override val deprecationReason: String? = valueDef.deprecationReason + val default: T? = valueDef.defaultValue } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt index 536f18f..9b3be83 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt @@ -340,7 +340,12 @@ class SchemaCompilation( inputTypeProxies[kClass] = typeProxy val fields = if (kClass.findAnnotation() == null) { - kClass.memberProperties.map { property -> handleKotlinInputProperty(property) } + kClass.memberProperties.map { property -> + handleKotlinInputProperty( + kProperty = property, + kqlProperty = inputObjectDef.kotlinProperties[property] + ) + } } else { listOf() } @@ -366,6 +371,9 @@ class SchemaCompilation( val inputValue = inputValues.find { it.name == name } val kqlInput = inputValue ?: InputValueDef(kType.jvmErasure, name) val inputType = handlePossiblyWrappedType(inputValue?.kType ?: kType, TypeCategory.INPUT) + if (kqlInput.isDeprecated && !inputType.isNullable()) { + throw SchemaException("Required arguments cannot be marked as deprecated") + } InputValue(kqlInput, inputType) } } @@ -391,9 +399,24 @@ class SchemaCompilation( return unionType } - private suspend fun handleKotlinInputProperty(kProperty: KProperty1<*, *>): InputValue<*> { + private suspend fun handleKotlinInputProperty( + kProperty: KProperty1, + kqlProperty: PropertyDef.Kotlin<*, *>? + ): InputValue<*> { val type = handlePossiblyWrappedType(kProperty.returnType, TypeCategory.INPUT) - return InputValue(InputValueDef(kProperty.returnType.jvmErasure, kProperty.name), type) + val actualKqlProperty = kqlProperty ?: PropertyDef.Kotlin(kProperty) + if (actualKqlProperty.isDeprecated && !type.isNullable()) { + throw SchemaException("Required fields cannot be marked as deprecated") + } + return InputValue( + InputValueDef( + kProperty.returnType.jvmErasure, + kProperty.name, + description = actualKqlProperty.description, + isDeprecated = actualKqlProperty.isDeprecated, + deprecationReason = actualKqlProperty.deprecationReason + ), type + ) } private suspend fun handleKotlinProperty( diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt index 505d5b6..da7e9b6 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt @@ -35,19 +35,18 @@ abstract class BaseSchemaTest { name description locations - args { + args(includeDeprecated: true) { ...InputValue } } } } - fragment FullType on __Type { fields(includeDeprecated: true) { name description - args { + args(includeDeprecated: true) { ...InputValue } type { @@ -56,7 +55,7 @@ abstract class BaseSchemaTest { isDeprecated deprecationReason } - inputFields { + inputFields(includeDeprecated: true) { ...InputValue } interfaces { @@ -78,6 +77,8 @@ abstract class BaseSchemaTest { description type { ...TypeRef } defaultValue + isDeprecated + deprecationReason } fragment TypeRef on __Type { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt index ef05c6b..bdf3610 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt @@ -2,15 +2,18 @@ package com.apurebase.kgraphql.specification.introspection import com.apurebase.kgraphql.defaultSchema import com.apurebase.kgraphql.deserialize +import com.apurebase.kgraphql.expect import com.apurebase.kgraphql.extract +import com.apurebase.kgraphql.schema.SchemaException import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat import org.junit.jupiter.api.Test +import kotlin.reflect.typeOf class DeprecationSpecificationTest { @Test - fun `queries may be documented`() { + fun `queries may be deprecated`() { val expected = "sample query" val schema = defaultSchema { query("sample") { @@ -26,7 +29,7 @@ class DeprecationSpecificationTest { } @Test - fun `mutations may be documented`() { + fun `mutations may be deprecated`() { val expected = "sample mutation" val schema = defaultSchema { mutation("sample") { @@ -44,7 +47,7 @@ class DeprecationSpecificationTest { data class Sample(val content: String) @Test - fun `kotlin field may be documented`() { + fun `kotlin field may be deprecated`() { val expected = "sample type" val schema = defaultSchema { query("sample") { @@ -65,9 +68,8 @@ class DeprecationSpecificationTest { } @Test - fun `extension field may be documented`() { + fun `extension field may be deprecated`() { val expected = "sample type" - val expectedDescription = "add operation" val schema = defaultSchema { query("sample") { resolver { "SAMPLE" } @@ -75,7 +77,6 @@ class DeprecationSpecificationTest { type { property("add") { - description = expectedDescription deprecate(expected) resolver { (content) -> content.uppercase() } } @@ -83,9 +84,8 @@ class DeprecationSpecificationTest { } val response = - deserialize(schema.executeBlocking("{__type(name: \"Sample\"){fields(includeDeprecated: true){name, description, isDeprecated, deprecationReason}}}")) + deserialize(schema.executeBlocking("{__type(name: \"Sample\"){fields(includeDeprecated: true){name, isDeprecated, deprecationReason}}}")) assertThat(response.extract("data/__type/fields[1]/deprecationReason"), equalTo(expected)) - assertThat(response.extract("data/__type/fields[1]/description"), equalTo(expectedDescription)) assertThat(response.extract("data/__type/fields[1]/isDeprecated"), equalTo(true)) } @@ -114,23 +114,123 @@ class DeprecationSpecificationTest { assertThat(response.extract("data/__type/enumValues[0]/isDeprecated"), equalTo(true)) } - data class Documented(val id: Int) + @Test + fun `optional input value may be deprecated`() { + data class InputType(val oldOptional: String?, val new: String) + + val expected = "deprecated input value" + val schema = defaultSchema { + inputType { + InputType::oldOptional.configure { + deprecate(expected) + } + } + } + + val response = + deserialize(schema.executeBlocking("{__type(name: \"InputType\"){inputFields(includeDeprecated: true){name, deprecationReason, isDeprecated}}}")) + assertThat(response.extract("data/__type/inputFields[0]/name"), equalTo("new")) + assertThat(response.extract("data/__type/inputFields[0]/deprecationReason"), equalTo(null)) + assertThat(response.extract("data/__type/inputFields[0]/isDeprecated"), equalTo(false)) + assertThat(response.extract("data/__type/inputFields[1]/name"), equalTo("oldOptional")) + assertThat(response.extract("data/__type/inputFields[1]/deprecationReason"), equalTo(expected)) + assertThat(response.extract("data/__type/inputFields[1]/isDeprecated"), equalTo(true)) + } @Test - fun `type may be documented`() { - val expected = "very documented type" + fun `required input value may not be deprecated`() { + data class InputType(val oldRequired: String, val new: String) + + expect("Required fields cannot be marked as deprecated") { + defaultSchema { + inputType { + InputType::oldRequired.configure { + deprecate("deprecated input value") + } + } + } + } + } + + @Test + fun `deprecated input values should not be returned by default`() { + data class InputType(val oldOptional: String?, val new: String) + + val expected = "deprecated input value" val schema = defaultSchema { - query("documented") { - resolver { -> Documented(1) } + inputType { + InputType::oldOptional.configure { + deprecate(expected) + } } + } - type { - description = "very documented type" + val response = + deserialize(schema.executeBlocking("{__type(name: \"InputType\"){inputFields{name, deprecationReason, isDeprecated}}}")) + assertThat(response.extract("data/__type/inputFields[0]/name"), equalTo("new")) + assertThat(response.extract("data/__type/inputFields[0]/deprecationReason"), equalTo(null)) + assertThat(response.extract("data/__type/inputFields[0]/isDeprecated"), equalTo(false)) + // oldOptional should not be returned + assertThat(response.contains("data/__type/inputFields[1]"), equalTo(false)) + } + + @Test + fun `optional field args may be deprecated`() { + val expected = "deprecated field arg" + @Suppress("UNUSED_ANONYMOUS_PARAMETER") + val schema = defaultSchema { + query("data") { + resolver { oldOptional: String?, new: String -> "" }.withArgs { + arg(String::class, typeOf()) { name = "oldOptional"; deprecate(expected) } + arg { name = "new" } + } + } + } + + val response = + deserialize(schema.executeBlocking("{__schema{queryType{fields{name, args(includeDeprecated: true){name deprecationReason isDeprecated}}}}}")) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[0]/name"), equalTo("oldOptional")) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[0]/deprecationReason"), equalTo(expected)) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[0]/isDeprecated"), equalTo(true)) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[1]/name"), equalTo("new")) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[1]/deprecationReason"), equalTo(null)) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[1]/isDeprecated"), equalTo(false)) + } + + @Test + fun `required field args may not be deprecated`() { + @Suppress("UNUSED_ANONYMOUS_PARAMETER") + expect("Required arguments cannot be marked as deprecated") { + defaultSchema { + query("data") { + resolver { oldRequired: String, new: String -> "" }.withArgs { + arg { name = "oldRequired"; deprecate("deprecated field arg") } + arg { name = "new" } + } + } + } + } + } + + @Test + fun `deprecated field args should not be returned by default`() { + val expected = "deprecated input value" + @Suppress("UNUSED_ANONYMOUS_PARAMETER") + val schema = defaultSchema { + query("data") { + resolver { oldOptional: String?, new: String -> "" }.withArgs { + arg(String::class, typeOf()) { name = "oldOptional"; deprecate(expected) } + arg { name = "new" } + } } } val response = - deserialize(schema.executeBlocking("query { __type(name: \"Documented\") { name, kind, description } }")) - assertThat(response.extract("data/__type/description"), equalTo(expected)) + deserialize(schema.executeBlocking("{__schema{queryType{fields{name, args{name deprecationReason isDeprecated}}}}}")) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[0]/name"), equalTo("new")) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[0]/deprecationReason"), equalTo(null)) + assertThat(response.extract("data/__schema/queryType/fields[0]/args[0]/isDeprecated"), equalTo(false)) + // oldOptional should not be returned + assertThat(response.contains("data/__schema/queryType/fields[0]/args[1]"), equalTo(false)) } } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt index 5a6d8b3..be8706d 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt @@ -134,4 +134,24 @@ class DocumentationSpecificationTest { assertThat(response.extract("data/__type/enumValues[0]/name"), equalTo(SampleEnum.ONE.name)) assertThat(response.extract("data/__type/enumValues[0]/description"), equalTo(expected)) } + + data class Documented(val id: Int) + + @Test + fun `type may be documented`() { + val expected = "very documented type" + val schema = defaultSchema { + query("documented") { + resolver { -> Documented(1) } + } + + type { + description = "very documented type" + } + } + + val response = + deserialize(schema.executeBlocking("query { __type(name: \"Documented\") { name, kind, description } }")) + assertThat(response.extract("data/__type/description"), equalTo(expected)) + } }