diff --git a/docs/content/Plugins/ktor.md b/docs/content/Plugins/ktor.md index 5a0cd7cd..32b0b95b 100644 --- a/docs/content/Plugins/ktor.md +++ b/docs/content/Plugins/ktor.md @@ -90,3 +90,44 @@ schema { } } ``` + +## Schema Definition Language (SDL) + +The [Schema Definition Language](https://graphql.org/learn/schema/#type-language) (or Type System Definition Language) is a human-readable, language-agnostic +representation of a GraphQL schema. + +See the following comparison: + +=== "KGraphQL" + ```kotlin + schema { + data class SampleData( + val id: Int, + val stringData: String, + val optionalList: List? + ) + + query("getSampleData") { + resolver { quantity: Int -> + (1..quantity).map { SampleData(it, "sample-$it", emptyList()) } + }.withArgs { + arg { name = "quantity"; defaultValue = 10 } + } + } + } + ``` +=== "SDL" + ``` + type Query { + getSampleData(quantity: Int! = 10): [SampleData!]! + } + + type SampleData { + id: Int! + optionalList: [String!] + stringData: String! + } + ``` + +If schema introspection is enabled, the ktor feature will expose the current schema in Schema Definition +Language under [http://localhost:8080/graphql?schema](http://localhost:8080/graphql?schema). diff --git a/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt b/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt index f314fd31..66c5044c 100644 --- a/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt +++ b/kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt @@ -61,6 +61,12 @@ class GraphQL(val schema: Schema) { } class FeatureInstance(featureKey: String = "KGraphQL") : Plugin { + companion object { + private val playgroundHtml: ByteArray? by lazy { + KtorGraphQLConfiguration::class.java.classLoader.getResource("playground.html")?.readBytes() + } + } + override val key = AttributeKey(featureKey) override fun install(pipeline: Application, configure: Configuration.() -> Unit): GraphQL { @@ -87,12 +93,15 @@ class GraphQL(val schema: Schema) { ) call.respondText(result, contentType = ContentType.Application.Json) } - if (config.playground) get { - @Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") - val playgroundHtml = - KtorGraphQLConfiguration::class.java.classLoader.getResource("playground.html") - .readBytes() - call.respondBytes(playgroundHtml, contentType = ContentType.Text.Html) + get { + val schemaRequested = call.request.queryParameters["schema"] != null + if (schemaRequested && config.introspection) { + call.respondText(schema.printSchema()) + } else if (config.playground) { + playgroundHtml?.let { + call.respondBytes(it, contentType = ContentType.Text.Html) + } + } } } } diff --git a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorMultipleEndpoints.kt b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorMultipleEndpoints.kt index 89117a73..0db11ada 100644 --- a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorMultipleEndpoints.kt +++ b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorMultipleEndpoints.kt @@ -57,7 +57,7 @@ class KtorMultipleEndpoints : KtorTest() { } } - client.get("/graphql").status shouldBeEqualTo HttpStatusCode.MethodNotAllowed + client.get("/graphql").status shouldBeEqualTo HttpStatusCode.NotFound } @Test @@ -79,4 +79,38 @@ class KtorMultipleEndpoints : KtorTest() { response.status shouldBeEqualTo HttpStatusCode.OK response.bodyAsText() shouldBeEqualTo playgroundHtml } + + @Test + fun `SDL should be provided by default`() = testApplication { + install(GraphQL) { + schema { + query("check") { + resolver { -> "OK" } + } + } + } + + val response = client.get("/graphql?schema") + response.status shouldBeEqualTo HttpStatusCode.OK + response.bodyAsText() shouldBeEqualTo """ + type Query { + check: String! + } + + """.trimIndent() + } + + @Test + fun `SDL should not be provided when introspection is disabled`() = testApplication { + install(GraphQL) { + introspection = false + schema { + query("check") { + resolver { -> "OK" } + } + } + } + + client.get("/graphql?schema").status shouldBeEqualTo HttpStatusCode.NotFound + } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt index 3ebdd1cf..fc7ac610 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt @@ -18,9 +18,10 @@ data class SchemaConfiguration( val wrapErrors: Boolean, val executor: Executor, val timeout: Long?, + // allow schema introspection val introspection: Boolean = true, val plugins: MutableMap, Any>, - val genericTypeResolver: GenericTypeResolver, + val genericTypeResolver: GenericTypeResolver ) { @Suppress("UNCHECKED_CAST") operator fun get(type: KClass) = plugins[type] as T? 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 4c0232c0..cf456dc3 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/request/Parser.kt @@ -178,7 +178,11 @@ open class Parser { return VariableDefinitionNode( variable = parseVariable(), type = expectToken(COLON).let { parseTypeReference() }, - defaultValue = if (expectOptionalToken(EQUALS) != null) parseValueLiteral(true) else null, + defaultValue = if (expectOptionalToken(EQUALS) != null) { + parseValueLiteral(true) + } else { + null + }, directives = parseDirectives(true), loc = loc(start) ) @@ -545,7 +549,11 @@ open class Parser { */ private fun parseTypeSystemDefinition(): DefinitionNode.TypeSystemDefinitionNode { // Many definitions begin with a description and require a lookahead. - val keywordToken = if (peekDescription()) lexer.lookahead() else lexer.token + val keywordToken = if (peekDescription()) { + lexer.lookahead() + } else { + lexer.token + } if (keywordToken.kind == NAME) { return when (keywordToken.value) { @@ -937,6 +945,7 @@ open class Parser { * `FRAGMENT_DEFINITION` * `FRAGMENT_SPREAD` * `INLINE_FRAGMENT` + * `VARIABLE_DEFINITION` * * TypeSystemDirectiveLocation : one of * `SCHEMA` diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt index 785b22b1..f41022b5 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt @@ -68,6 +68,8 @@ class DefaultSchema( ) } + override fun printSchema() = SchemaPrinter().print(model) + override fun typeByKClass(kClass: KClass<*>): Type? = model.queryTypes[kClass] override fun typeByKType(kType: KType): Type? = typeByKClass(kType.jvmErasure) diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/Schema.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/Schema.kt index c1d77ad9..a7de3c96 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/Schema.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/Schema.kt @@ -25,4 +25,9 @@ interface Schema : __Schema { options: ExecutionOptions = ExecutionOptions(), operationName: String? = null ) = runBlocking { execute(request, variables, context, options, operationName) } + + /** + * Prints the current schema in schema definition language (SDL) + */ + fun printSchema(): String } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/SchemaPrinter.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/SchemaPrinter.kt new file mode 100644 index 00000000..209acf18 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/SchemaPrinter.kt @@ -0,0 +1,294 @@ +package com.apurebase.kgraphql.schema + +import com.apurebase.kgraphql.request.isIntrospectionType +import com.apurebase.kgraphql.schema.builtin.BuiltInScalars +import com.apurebase.kgraphql.schema.directive.Directive +import com.apurebase.kgraphql.schema.introspection.TypeKind +import com.apurebase.kgraphql.schema.introspection.__Described +import com.apurebase.kgraphql.schema.introspection.__Directive +import com.apurebase.kgraphql.schema.introspection.__InputValue +import com.apurebase.kgraphql.schema.introspection.__Schema +import com.apurebase.kgraphql.schema.introspection.__Type +import com.apurebase.kgraphql.schema.introspection.typeReference +import com.apurebase.kgraphql.schema.model.Depreciable + +data class SchemaPrinterConfig( + /** + * Whether to *always* include the schema itself. Otherwise, it will only be included + * if required by the spec. + */ + val includeSchemaDefinition: Boolean = false, + + /** + * Whether to include descriptions. + */ + val includeDescriptions: Boolean = false, + + /** + * Whether to include built-in directives. + */ + val includeBuiltInDirectives: Boolean = false +) + +class SchemaPrinter(private val config: SchemaPrinterConfig = SchemaPrinterConfig()) { + /** + * Returns the given [schema] in schema definition language (SDL). Types and fields are sorted + * ascending by their name and appear in order of their corresponding spec section, i.e. + * + * - 3.3 Schema + * - 3.4 Types + * - Scalars + * - Objects + * - Interfaces + * - Unions + * - Enums + * - Input Types + * - 3.13 Directives + * + * If descriptions are included, they will use single quotes, broken up on newlines, e.g. + * + * ``` + * "This is a long comment," + * "spanning over multiple" + * "lines." + * scalar DescriptedScalar + * ``` + */ + fun print(schema: __Schema): String { + // All actual (non-introspection) types of the schema + val schemaTypes = + schema.types.filterNot { it.isIntrospectionType() || it.name == null }.sortedByName().groupBy { it.kind } + + // Schema + // https://spec.graphql.org/draft/#sec-Root-Operation-Types.Default-Root-Operation-Type-Names + val schemaDefinition = if (includeSchemaDefinition(schema)) { + buildString { + appendLine("schema {") + val indentation = " " + // The query root operation type must always be provided + appendDescription(schema.queryType, indentation) + appendLine("${indentation}query: ${schema.queryType.name}") + // The mutation root operation type is optional; if it is not provided, the service does not support mutations + schema.mutationType?.let { + appendDescription(it, indentation) + appendLine("${indentation}mutation: ${it.name}") + } + // Similarly, the subscription root operation type is also optional; if it is not provided, the service does not support subscriptions + schema.subscriptionType?.let { + appendDescription(it, indentation) + appendLine("${indentation}subscription: ${it.name}") + } + appendLine("}") + } + } else { + "" + } + + // Scalars + // https://spec.graphql.org/draft/#sec-Scalars.Built-in-Scalars + // "When representing a GraphQL schema using the type system definition language, all built-in scalars must be omitted for brevity." + val scalars = buildString { + schemaTypes[TypeKind.SCALAR]?.filter { !it.isBuiltInScalar() }?.forEachIndexed { index, type -> + if (index > 0) { + appendLine() + } + appendDescription(type) + appendLine("scalar ${type.name}") + } + } + + // Objects (includes Query, Mutation, and Subscription) with non-empty fields + // https://spec.graphql.org/draft/#sec-Objects + val objects = buildString { + schemaTypes[TypeKind.OBJECT]?.filter { !it.fields.isNullOrEmpty() }?.forEachIndexed { index, type -> + if (index > 0) { + appendLine() + } + appendDescription(type) + appendLine("type ${type.name}${type.implements()} {") + appendFields(type) + appendLine("}") + } + } + + // Interfaces + // https://spec.graphql.org/draft/#sec-Interfaces + val interfaces = buildString { + schemaTypes[TypeKind.INTERFACE]?.forEachIndexed { index, type -> + if (index > 0) { + appendLine() + } + appendDescription(type) + appendLine("interface ${type.name}${type.implements()} {") + appendFields(type) + appendLine("}") + } + } + + // Unions + // https://spec.graphql.org/draft/#sec-Unions + val unions = buildString { + schemaTypes[TypeKind.UNION]?.forEachIndexed { index, type -> + if (index > 0) { + appendLine() + } + appendDescription(type) + val possibleTypes = type.possibleTypes.sortedByName().map { it.name } + appendLine("union ${type.name} = ${possibleTypes.joinToString(" | ")}") + } + } + + // Enums + // https://spec.graphql.org/draft/#sec-Enums + val enums = buildString { + schemaTypes[TypeKind.ENUM]?.forEachIndexed { index, type -> + if (index > 0) { + appendLine() + } + appendDescription(type) + appendLine("enum ${type.name} {") + val indentation = " " + type.enumValues.sortedByName().forEach { enumValue -> + appendDescription(enumValue, indentation) + appendLine("${indentation}${enumValue.name}${enumValue.deprecationInfo()}") + } + appendLine("}") + } + } + + // Input Types + // https://spec.graphql.org/draft/#sec-Input-Objects + val inputTypes = buildString { + schemaTypes[TypeKind.INPUT_OBJECT]?.filter { !it.inputFields.isNullOrEmpty() } + ?.forEachIndexed { index, type -> + if (index > 0) { + appendLine() + } + appendDescription(type) + appendLine("input ${type.name}${type.implements()} {") + val indentation = " " + type.inputFields.sortedByName().forEach { field -> + appendDescription(field, indentation) + appendLine("${indentation}${field.name}: ${field.type.typeReference()}${field.deprecationInfo()}") + } + appendLine("}") + } + } + + // Directives + // https://spec.graphql.org/draft/#sec-Type-System.Directives.Built-in-Directives + // "When representing a GraphQL schema using the type system definition language any built-in directive may be omitted for brevity." + val directives = buildString { + schema.directives.filter { config.includeBuiltInDirectives || !it.isBuiltIn() }.sortedByName() + .forEachIndexed { index, directive -> + if (index > 0) { + appendLine() + } + val args = directive.args.takeIf { it.isNotEmpty() } + ?.joinToString(", ", prefix = "(", postfix = ")") { arg -> + arg.name + ": " + arg.type.typeReference() + arg.defaultInfo() + } ?: "" + appendLine("directive @${directive.name}$args on ${directive.locations.joinToString(" | ") { it.name }}") + } + } + + return listOf( + schemaDefinition, + scalars, + objects, + interfaces, + unions, + enums, + inputTypes, + directives + ).filterNot { it.isBlank() }.joinToString("\n") + } + + // Computes whether the schema definition should be included. Returns `true` if enforced by the configuration, or if + // required by the spec: + // "The type system definition language can omit the schema definition when each root operation type uses its respective default root type name and no other type uses any default root type name." + // "Likewise, when representing a GraphQL schema using the type system definition language, a schema definition should be omitted if each root operation type uses its respective default root type name and no other type uses any default root type name." + private fun includeSchemaDefinition(schema: __Schema): Boolean = + config.includeSchemaDefinition || schema.hasRootOperationTypeWithNonDefaultName() || schema.hasRegularTypeWithDefaultRootTypeName() + + private fun __Schema.hasRootOperationTypeWithNonDefaultName(): Boolean = queryType.name != "Query" || + (mutationType != null && mutationType?.name != "Mutation") || + (subscriptionType != null && subscriptionType?.name != "Subscription") + + private fun __Schema.hasRegularTypeWithDefaultRootTypeName() = types.any { + (it != queryType && it.name == "Query") || + (it != mutationType && it.name == "Mutation") || + (it != subscriptionType && it.name == "Subscription") + } + + private fun Depreciable.deprecationInfo(): String = if (isDeprecated) { + " @deprecated(reason: \"$deprecationReason\")" + } else { + "" + } + + private fun __Directive.isBuiltIn(): Boolean = + name in setOf(Directive.DEPRECATED.name, Directive.INCLUDE.name, Directive.SKIP.name) + + private fun __Type.isBuiltInScalar(): Boolean = name in BuiltInScalars.entries.map { it.typeDef.name } + + private fun __Type.implements(): String = + interfaces + .sortedByName() + .mapNotNull { it.name } + .takeIf { it.isNotEmpty() } + ?.joinToString(separator = " & ", prefix = " implements ") ?: "" + + private fun Any.description(): List? = when (this) { + is __Described -> description?.takeIf { it.isNotBlank() }?.lines() + is __Type -> description?.takeIf { it.isNotBlank() }?.lines() + else -> null + } + + // https://spec.graphql.org/October2021/#sec-Descriptions + private fun StringBuilder.appendDescription(type: Any, indentation: String = ""): StringBuilder { + type.description()?.takeIf { config.includeDescriptions }?.let { description -> + description.forEach { + appendLine("$indentation\"$it\"") + } + } + return this + } + + private fun StringBuilder.appendFields(type: __Type): StringBuilder { + val indentation = " " + type.fields.sortedByName().forEach { field -> + appendDescription(field, indentation) + // Write each field arg in it's own line if we have to add directives or description, + // otherwise print them on a single line + if (field.args.any { it.isDeprecated || (config.includeDescriptions && !it.description.isNullOrBlank()) }) { + appendLine("${indentation}${field.name}(") + field.args.forEach { arg -> + val argsIndentation = "$indentation$indentation" + appendDescription(arg, argsIndentation) + appendLine("$argsIndentation${arg.name}: ${arg.type.typeReference()}${arg.defaultInfo()}${arg.deprecationInfo()}") + } + appendLine("${indentation}): ${field.type.typeReference()}${field.deprecationInfo()}") + } else { + val args = + field.args.takeIf { it.isNotEmpty() }?.joinToString(", ", prefix = "(", postfix = ")") { arg -> + arg.name + ": " + arg.type.typeReference() + arg.defaultInfo() + } ?: "" + appendLine("${indentation}${field.name}$args: ${field.type.typeReference()}${field.deprecationInfo()}") + } + } + return this + } + + private fun __InputValue.defaultInfo(): String = defaultValue?.let { + " = $it" + } ?: "" + + @JvmName("sortedTypesByName") + private fun List<__Type>?.sortedByName() = + orEmpty().sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name.toString() }) + + @JvmName("sortedDescribedByName") + private fun List?.sortedByName() = + orEmpty().sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name }) +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/Directive.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/Directive.kt index 13346a6c..f63fd379 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/Directive.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/Directive.kt @@ -1,8 +1,12 @@ package com.apurebase.kgraphql.schema.directive +import com.apurebase.kgraphql.schema.directive.DirectiveLocation.ARGUMENT_DEFINITION +import com.apurebase.kgraphql.schema.directive.DirectiveLocation.ENUM_VALUE import com.apurebase.kgraphql.schema.directive.DirectiveLocation.FIELD +import com.apurebase.kgraphql.schema.directive.DirectiveLocation.FIELD_DEFINITION import com.apurebase.kgraphql.schema.directive.DirectiveLocation.FRAGMENT_SPREAD import com.apurebase.kgraphql.schema.directive.DirectiveLocation.INLINE_FRAGMENT +import com.apurebase.kgraphql.schema.directive.DirectiveLocation.INPUT_FIELD_DEFINITION import com.apurebase.kgraphql.schema.introspection.__Directive import com.apurebase.kgraphql.schema.introspection.__InputValue import com.apurebase.kgraphql.schema.model.FunctionWrapper @@ -42,8 +46,10 @@ data class Directive( companion object { /** - * The @skip directive may be provided for fields, fragment spreads, and inline fragments. - * Allows for conditional exclusion during execution as described by the if argument. + * https://spec.graphql.org/draft/#sec--skip + * + * The `@skip` built-in directive may be provided for fields, fragment spreads, and inline fragments, + * and allows for conditional exclusion during execution as described by the `if` argument. */ val SKIP = Partial( "skip", @@ -52,13 +58,29 @@ data class Directive( ) /** - * The @include directive may be provided for fields, fragment spreads, and inline fragments. - * Allows for conditional inclusion during execution as described by the if argument. + * https://spec.graphql.org/draft/#sec--include + * + * The `@include` built-in directive may be provided for fields, fragment spreads, and inline fragments, + * and allows for conditional inclusion during execution as described by the `if` argument. */ val INCLUDE = Partial( "include", listOf(FIELD, FRAGMENT_SPREAD, INLINE_FRAGMENT), DirectiveExecution(FunctionWrapper.on { `if`: Boolean -> DirectiveResult(`if`) }) ) + + /** + * https://spec.graphql.org/draft/#sec--deprecated + * + * The `@deprecated` built-in directive is used within the type system definition language to indicate + * deprecated portions of a GraphQL service's schema, such as deprecated fields on a type, arguments on + * a field, input fields on an input type, or values of an enum type. + */ + val DEPRECATED = Partial( + "deprecated", + listOf(FIELD_DEFINITION, ARGUMENT_DEFINITION, INPUT_FIELD_DEFINITION, ENUM_VALUE), + // DirectiveExecution is a no-op, since it cannot be used during execution. + DirectiveExecution(FunctionWrapper.on { reason: String? -> DirectiveResult(true) }) + ) } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/DirectiveLocation.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/DirectiveLocation.kt index 8397c889..63c9a351 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/DirectiveLocation.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/directive/DirectiveLocation.kt @@ -1,13 +1,28 @@ package com.apurebase.kgraphql.schema.directive enum class DirectiveLocation { + // ExecutableDirectiveLocation QUERY, MUTATION, SUBSCRIPTION, FIELD, FRAGMENT_DEFINITION, FRAGMENT_SPREAD, - INLINE_FRAGMENT; + INLINE_FRAGMENT, + VARIABLE_DEFINITION, + + // TypeSystemDirectiveLocation + SCHEMA, + SCALAR, + OBJECT, + FIELD_DEFINITION, + ARGUMENT_DEFINITION, + INTERFACE, + UNION, + ENUM, + ENUM_VALUE, + INPUT_OBJECT, + INPUT_FIELD_DEFINITION; companion object { fun from(str: String) = str.lowercase().let { lowered -> diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt index 3a1703b4..fde49f39 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ArgumentTransformer.kt @@ -5,6 +5,7 @@ import com.apurebase.kgraphql.InvalidInputValueException import com.apurebase.kgraphql.request.Variables import com.apurebase.kgraphql.schema.DefaultSchema import com.apurebase.kgraphql.schema.introspection.TypeKind +import com.apurebase.kgraphql.schema.introspection.typeReference import com.apurebase.kgraphql.schema.model.ast.ArgumentNodes import com.apurebase.kgraphql.schema.model.ast.ValueNode import com.apurebase.kgraphql.schema.model.ast.ValueNode.ObjectValueNode @@ -46,7 +47,7 @@ open class ArgumentTransformer(val schema: DefaultSchema) { value == null && parameter.type.kind != TypeKind.NON_NULL -> parameter.default value == null && parameter.type.kind == TypeKind.NON_NULL -> { parameter.default ?: throw InvalidInputValueException( - "argument '${parameter.name}' of type ${schema.typeReference(parameter.type)} on field '$funName' is not nullable, value cannot be null", + "argument '${parameter.name}' of type ${parameter.type.typeReference()} on field '$funName' is not nullable, value cannot be null", executionNode.selectionNode ) } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/SchemaProxy.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/SchemaProxy.kt index 896cda0b..9ed4d035 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/SchemaProxy.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/SchemaProxy.kt @@ -57,4 +57,6 @@ class SchemaProxy( ): String { return getProxied().execute(request, variables, context, options, operationName) } + + override fun printSchema(): String = getProxied().printSchema() } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__Type.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__Type.kt index b083d169..878f162a 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__Type.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/introspection/__Type.kt @@ -7,42 +7,29 @@ package com.apurebase.kgraphql.schema.introspection interface __Type { val kind: TypeKind val name: String? - val description: String + val description: String? - //OBJECT and INTERFACE only + // OBJECT and INTERFACE only val fields: List<__Field>? - //OBJECT and INTERFACE only + // OBJECT and INTERFACE only val interfaces: List<__Type>? - //INTERFACE and UNION only + // INTERFACE and UNION only val possibleTypes: List<__Type>? - //ENUM only + // ENUM only val enumValues: List<__EnumValue>? - //INPUT_OBJECT only + // INPUT_OBJECT only val inputFields: List<__InputValue>? - //NON_NULL and LIST only + // NON_NULL and LIST only val ofType: __Type? } -fun __Type.asString() = buildString { - append(kind) - append(" : ") - append(name) - append(" ") - - if (fields != null) { - append("[") - fields?.forEach { field -> - append(field.name).append(" : ").append(field.type.name ?: field.type.kind).append(" ") - } - append("]") - } - - if (ofType != null) { - append(" => ").append(ofType?.name) - } +fun __Type.typeReference(): String = when (kind) { + TypeKind.NON_NULL -> "${ofType?.typeReference()}!" + TypeKind.LIST -> "[${ofType?.typeReference()}]" + else -> name ?: "" } 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 ea6bf5b0..321d1b34 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 @@ -46,7 +46,8 @@ data class MutableSchemaDefinition( private val unions: ArrayList = arrayListOf(), private val directives: ArrayList = arrayListOf( Directive.SKIP, - Directive.INCLUDE + Directive.INCLUDE, + Directive.DEPRECATED ), private val inputObjects: ArrayList> = arrayListOf() ) { @@ -87,7 +88,7 @@ data class MutableSchemaDefinition( if (scalars.any { it.kClass == member } || enums.any { it.kClass == member }) { throw SchemaException( "The member types of a Union type must all be Object base types; " + - "Scalar, Interface and Union types may not be member types of a Union" + "Scalar, Interface and Union types may not be member types of a Union" ) } @@ -120,7 +121,7 @@ data class MutableSchemaDefinition( fun addSubscription(subscription: SubscriptionDef<*>) { if (subscription.checkEqualName(subscriptions)) { - throw SchemaException("Cannot add mutation with duplicated name ${subscription.name}") + throw SchemaException("Cannot add subscription with duplicated name ${subscription.name}") } subscriptions.add(subscription) } @@ -135,7 +136,7 @@ data class MutableSchemaDefinition( fun addInputObject(input: TypeDef.Input<*>) = addType(input, inputObjects, "Input") - fun addType(type: T, target: ArrayList, typeCategory: String) { + private fun addType(type: T, target: ArrayList, typeCategory: String) { if (type.name.startsWith("__")) { throw SchemaException("Type name starting with \"__\" are excluded for introspection system") } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/LookupSchema.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/LookupSchema.kt index 3b05ef79..d1ca81d3 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/LookupSchema.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/LookupSchema.kt @@ -3,7 +3,6 @@ package com.apurebase.kgraphql.schema.structure import com.apurebase.kgraphql.isIterable import com.apurebase.kgraphql.request.TypeReference import com.apurebase.kgraphql.schema.Schema -import com.apurebase.kgraphql.schema.introspection.TypeKind import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.jvm.jvmErasure @@ -43,11 +42,4 @@ interface LookupSchema : Schema { return TypeReference(name, kType.isMarkedNullable) } } - - fun typeReference(type: Type) = TypeReference( - name = type.unwrapped().name!!, - isNullable = type.isNullable(), - isList = type.isList(), - isElementNullable = type.isList() && type.unwrapList().ofType?.kind == TypeKind.NON_NULL - ) } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaModel.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaModel.kt index 2fb944b6..b71e7dff 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaModel.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaModel.kt @@ -33,7 +33,7 @@ data class SchemaModel( // workaround on the fact that Double and Float are treated as GraphQL Float .filterNot { it is Type.Scalar<*> && it.kClass == Float::class } .filterNot { it.kClass?.findAnnotation() != null } - // query and mutation must be present in introspection 'types' field for introspection tools + // query must be present in introspection 'types' field for introspection tools .plus(query) .toMutableList() if (mutation != null) { diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Type.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Type.kt index 81535fc2..8c211fc9 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Type.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Type.kt @@ -7,7 +7,6 @@ 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.__Type -import com.apurebase.kgraphql.schema.introspection.asString import com.apurebase.kgraphql.schema.model.TypeDef import kotlin.reflect.KClass import kotlin.reflect.KType @@ -77,7 +76,7 @@ interface Type : __Type { class OperationObject( override val name: String, - override val description: String, + override val description: String?, fields: List ) : ComplexType(fields) { @@ -110,7 +109,7 @@ interface Type : __Type { override val name: String = definition.name - override val description: String = definition.description ?: "" + override val description: String? = definition.description override val enumValues: List<__EnumValue>? = null @@ -136,7 +135,7 @@ interface Type : __Type { override val name: String = definition.name - override val description: String = definition.description ?: "" + override val description: String? = definition.description override val enumValues: List<__EnumValue>? = null @@ -159,7 +158,7 @@ interface Type : __Type { override val name: String = kqlType.name - override val description: String = kqlType.description ?: "" + override val description: String? = kqlType.description override val enumValues: List<__EnumValue>? = null @@ -188,7 +187,7 @@ interface Type : __Type { override val name: String = kqlType.name - override val description: String = kqlType.description ?: "" + override val description: String? = kqlType.description override val enumValues: List<__EnumValue> = values @@ -214,7 +213,7 @@ interface Type : __Type { override val name: String = kqlType.name - override val description: String = kqlType.description ?: "" + override val description: String? = kqlType.description override val enumValues: List<__EnumValue>? = null @@ -238,7 +237,7 @@ interface Type : __Type { override val name: String = kqlType.name - override val description: String = kqlType.description ?: "" + override val description: String? = kqlType.description override val enumValues: List<__EnumValue>? = null @@ -260,7 +259,7 @@ interface Type : __Type { override val name: String? = null - override val description: String = "" + override val description: String? = null override val enumValues: List<__EnumValue>? = null @@ -282,7 +281,7 @@ interface Type : __Type { override val name: String? = null - override val description: String = "" + override val description: String? = null override val enumValues: List<__EnumValue>? = null @@ -317,8 +316,6 @@ interface Type : __Type { override val inputFields: List<__InputValue>? = null - override fun toString(): String = asString() - override fun isInstance(value: Any?): Boolean = false } @@ -344,8 +341,6 @@ interface Type : __Type { override val inputFields: List<__InputValue>? = null - override fun toString() = asString() - override fun isInstance(value: Any?): Boolean = false } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/TypeProxy.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/TypeProxy.kt index 6dea57e6..86b9a62b 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/TypeProxy.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/TypeProxy.kt @@ -20,7 +20,7 @@ open class TypeProxy(var proxied: Type) : Type { override val name: String? get() = proxied.name - override val description: String + override val description: String? get() = proxied.description override val fields: List<__Field>? diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaPrinterTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaPrinterTest.kt new file mode 100644 index 00000000..c76792d7 --- /dev/null +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaPrinterTest.kt @@ -0,0 +1,658 @@ +package com.apurebase.kgraphql.schema + +import com.apurebase.kgraphql.KGraphQL +import org.amshove.kluent.shouldBeEqualTo +import org.junit.jupiter.api.Test +import java.time.LocalDate +import java.util.UUID +import kotlin.random.Random +import kotlin.reflect.typeOf + +class SchemaPrinterTest { + + data class Author(val name: String, val books: List) + data class Book(val title: String?, val author: Author) + data class Scenario(val author: String, val content: String) + data class InputObject(val id: Int, val stringInput: String, val intInput: Int, val optional: String?) + data class TestObject(val name: String) + enum class TestEnum { + TYPE1, TYPE2 + } + + data class DeprecatedObject( + val old: String, + val new: String + ) + + interface BaseInterface { + val base: String + } + + interface SimpleInterface : BaseInterface { + val simple: String + } + + interface OtherInterface { + val other1: String? + val other2: List? + } + + data class Simple(override val base: String, override val simple: String, val extra: String) : SimpleInterface + data class Complex( + override val base: String, + override val other1: String?, + override val other2: List?, + val extra: Int + ) : BaseInterface, OtherInterface + + data class NestedLists( + val nested1: List>, + val nested2: List?>>?>>, + val nested3: List>?>>>>>? + ) + + @Test + fun `schema with types should be printed as expected`() { + val schema = KGraphQL.schema { + type() + type() + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Author { + books: [Book!]! + name: String! + } + + type Book { + author: Author! + title: String + } + + """.trimIndent() + } + + @Test + fun `schema with nested lists should be printed as expected`() { + val schema = KGraphQL.schema { + type() + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type NestedLists { + nested1: [[String]!]! + nested2: [[[[[String!]]!]]!]! + nested3: [[[[[[[String]!]]!]!]!]!] + } + + """.trimIndent() + } + + @Test + fun `schema with union types should be printed as expected`() { + val schema = KGraphQL.schema { + query("scenario") { + resolver { -> Scenario("Gamil Kalus", "TOO LONG") } + } + + val linked = unionType("Linked") { + type() + type() + } + + type { + unionProperty("pdf") { + returnType = linked + description = "link to pdf representation of scenario" + resolver { scenario: Scenario -> + if (scenario.author.startsWith("Gamil")) { + Scenario("gambino", "nope") + } else { + Author("Chance", emptyList()) + } + } + } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Author { + books: [Book!]! + name: String! + } + + type Book { + author: Author! + title: String + } + + type Query { + scenario: Scenario! + } + + type Scenario { + author: String! + content: String! + pdf: Linked + } + + union Linked = Author | Scenario + + """.trimIndent() + } + + @Test + fun `schema with interfaces should be printed as expected`() { + val schema = KGraphQL.schema { + type() + type() + type() + type() + type() + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Complex implements BaseInterface & OtherInterface { + base: String! + extra: Int! + other1: String + other2: [String] + } + + type Simple implements BaseInterface & SimpleInterface { + base: String! + extra: String! + simple: String! + } + + interface BaseInterface { + base: String! + } + + interface OtherInterface { + other1: String + other2: [String] + } + + interface SimpleInterface implements BaseInterface { + base: String! + simple: String! + } + + """.trimIndent() + } + + @Test + fun `schema with input types should be printed as expected`() { + val schema = KGraphQL.schema { + mutation("add") { + resolver { inputObject: InputObject -> inputObject.id } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Mutation { + add(inputObject: InputObject!): Int! + } + + input InputObject { + id: Int! + intInput: Int! + optional: String + stringInput: String! + } + + """.trimIndent() + } + + @Test + fun `schema with custom scalars should be printed as expected`() { + val schema = KGraphQL.schema { + stringScalar { + deserialize = UUID::fromString + serialize = UUID::toString + } + stringScalar { + deserialize = LocalDate::parse + serialize = LocalDate::toString + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + scalar LocalDate + + scalar UUID + + """.trimIndent() + } + + @Test + fun `schema with custom type extensions should be printed as expected`() { + val schema = KGraphQL.schema { + type { + property("addedProperty") { + resolver { _ -> "added" } + } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type TestObject { + addedProperty: String! + name: String! + } + + """.trimIndent() + } + + @Test + fun `schema with queries should be printed as expected`() { + val schema = KGraphQL.schema { + query("getString") { + resolver { -> "foo" } + } + query("randomString") { + resolver { possibleReturns: List -> possibleReturns.random() } + } + query("randomInt") { + resolver { min: Int, max: Int? -> Random.nextInt(min, max ?: Integer.MAX_VALUE) } + } + query("getNullString") { + resolver { null } + } + query("getObject") { + resolver { nullObject: Boolean -> + if (nullObject) { + null + } else { + TestObject("foo") + } + } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Query { + getNullString: String + getObject(nullObject: Boolean!): TestObject + getString: String! + randomInt(min: Int!, max: Int): Int! + randomString(possibleReturns: [String!]!): String! + } + + type TestObject { + name: String! + } + + """.trimIndent() + } + + @Test + fun `schema with mutations should be printed as expected`() { + val schema = KGraphQL.schema { + mutation("addString") { + resolver { string: String -> string } + } + mutation("addFloat") { + // Float is Kotlin Double + resolver { float: Double -> float } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Mutation { + addFloat(float: Float!): Float! + addString(string: String!): String! + } + + """.trimIndent() + } + + @Test + fun `schema with enums should be printed as expected`() { + val schema = KGraphQL.schema { + enum() + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + enum TestEnum { + TYPE1 + TYPE2 + } + + """.trimIndent() + } + + @Test + fun `schema with default values should be printed as expected`() { + val schema = KGraphQL.schema { + enum() + + query("getStringWithDefault") { + resolver { type: TestEnum, string: String -> type.name + string }.withArgs { + arg { name = "type"; defaultValue = TestEnum.TYPE1 } + } + } + query("getStringsForTypes") { + resolver { types: List? -> types.orEmpty().map { it.name } }.withArgs { + arg(List::class, typeOf?>()) { + name = "types"; defaultValue = listOf(TestEnum.TYPE1, TestEnum.TYPE2) + } + } + } + mutation("addStringWithDefault") { + resolver { prefix: String, string: String, suffix: String? -> prefix + string + suffix }.withArgs { + arg { name = "prefix"; defaultValue = "\"_\"" } + arg(String::class, typeOf()) { name = "suffix"; defaultValue = null } + } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type Mutation { + addStringWithDefault(prefix: String! = "_", string: String!, suffix: String): String! + } + + type Query { + getStringsForTypes(types: [TestEnum!] = [TYPE1, TYPE2]): [String!]! + getStringWithDefault(type: TestEnum! = TYPE1, string: String!): String! + } + + enum TestEnum { + TYPE1 + TYPE2 + } + + """.trimIndent() + } + + @Test + fun `schema with deprecations should be printed as expected`() { + val schema = KGraphQL.schema { + type { + property(DeprecatedObject::old) { + deprecate("deprecated old value") + } + } + enum { + value(TestEnum.TYPE2) { + deprecate("deprecated enum value") + } + } + mutation("doStuff") { + resolver { inputObject: InputObject -> inputObject.id } + } + inputType { + InputObject::optional.configure { + deprecate("deprecated old input value") + } + } + query("data") { + resolver { oldOptional: String?, new: String -> "" }.withArgs { + arg(String::class, typeOf()) { + name = "oldOptional"; defaultValue = "\"\""; deprecate("deprecated arg") + } + arg { name = "new" } + } + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + type DeprecatedObject { + new: String! + old: String! @deprecated(reason: "deprecated old value") + } + + type Mutation { + doStuff(inputObject: InputObject!): Int! + } + + type Query { + data( + oldOptional: String = "" @deprecated(reason: "deprecated arg") + new: String! + ): String! + } + + enum TestEnum { + TYPE1 + TYPE2 @deprecated(reason: "deprecated enum value") + } + + input InputObject { + id: Int! + intInput: Int! + optional: String @deprecated(reason: "deprecated old input value") + stringInput: String! + } + + """.trimIndent() + } + + @Test + fun `schema with descriptions should be printed as expected if descriptions are included`() { + val schema = KGraphQL.schema { + type { + property(TestObject::name) { + description = "This is the name" + } + } + enum { + value(TestEnum.TYPE1) { + description = "Enum value description" + } + } + query("getObject") { + description = "Get a test object" + resolver { name: String -> TestObject(name) }.withArgs { + arg { name = "name"; description = "The desired name" } + } + } + mutation("addObject") { + description = """ + Add a test object + With some multi-line description + (& special characters like " and \n) + """.trimIndent() + resolver { toAdd: TestObject -> toAdd } + } + subscription("subscribeObject") { + description = "Subscribe to an object" + resolver { -> TestObject("name") } + } + } + + SchemaPrinter( + SchemaPrinterConfig( + includeSchemaDefinition = true, + includeDescriptions = true + ) + ).print(schema) shouldBeEqualTo """ + schema { + "Query object" + query: Query + "Mutation object" + mutation: Mutation + "Subscription object" + subscription: Subscription + } + + "Mutation object" + type Mutation { + "Add a test object" + "With some multi-line description" + "(& special characters like " and \n)" + addObject(toAdd: TestObject!): TestObject! + } + + "Query object" + type Query { + "Get a test object" + getObject( + "The desired name" + name: String! + ): TestObject! + } + + "Subscription object" + type Subscription { + "Subscribe to an object" + subscribeObject: TestObject! + } + + type TestObject { + "This is the name" + name: String! + } + + enum TestEnum { + "Enum value description" + TYPE1 + TYPE2 + } + + input TestObject { + name: String! + } + + """.trimIndent() + } + + @Test + fun `schema with descriptions should be printed as expected if descriptions are excluded`() { + val schema = KGraphQL.schema { + type { + property(TestObject::name) { + description = "This is the name" + } + } + enum { + value(TestEnum.TYPE1) { + description = "Enum value description" + } + } + query("getObject") { + description = "Get a test object" + resolver { -> TestObject("name") } + } + mutation("addObject") { + description = """ + Add a test object + With some multi-line description + (& special characters like " and \n) + """.trimIndent() + resolver { toAdd: TestObject -> toAdd } + } + subscription("subscribeObject") { + description = "Subscribe to an object" + resolver { -> TestObject("name") } + } + } + + SchemaPrinter(SchemaPrinterConfig(includeDescriptions = false)).print(schema) shouldBeEqualTo """ + type Mutation { + addObject(toAdd: TestObject!): TestObject! + } + + type Query { + getObject: TestObject! + } + + type Subscription { + subscribeObject: TestObject! + } + + type TestObject { + name: String! + } + + enum TestEnum { + TYPE1 + TYPE2 + } + + input TestObject { + name: String! + } + + """.trimIndent() + } + + @Test + fun `schema built-in directives should be printed as expected if built-in directives are included`() { + val schema = KGraphQL.schema { + type() + } + + SchemaPrinter(SchemaPrinterConfig(includeBuiltInDirectives = true)).print(schema) shouldBeEqualTo """ + type TestObject { + name: String! + } + + directive @deprecated(reason: String) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE + + directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + + """.trimIndent() + } + + @Test + fun `schema itself should be included if enforced`() { + val schema = KGraphQL.schema { + type() + } + + SchemaPrinter(SchemaPrinterConfig(includeSchemaDefinition = true)).print(schema) shouldBeEqualTo """ + schema { + query: Query + } + + type TestObject { + name: String! + } + + """.trimIndent() + } + + @Test + fun `schema itself should by default be included if required - other type named Mutation`() { + val schema = KGraphQL.schema { + type { + name = "Mutation" + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + schema { + query: Query + } + + type Mutation { + name: String! + } + + """.trimIndent() + } + + @Test + fun `schema itself should by default be included if required - other type named Subscription`() { + val schema = KGraphQL.schema { + type { + name = "Subscription" + } + } + + SchemaPrinter().print(schema) shouldBeEqualTo """ + schema { + query: Query + } + + type Subscription { + name: String! + } + + """.trimIndent() + } +} diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt index 01bd3dee..564b3b09 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt @@ -12,12 +12,12 @@ import org.amshove.kluent.shouldNotBeEqualTo import org.amshove.kluent.shouldNotContain import org.amshove.kluent.shouldThrow import org.amshove.kluent.withMessage -import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.startsWith import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.hasSize import org.hamcrest.collection.IsEmptyCollection.empty import org.junit.jupiter.api.Test @@ -95,7 +95,13 @@ class IntrospectionSpecificationTest { type() } - resolver { (string) -> if (string.isEmpty()) Union1("!!") else Union2("??") } + resolver { (string) -> + if (string.isEmpty()) { + Union1("!!") + } else { + Union2("??") + } + } } } @@ -106,13 +112,17 @@ class IntrospectionSpecificationTest { val response = deserialize( schema.executeBlocking( - "{data(input: \"\"){" + - "string, " + - "union{" + - "... on Union1{one, __typename} " + - "... on Union2{two}" + - "}" + - "}}" + """ + { + data(input: "") { + string, + union { + ...on Union1 { one, __typename } + ...on Union2 { two } + } + } + } + """.trimIndent() ) ) @@ -328,6 +338,46 @@ class IntrospectionSpecificationTest { } } + @Test + fun `__Directive introspection should return all built-in directives as expected`() { + val schema = defaultSchema { + query("interface") { + resolver { -> Face("~~MOCK~~") } + } + } + + val response = deserialize( + schema.executeBlocking( + "{__schema{directives{name isRepeatable args{name type{name kind ofType{name kind}}}}}}" + ) + ) + + assertThat(response.extract>("data/__schema/directives"), hasSize(3)) + + assertThat(response.extract("data/__schema/directives[0]/name"), equalTo("skip")) + assertThat(response.extract("data/__schema/directives[0]/isRepeatable"), equalTo(false)) + assertThat(response.extract>("data/__schema/directives[0]/args"), hasSize(1)) + assertThat(response.extract("data/__schema/directives[0]/args[0]/name"), equalTo("if")) + assertThat(response.extract("data/__schema/directives[0]/args[0]/type/kind"), equalTo("NON_NULL")) + assertThat(response.extract("data/__schema/directives[0]/args[0]/type/ofType/name"), equalTo("Boolean")) + assertThat(response.extract("data/__schema/directives[0]/args[0]/type/ofType/kind"), equalTo("SCALAR")) + + assertThat(response.extract("data/__schema/directives[1]/name"), equalTo("include")) + assertThat(response.extract("data/__schema/directives[1]/isRepeatable"), equalTo(false)) + assertThat(response.extract>("data/__schema/directives[1]/args"), hasSize(1)) + assertThat(response.extract("data/__schema/directives[1]/args[0]/name"), equalTo("if")) + assertThat(response.extract("data/__schema/directives[1]/args[0]/type/kind"), equalTo("NON_NULL")) + assertThat(response.extract("data/__schema/directives[1]/args[0]/type/ofType/name"), equalTo("Boolean")) + assertThat(response.extract("data/__schema/directives[1]/args[0]/type/ofType/kind"), equalTo("SCALAR")) + + assertThat(response.extract("data/__schema/directives[2]/name"), equalTo("deprecated")) + assertThat(response.extract("data/__schema/directives[2]/isRepeatable"), equalTo(false)) + assertThat(response.extract>("data/__schema/directives[2]/args"), hasSize(1)) + assertThat(response.extract("data/__schema/directives[2]/args[0]/name"), equalTo("reason")) + assertThat(response.extract("data/__schema/directives[2]/args[0]/type/kind"), equalTo("SCALAR")) + assertThat(response.extract("data/__schema/directives[2]/args[0]/type/name"), equalTo("String")) + } + /** * Not part of spec, but assumption of many graphql tools */ @@ -358,10 +408,9 @@ class IntrospectionSpecificationTest { deserialize(schema.executeBlocking("{__schema{directives{name, onField, onFragment, onOperation}}}")) val directives = response.extract>>("data/__schema/directives") directives.forEach { directive -> - assertThat(directive["name"] as String, anyOf(equalTo("skip"), equalTo("include"))) - assertThat(directive["onField"] as Boolean, equalTo(true)) - assertThat(directive["onFragment"] as Boolean, equalTo(true)) - assertThat(directive["onOperation"] as Boolean, equalTo(false)) + assertThat(directive["onField"], notNullValue()) + assertThat(directive["onFragment"], notNullValue()) + assertThat(directive["onOperation"], notNullValue()) } }