Skip to content

Commit

Permalink
Validation: check that directives/enums with the same name as those i…
Browse files Browse the repository at this point in the history
…n nullability are semantically equal
  • Loading branch information
BoD committed Dec 11, 2023
1 parent 7c84d42 commit c503433
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ class IncompatibleDirectiveDefinition(
override val message = "Unexpected '@$directiveName' directive definition. Expecting '$expectedDefinition'."
}

/**
* The enum definition is inconsistent with the expected one.
*/
class IncompatibleEnumDefinition(
enumName: String,
expectedDefinition: String,
override val sourceLocation: SourceLocation?,
) : GraphQLValidationIssue {
override val message = "Unexpected '@$enumName' enum definition. Expecting '$expectedDefinition'."
}

/**
* Fields have different shapes and cannot be merged
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,16 @@ private fun GQLValue?.toBoolean(): Boolean {
}
}

private fun GQLValue?.toCatchTo(): CatchTo? {
private fun GQLValue?.toCatchTo(): CatchTo {
return when (this) {
is GQLEnumValue -> when (this.value) {
"NULL" -> CatchTo.NULL
"RESULT" -> CatchTo.RESULT
"THROW" -> CatchTo.THROW
else -> null
else -> error("Unknown CatchTo value: ${this.value}")
}

else -> null
else -> error("${this?.sourceLocation}: expected CatchTo! value")
}
}

Expand All @@ -143,8 +143,8 @@ private fun GQLDirective.isDefinedAndMatchesOriginalName(schema: Schema, origina
fun List<GQLDirective>.findCatches(schema: Schema): List<Catch> {
return filter {
it.isDefinedAndMatchesOriginalName(schema, Schema.CATCH)
}.mapNotNull {
val to = it.getArgument("to", schema).toCatchTo() ?: return@mapNotNull null
}.map {
val to = it.getArgument("to", schema).toCatchTo()
Catch(
to = to,
level = it.getArgument("level", schema)?.toIntOrNull(),
Expand All @@ -169,4 +169,4 @@ fun GQLTypeDefinition.findSemanticNonNulls(fieldName: String, schema: Schema): L
}.map {
it.getArgument("level", schema)?.toIntOrNull()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,27 @@ fun builtinDefinitions() = definitionsFromString(builtinsDefinitionsStr)
*/
fun linkDefinitions() = definitionsFromString(linkDefinitionsStr)

const val KOTLIN_LABS_VERSION = "v0.2"

/**
* Extra apollo Kotlin specific definitions from https://specs.apollo.dev/kotlin_labs/<[version]>
*/
fun kotlinLabsDefinitions(version: String): List<GQLDefinition> {
return definitionsFromString(when (version) {
"v0.2" -> kotlinLabsDefinitions
else -> error("kotlin_labs/$version definitions are not supported, please use v0.2")
KOTLIN_LABS_VERSION -> kotlinLabsDefinitions
else -> error("kotlin_labs/$version definitions are not supported, please use $KOTLIN_LABS_VERSION")
})
}

const val NULLABILITY_VERSION = "v0.1"

