Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add validation to check schema definitions are compatible with the bundled ones #5444

Merged
merged 12 commits into from
Dec 12, 2023
4 changes: 3 additions & 1 deletion libraries/apollo-ast/api/apollo-ast.api
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,8 @@ public final class com/apollographql/apollo3/ast/GqldirectiveKt {
}

public final class com/apollographql/apollo3/ast/GqldocumentKt {
public static final field KOTLIN_LABS_VERSION Ljava/lang/String;
public static final field NULLABILITY_VERSION Ljava/lang/String;
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
public static final fun builtinDefinitions ()Ljava/util/List;
public static final fun kotlinLabsDefinitions (Ljava/lang/String;)Ljava/util/List;
public static final fun linkDefinitions ()Ljava/util/List;
Expand Down Expand Up @@ -827,7 +829,7 @@ public abstract interface class com/apollographql/apollo3/ast/GraphQLIssue : com
public abstract interface class com/apollographql/apollo3/ast/GraphQLValidationIssue : com/apollographql/apollo3/ast/GraphQLIssue {
}

public final class com/apollographql/apollo3/ast/IncompatibleDirectiveDefinition : com/apollographql/apollo3/ast/GraphQLValidationIssue {
public final class com/apollographql/apollo3/ast/IncompatibleDefinition : com/apollographql/apollo3/ast/GraphQLValidationIssue {
public fun <init> (Ljava/lang/String;Ljava/lang/String;Lcom/apollographql/apollo3/ast/SourceLocation;)V
public fun getMessage ()Ljava/lang/String;
public fun getSourceLocation ()Lcom/apollographql/apollo3/ast/SourceLocation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,25 +49,14 @@ class UnknownDirective @ApolloInternal constructor(
}

/**
* The directive definition is inconsistent with the expected one.
* The definition is inconsistent with the expected one.
*/
class IncompatibleDirectiveDefinition(
directiveName: String,
class IncompatibleDefinition(
name: String,
expectedDefinition: String,
override val sourceLocation: SourceLocation?,
) : GraphQLValidationIssue {
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'."
override val message = "Unexpected '$name' definition. Expecting '$expectedDefinition'."
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ import com.apollographql.apollo3.ast.GQLTypeDefinition
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.IncompatibleDefinition
import com.apollographql.apollo3.ast.Issue
import com.apollographql.apollo3.ast.KOTLIN_LABS_VERSION
import com.apollographql.apollo3.ast.MergeOptions
Expand Down Expand Up @@ -141,25 +140,27 @@ internal fun validateSchema(definitions: List<GQLDefinition>, requiresApolloDefi
val existing = directiveDefinitions[definition.name]
if (existing != null) {
if (!existing.semanticEquals(definition)) {
issues.add(IncompatibleDirectiveDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
issues.add(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
}
}
}

is GQLEnumTypeDefinition -> {
val existing = typeDefinitions[definition.name]
if (existing != null) {
if (existing !is GQLEnumTypeDefinition || !existing.semanticEquals(definition)) {
BoD marked this conversation as resolved.
Show resolved Hide resolved
issues.add(IncompatibleEnumDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
issues.add(IncompatibleDefinition(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))
issues.add(IncompatibleDefinition(Schema.ONE_OF, "directive @oneOf on INPUT_OBJECT", it.sourceLocation))
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,183 +1,200 @@
package com.apollographql.apollo3.ast.internal

import com.apollographql.apollo3.ast.GQLArgument
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.GQLInputValueDefinition
import com.apollographql.apollo3.ast.GQLIntValue
import com.apollographql.apollo3.ast.GQLListType
import com.apollographql.apollo3.ast.GQLListValue
import com.apollographql.apollo3.ast.GQLNamed
import com.apollographql.apollo3.ast.GQLNamedType
import com.apollographql.apollo3.ast.GQLNode
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
}
internal fun GQLNode.semanticEquals(other: GQLNode): Boolean {
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
when (this) {
is GQLDirectiveDefinition -> {
if (other !is GQLDirectiveDefinition) {
return false
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

if (!argument.type.semanticEquals(otherArgument.type)) {
return false
}
is GQLInputValueDefinition -> {
if (other !is GQLInputValueDefinition) {
return false
}

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

return true
}
if (defaultValue != null && other.defaultValue != null) {
if (!defaultValue.semanticEquals(other.defaultValue)) {
return false
}
} else if (defaultValue != null || other.defaultValue != null) {
return false
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
}
}

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

is GQLListType -> {
other is GQLListType && type.semanticEquals(other.type)
if (other !is GQLListType) {
return false
}
}

is GQLNamedType -> {
other is GQLNamedType && name == other.name
if (other !is GQLNamedType) {
return false
}
}
}
}

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

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

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)
if (other !is GQLObjectValue) {
return false
}
}

is GQLStringValue -> {
other is GQLStringValue && value == other.value
if (other !is GQLStringValue) {
return false
}
if (value != other.value) {
return false
}
}

is GQLBooleanValue -> {
other is GQLBooleanValue && value == other.value
if (other !is GQLBooleanValue) {
return false
}
if (value != other.value) {
return false
}
}

is GQLIntValue -> {
other is GQLIntValue && value == other.value
if (other !is GQLIntValue) {
return false
}
if (value != other.value) {
return false
}
}

is GQLFloatValue -> {
other is GQLFloatValue && value == other.value
if (other !is GQLFloatValue) {
return false
}
if (value != other.value) {
return false
}
}

is GQLEnumValue -> {
other is GQLEnumValue && value == other.value
if (other !is GQLEnumValue) {
return false
}
if (value != other.value) {
return false
}
}

is GQLVariableValue -> {
other is GQLVariableValue && name == other.name
if (other !is GQLVariableValue) {
return false
}
if (name != other.name) {
return false
}
}
}
}


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

if (directives.size != definition.directives.size) {
return false
}
is GQLDirective -> {
if (other !is GQLDirective) {
return false
}
if (name != other.name) {
return false
}
}

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

if (enumValues.size != definition.enumValues.size) {
return false
else -> {}
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
}

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

if (value.directives.size != otherValue.directives.size) {
if (this is GQLNamed) {
if (other !is GQLNamed) {
return false
}

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

return true
}

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

if (arguments.size != other.arguments.size) {
if (children.size != other.children.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)) {
for (i in children.indices) {
if (!children[i].semanticEquals(other.children[i])) {
return false
}
}

return true
}


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