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
2 changes: 1 addition & 1 deletion libraries/apollo-ast/api/apollo-ast.api
Original file line number Diff line number Diff line change
Expand Up @@ -827,7 +827,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,14 +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'."
override val message = "Unexpected '$name' definition. Expecting '$expectedDefinition'."
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.apollographql.apollo3.ast

import com.apollographql.apollo3.annotations.ApolloDeprecatedSince
import com.apollographql.apollo3.annotations.ApolloExperimental
import com.apollographql.apollo3.annotations.ApolloInternal
import com.apollographql.apollo3.ast.internal.ExtensionsMerger
import com.apollographql.apollo3.ast.internal.builtinsDefinitionsStr
import com.apollographql.apollo3.ast.internal.ensureSchemaDefinition
Expand Down Expand Up @@ -79,23 +80,27 @@ fun builtinDefinitions() = definitionsFromString(builtinsDefinitionsStr)
*/
fun linkDefinitions() = definitionsFromString(linkDefinitionsStr)

@ApolloInternal 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")
})
}

@ApolloInternal 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 @@ -29,9 +29,11 @@ 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.IncompatibleDefinition
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 +65,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,9 +134,33 @@ 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(IncompatibleDefinition(definition.name, definition.toSemanticSdl(), definition.sourceLocation))
}
}
}

is GQLEnumTypeDefinition -> {
val existing = typeDefinitions[definition.name]
if (existing != null) {
if (!existing.semanticEquals(definition)) {
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 Expand Up @@ -491,6 +517,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,221 @@
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.GQLEnumValueDefinition
import com.apollographql.apollo3.ast.GQLFloatValue
import com.apollographql.apollo3.ast.GQLInputObjectTypeDefinition
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.GQLScalarTypeDefinition
import com.apollographql.apollo3.ast.GQLStringValue
import com.apollographql.apollo3.ast.GQLVariableValue
import com.apollographql.apollo3.ast.toUtf8

/**
* Returns true if the two nodes are semantically equal, which ignores the source location and the description.
* Note that not all cases are implemented - currently [GQLEnumTypeDefinition] and [GQLDirectiveDefinition] are fully supported, and
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
* unsupported types will throw.
*/
internal fun GQLNode.semanticEquals(other: GQLNode?): Boolean {
if (other == null) return false
when (this) {
is GQLDirectiveDefinition -> {
if (other !is GQLDirectiveDefinition) {
return false
martinbonnin marked this conversation as resolved.
Show resolved Hide resolved
}

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

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

is GQLInputValueDefinition -> {
if (other !is GQLInputValueDefinition) {
return false
}

if (!type.semanticEquals(other.type)) {
return false
}

if (defaultValue != null) {
if (!defaultValue.semanticEquals(other.defaultValue)) {
return false
}
} else if (other.defaultValue != null) {
return false
}
}

is GQLNonNullType -> {
if (other !is GQLNonNullType) {
return false
}
}

is GQLListType -> {
if (other !is GQLListType) {
return false
}
}

is GQLNamedType -> {
if (other !is GQLNamedType) {
return false
}
}

is GQLNullValue -> {
if (other !is GQLNullValue) {
return false
}
}

is GQLListValue -> {
if (other !is GQLListValue) {
return false
}
}

is GQLObjectValue -> {
if (other !is GQLObjectValue) {
return false
}
}

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

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

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

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

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

is GQLVariableValue -> {
if (other !is GQLVariableValue) {
return false
}
}

is GQLEnumTypeDefinition -> {
if (other !is GQLEnumTypeDefinition) {
return false
}
}

is GQLDirective -> {
if (other !is GQLDirective) {
return false
}
}

is GQLArgument -> {
if (other !is GQLArgument) {
return false
}
}

is GQLEnumValueDefinition -> {
if (other !is GQLEnumValueDefinition) {
return false
}
}

is GQLInputObjectTypeDefinition -> {
if (other !is GQLInputObjectTypeDefinition) {
return false
}
}

is GQLScalarTypeDefinition -> {
if (other !is GQLScalarTypeDefinition) {
return false
}
}

else -> {
TODO("semanticEquals not supported for ${this::class.simpleName}")
}
}

if (this is GQLNamed) {
if (other !is GQLNamed) {
return false
}
if (name != other.name) {
return false
}
}

if (children.size != other.children.size) {
return false
}
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()
}

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 @@ -12,8 +12,9 @@ import com.apollographql.apollo3.ast.GQLOperationDefinition
import com.apollographql.apollo3.ast.GQLScalarTypeDefinition
import com.apollographql.apollo3.ast.GQLSchemaDefinition
import com.apollographql.apollo3.ast.GQLTypeDefinition
import com.apollographql.apollo3.ast.IncompatibleDirectiveDefinition
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.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 All @@ -576,7 +577,7 @@ internal fun List<Issue>.group(
* Because some users might have added the apollo directive to their schema, we just let that through for now
*/
is DirectiveRedefinition -> if (it.name in apolloDirectives) Severity.None else Severity.Warning
is IncompatibleDirectiveDefinition -> Severity.Warning
is IncompatibleDefinition -> Severity.Warning
else -> Severity.Error
}

Expand Down

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