/**
* Extra nullability definitions from https://specs.apollo.dev/nullability/<[version]>
*/
fun nullabilityDefinitions(version: String): List<GQLDefinition> {
return definitionsFromString(when (version) {
"v0.1" -> nullabilityDefinitionsStr
else -> error("nullability/$version definitions are not supported, please use v0.1")
NULLABILITY_VERSION -> nullabilityDefinitionsStr
else -> error("nullability/$version definitions are not supported, please use $NULLABILITY_VERSION")
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import com.apollographql.apollo3.ast.GQLTypeDefinition.Companion.builtInTypes
import com.apollographql.apollo3.ast.GQLTypeSystemExtension
import com.apollographql.apollo3.ast.GQLUnionTypeDefinition
import com.apollographql.apollo3.ast.IncompatibleDirectiveDefinition
import com.apollographql.apollo3.ast.IncompatibleEnumDefinition
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.KOTLIN_LABS_VERSION
import com.apollographql.apollo3.ast.MergeOptions
import com.apollographql.apollo3.ast.NULLABILITY_VERSION
import com.apollographql.apollo3.ast.NoQueryType
import com.apollographql.apollo3.ast.OtherValidationIssue
import com.apollographql.apollo3.ast.Schema
Expand Down Expand Up @@ -63,7 +66,7 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi

var directivesToStrip = foreignSchemas.flatMap { it.directivesToStrip }

val kotlinLabsDefinitions = kotlinLabsDefinitions("v0.2")
val kotlinLabsDefinitions = kotlinLabsDefinitions(KOTLIN_LABS_VERSION)

if (requiresApolloDefinitions && foreignSchemas.none { it.name == "kotlin_labs" }) {
/**
Expand Down Expand Up @@ -132,6 +135,28 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
}
}

nullabilityDefinitions(NULLABILITY_VERSION).forEach { definition ->
when (definition) {
is GQLDirectiveDefinition -> {
val existing = directiveDefinitions[definition.name]
if (existing != null) {
if (!existing.semanticEquals(definition)) {
issues.add(IncompatibleDirectiveDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
}
}
}
is GQLEnumTypeDefinition -> {
val existing = typeDefinitions[definition.name]
if (existing != null) {
if (existing !is GQLEnumTypeDefinition || !existing.semanticEquals(definition)) {
issues.add(IncompatibleEnumDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
}
}
}
else -> {}
}
}

directiveDefinitions[Schema.ONE_OF]?.let {
if (it.locations != listOf(GQLDirectiveLocation.INPUT_OBJECT) || it.arguments.isNotEmpty() || it.repeatable) {
issues.add(IncompatibleDirectiveDefinition(Schema.ONE_OF, "directive @oneOf on INPUT_OBJECT", it.sourceLocation))
Expand Down Expand Up @@ -491,6 +516,7 @@ private fun ValidationScope.validateCatch(schemaDefinition: GQLSchemaDefinition?
}

}

private fun ValidationScope.validateInputObjects() {
typeDefinitions.values.filterIsInstance<GQLInputObjectTypeDefinition>().forEach { o ->
if (o.inputFields.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package com.apollographql.apollo3.ast.internal

import com.apollographql.apollo3.ast.GQLBooleanValue
import com.apollographql.apollo3.ast.GQLDirective
import com.apollographql.apollo3.ast.GQLDirectiveDefinition
import com.apollographql.apollo3.ast.GQLEnumTypeDefinition
import com.apollographql.apollo3.ast.GQLEnumValue
import com.apollographql.apollo3.ast.GQLFloatValue
import com.apollographql.apollo3.ast.GQLIntValue
import com.apollographql.apollo3.ast.GQLListType
import com.apollographql.apollo3.ast.GQLListValue
import com.apollographql.apollo3.ast.GQLNamedType
import com.apollographql.apollo3.ast.GQLNonNullType
import com.apollographql.apollo3.ast.GQLNullValue
import com.apollographql.apollo3.ast.GQLObjectValue
import com.apollographql.apollo3.ast.GQLStringValue
import com.apollographql.apollo3.ast.GQLType
import com.apollographql.apollo3.ast.GQLValue
import com.apollographql.apollo3.ast.GQLVariableValue
import com.apollographql.apollo3.ast.toUtf8

internal fun GQLDirectiveDefinition.semanticEquals(other: GQLDirectiveDefinition): Boolean {
if (name != other.name) {
return false
}

if (locations != other.locations) {
return false
}

if (repeatable != other.repeatable) {
return false
}

if (arguments.size != other.arguments.size) {
return false
}

arguments.forEach { argument ->
val otherArgument = other.arguments.firstOrNull { it.name == argument.name }
if (otherArgument == null) {
return false
}

if (!argument.type.semanticEquals(otherArgument.type)) {
return false
}

if (argument.defaultValue != null && otherArgument.defaultValue != null) {
if (!argument.defaultValue.semanticEquals(otherArgument.defaultValue)) {
return false
}
} else if (argument.defaultValue != null || otherArgument.defaultValue != null) {
return false
}
}

return true
}

internal fun GQLType.semanticEquals(other: GQLType): Boolean {
return when (this) {
is GQLNonNullType -> {
other is GQLNonNullType && type.semanticEquals(other.type)
}

is GQLListType -> {
other is GQLListType && type.semanticEquals(other.type)
}

is GQLNamedType -> {
other is GQLNamedType && name == other.name
}
}
}

internal fun GQLValue.semanticEquals(other: GQLValue): Boolean {
return when (this) {
is GQLNullValue -> {
other is GQLNullValue
}

is GQLListValue -> {
other is GQLListValue && values.size == other.values.size && values.zip(other.values).all { (a, b) -> a.semanticEquals(b) }
}

is GQLObjectValue -> {
other is GQLObjectValue && fields.size == other.fields.size && fields.sortedBy { it.name }.zip(other.fields.sortedBy { it.name }).all { (a, b) ->
a.name == b.name && a.value.semanticEquals(b.value)
}
}

is GQLStringValue -> {
other is GQLStringValue && value == other.value
}

is GQLBooleanValue -> {
other is GQLBooleanValue && value == other.value
}

is GQLIntValue -> {
other is GQLIntValue && value == other.value
}

is GQLFloatValue -> {
other is GQLFloatValue && value == other.value
}

is GQLEnumValue -> {
other is GQLEnumValue && value == other.value
}

is GQLVariableValue -> {
other is GQLVariableValue && name == other.name
}
}
}


internal fun GQLEnumTypeDefinition.semanticEquals(definition: GQLEnumTypeDefinition): Boolean {
if (name != definition.name) {
return false
}

if (directives.size != definition.directives.size) {
return false
}

directives.sortedBy { it.name }.zip(definition.directives.sortedBy { it.name }).forEach { (a, b) ->
if (!a.semanticEquals(b)) {
return false
}
}

if (enumValues.size != definition.enumValues.size) {
return false
}

enumValues.forEach { value ->
val otherValue = definition.enumValues.firstOrNull { it.name == value.name }
if (otherValue == null) {
return false
}

if (value.directives.size != otherValue.directives.size) {
return false
}

if (value.directives.sortedBy { it.name }.zip(otherValue.directives.sortedBy { it.name }).any { (a, b) -> !a.semanticEquals(b) }) {
return false
}
}

return true
}

private fun GQLDirective.semanticEquals(other: GQLDirective): Boolean {
if (name != other.name) {
return false
}

if (arguments.size != other.arguments.size) {
return false
}

arguments.forEach { argument ->
val otherArgument = other.arguments.firstOrNull { it.name == argument.name }
if (otherArgument == null) {
return false
}

if (!argument.value.semanticEquals(otherArgument.value)) {
return false
}
}

return true
}


internal fun GQLDirectiveDefinition.toSemanticSdl(): String {
return copy(description = null, arguments = arguments.map { it.copy(description = null) }).toUtf8().trim()
}

internal fun GQLEnumTypeDefinition.toSemanticSdl(): String {
return copy(description = null, enumValues = enumValues.map { it.copy(description = null) }).toUtf8().replace(Regex("[\\n ]+"), " ").trim()
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.apollographql.apollo3.ast.GQLSchemaDefinition
import com.apollographql.apollo3.ast.GQLTypeDefinition
import com.apollographql.apollo3.ast.IncompatibleDirectiveDefinition
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.KOTLIN_LABS_VERSION
import com.apollographql.apollo3.ast.ParserOptions
import com.apollographql.apollo3.ast.QueryDocumentMinifier
import com.apollographql.apollo3.ast.Schema
Expand Down Expand Up @@ -563,7 +564,7 @@ internal fun List<Issue>.group(
val ignored = mutableListOf<Issue>()
val warnings = mutableListOf<Issue>()
val errors = mutableListOf<Issue>()
val apolloDirectives = kotlinLabsDefinitions("v0.2").mapNotNull { (it as? GQLDirectiveDefinition)?.name }.toSet()
val apolloDirectives = kotlinLabsDefinitions(KOTLIN_LABS_VERSION).mapNotNull { (it as? GQLDirectiveDefinition)?.name }.toSet()

forEach {
val severity = when (it) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
directive @ignoreFieldErrors repeatable on QUERY | MUTATION | SUBSCRIPTION
directive @catch on FIELD_DEFINITION
enum CatchTo {
NULL
EMPTY
ERROR
}

type Query {
x: Int
}

0 comments on commit c503433

Please sign in to comment.