diff --git a/libraries/apollo-ast/api/apollo-ast.api b/libraries/apollo-ast/api/apollo-ast.api index 0b5ab01ce3b..bf062c5b3c5 100644 --- a/libraries/apollo-ast/api/apollo-ast.api +++ b/libraries/apollo-ast/api/apollo-ast.api @@ -48,7 +48,7 @@ public final class com/apollographql/apollo/ast/ApolloParser { public static synthetic fun toSchema$default (Ljava/io/File;ZILjava/lang/Object;)Lcom/apollographql/apollo/ast/Schema; public static final fun validateAsExecutable (Lcom/apollographql/apollo/ast/GQLDocument;Lcom/apollographql/apollo/ast/Schema;)Lcom/apollographql/apollo/ast/ExecutableValidationResult; public static final fun validateAsSchema (Lcom/apollographql/apollo/ast/GQLDocument;)Lcom/apollographql/apollo/ast/GQLResult; - public static final fun validateAsSchema (Lcom/apollographql/apollo/ast/GQLDocument;Lcom/apollographql/apollo/ast/internal/SchemaValidationOptions;)Lcom/apollographql/apollo/ast/GQLResult; + public static final fun validateAsSchema (Lcom/apollographql/apollo/ast/GQLDocument;Lcom/apollographql/apollo/ast/SchemaValidationOptions;)Lcom/apollographql/apollo/ast/GQLResult; public static final fun validateAsSchemaAndAddApolloDefinition (Lcom/apollographql/apollo/ast/GQLDocument;)Lcom/apollographql/apollo/ast/GQLResult; } @@ -1053,9 +1053,8 @@ public final class com/apollographql/apollo/ast/IssueKt { public final class com/apollographql/apollo/ast/MergeOptions { public static final field Companion Lcom/apollographql/apollo/ast/MergeOptions$Companion; - public fun (ZZ)V - public final fun getAllowAddingDirectivesToExistingFieldDefinitions ()Z - public final fun getAllowFieldNullabilityModification ()Z + public fun (Z)V + public final fun getAllowMergingFieldDefinitions ()Z } public final class com/apollographql/apollo/ast/MergeOptions$Companion { @@ -1238,6 +1237,37 @@ public final class com/apollographql/apollo/ast/SchemaValidationException : com/ public synthetic fun (Ljava/lang/String;Lcom/apollographql/apollo/ast/SourceLocation;ILkotlin/jvm/internal/DefaultConstructorMarker;)V } +public final class com/apollographql/apollo/ast/SchemaValidationOptions { + public fun (ZLjava/util/List;)V + public fun (ZLjava/util/List;Z)V + public fun (ZLjava/util/List;ZZLcom/apollographql/apollo/ast/MergeOptions;)V + public final fun getAddKotlinLabsDefinitions ()Z + public final fun getComputeKeyFields ()Z + public final fun getExcludeCacheDirectives ()Z + public final fun getForeignSchemas ()Ljava/util/List; + public final fun getMergeOptions ()Lcom/apollographql/apollo/ast/MergeOptions; +} + +public final class com/apollographql/apollo/ast/SchemaValidationOptions$Builder { + public fun ()V + public final fun addForeignSchema (Lcom/apollographql/apollo/ast/ForeignSchema;)Lcom/apollographql/apollo/ast/SchemaValidationOptions$Builder; + public final fun addKotlinLabsDefinitions (Z)Lcom/apollographql/apollo/ast/SchemaValidationOptions$Builder; + public final fun build ()Lcom/apollographql/apollo/ast/SchemaValidationOptions; + public final fun computeKeyFields (Z)Lcom/apollographql/apollo/ast/SchemaValidationOptions$Builder; + public final fun excludeCacheDirectives (Z)Lcom/apollographql/apollo/ast/SchemaValidationOptions$Builder; + public final fun foreignSchemas (Ljava/util/List;)Lcom/apollographql/apollo/ast/SchemaValidationOptions$Builder; + public final fun getAddKotlinLabsDefinitions ()Z + public final fun getComputeKeyFields ()Z + public final fun getExcludeCacheDirectives ()Z + public final fun getForeignSchemas ()Ljava/util/List; + public final fun getMergeOptions ()Lcom/apollographql/apollo/ast/MergeOptions; + public final fun mergeOptions (Lcom/apollographql/apollo/ast/MergeOptions;)Lcom/apollographql/apollo/ast/SchemaValidationOptions$Builder; + public final fun setAddKotlinLabsDefinitions (Z)V + public final fun setComputeKeyFields (Z)V + public final fun setExcludeCacheDirectives (Z)V + public final fun setMergeOptions (Lcom/apollographql/apollo/ast/MergeOptions;)V +} + public class com/apollographql/apollo/ast/SourceAwareException : java/lang/RuntimeException { public static final field Companion Lcom/apollographql/apollo/ast/SourceAwareException$Companion; public fun (Ljava/lang/String;Lcom/apollographql/apollo/ast/SourceLocation;)V diff --git a/libraries/apollo-ast/api/apollo-ast.klib.api b/libraries/apollo-ast/api/apollo-ast.klib.api index 5528aa222b1..d3771ee5903 100644 --- a/libraries/apollo-ast/api/apollo-ast.klib.api +++ b/libraries/apollo-ast/api/apollo-ast.klib.api @@ -1206,12 +1206,10 @@ final class com.apollographql.apollo.ast/InvalidDeferLabel : com.apollographql.a } final class com.apollographql.apollo.ast/MergeOptions { // com.apollographql.apollo.ast/MergeOptions|null[0] - constructor (kotlin/Boolean, kotlin/Boolean) // com.apollographql.apollo.ast/MergeOptions.|(kotlin.Boolean;kotlin.Boolean){}[0] + constructor (kotlin/Boolean) // com.apollographql.apollo.ast/MergeOptions.|(kotlin.Boolean){}[0] - final val allowAddingDirectivesToExistingFieldDefinitions // com.apollographql.apollo.ast/MergeOptions.allowAddingDirectivesToExistingFieldDefinitions|{}allowAddingDirectivesToExistingFieldDefinitions[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.ast/MergeOptions.allowAddingDirectivesToExistingFieldDefinitions.|(){}[0] - final val allowFieldNullabilityModification // com.apollographql.apollo.ast/MergeOptions.allowFieldNullabilityModification|{}allowFieldNullabilityModification[0] - final fun (): kotlin/Boolean // com.apollographql.apollo.ast/MergeOptions.allowFieldNullabilityModification.|(){}[0] + final val allowMergingFieldDefinitions // com.apollographql.apollo.ast/MergeOptions.allowMergingFieldDefinitions|{}allowMergingFieldDefinitions[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/MergeOptions.allowMergingFieldDefinitions.|(){}[0] final object Companion { // com.apollographql.apollo.ast/MergeOptions.Companion|null[0] final val Default // com.apollographql.apollo.ast/MergeOptions.Companion.Default|{}Default[0] @@ -1443,6 +1441,51 @@ final class com.apollographql.apollo.ast/SchemaValidationException : com.apollog constructor (kotlin/String, com.apollographql.apollo.ast/SourceLocation? = ...) // com.apollographql.apollo.ast/SchemaValidationException.|(kotlin.String;com.apollographql.apollo.ast.SourceLocation?){}[0] } +final class com.apollographql.apollo.ast/SchemaValidationOptions { // com.apollographql.apollo.ast/SchemaValidationOptions|null[0] + constructor (kotlin/Boolean, kotlin.collections/List) // com.apollographql.apollo.ast/SchemaValidationOptions.|(kotlin.Boolean;kotlin.collections.List){}[0] + constructor (kotlin/Boolean, kotlin.collections/List, kotlin/Boolean) // com.apollographql.apollo.ast/SchemaValidationOptions.|(kotlin.Boolean;kotlin.collections.List;kotlin.Boolean){}[0] + constructor (kotlin/Boolean, kotlin.collections/List, kotlin/Boolean, kotlin/Boolean, com.apollographql.apollo.ast/MergeOptions) // com.apollographql.apollo.ast/SchemaValidationOptions.|(kotlin.Boolean;kotlin.collections.List;kotlin.Boolean;kotlin.Boolean;com.apollographql.apollo.ast.MergeOptions){}[0] + + final val addKotlinLabsDefinitions // com.apollographql.apollo.ast/SchemaValidationOptions.addKotlinLabsDefinitions|{}addKotlinLabsDefinitions[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/SchemaValidationOptions.addKotlinLabsDefinitions.|(){}[0] + final val computeKeyFields // com.apollographql.apollo.ast/SchemaValidationOptions.computeKeyFields|{}computeKeyFields[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/SchemaValidationOptions.computeKeyFields.|(){}[0] + final val excludeCacheDirectives // com.apollographql.apollo.ast/SchemaValidationOptions.excludeCacheDirectives|{}excludeCacheDirectives[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/SchemaValidationOptions.excludeCacheDirectives.|(){}[0] + final val foreignSchemas // com.apollographql.apollo.ast/SchemaValidationOptions.foreignSchemas|{}foreignSchemas[0] + final fun (): kotlin.collections/List // com.apollographql.apollo.ast/SchemaValidationOptions.foreignSchemas.|(){}[0] + final val mergeOptions // com.apollographql.apollo.ast/SchemaValidationOptions.mergeOptions|{}mergeOptions[0] + final fun (): com.apollographql.apollo.ast/MergeOptions // com.apollographql.apollo.ast/SchemaValidationOptions.mergeOptions.|(){}[0] + + final class Builder { // com.apollographql.apollo.ast/SchemaValidationOptions.Builder|null[0] + constructor () // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.|(){}[0] + + final val foreignSchemas // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.foreignSchemas|{}foreignSchemas[0] + final fun (): kotlin.collections/MutableList // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.foreignSchemas.|(){}[0] + + final var addKotlinLabsDefinitions // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.addKotlinLabsDefinitions|{}addKotlinLabsDefinitions[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.addKotlinLabsDefinitions.|(){}[0] + final fun (kotlin/Boolean) // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.addKotlinLabsDefinitions.|(kotlin.Boolean){}[0] + final var computeKeyFields // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.computeKeyFields|{}computeKeyFields[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.computeKeyFields.|(){}[0] + final fun (kotlin/Boolean) // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.computeKeyFields.|(kotlin.Boolean){}[0] + final var excludeCacheDirectives // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.excludeCacheDirectives|{}excludeCacheDirectives[0] + final fun (): kotlin/Boolean // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.excludeCacheDirectives.|(){}[0] + final fun (kotlin/Boolean) // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.excludeCacheDirectives.|(kotlin.Boolean){}[0] + final var mergeOptions // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.mergeOptions|{}mergeOptions[0] + final fun (): com.apollographql.apollo.ast/MergeOptions // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.mergeOptions.|(){}[0] + final fun (com.apollographql.apollo.ast/MergeOptions) // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.mergeOptions.|(com.apollographql.apollo.ast.MergeOptions){}[0] + + final fun addForeignSchema(com.apollographql.apollo.ast/ForeignSchema): com.apollographql.apollo.ast/SchemaValidationOptions.Builder // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.addForeignSchema|addForeignSchema(com.apollographql.apollo.ast.ForeignSchema){}[0] + final fun addKotlinLabsDefinitions(kotlin/Boolean): com.apollographql.apollo.ast/SchemaValidationOptions.Builder // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.addKotlinLabsDefinitions|addKotlinLabsDefinitions(kotlin.Boolean){}[0] + final fun build(): com.apollographql.apollo.ast/SchemaValidationOptions // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.build|build(){}[0] + final fun computeKeyFields(kotlin/Boolean): com.apollographql.apollo.ast/SchemaValidationOptions.Builder // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.computeKeyFields|computeKeyFields(kotlin.Boolean){}[0] + final fun excludeCacheDirectives(kotlin/Boolean): com.apollographql.apollo.ast/SchemaValidationOptions.Builder // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.excludeCacheDirectives|excludeCacheDirectives(kotlin.Boolean){}[0] + final fun foreignSchemas(kotlin.collections/List): com.apollographql.apollo.ast/SchemaValidationOptions.Builder // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.foreignSchemas|foreignSchemas(kotlin.collections.List){}[0] + final fun mergeOptions(com.apollographql.apollo.ast/MergeOptions): com.apollographql.apollo.ast/SchemaValidationOptions.Builder // com.apollographql.apollo.ast/SchemaValidationOptions.Builder.mergeOptions|mergeOptions(com.apollographql.apollo.ast.MergeOptions){}[0] + } +} + final class com.apollographql.apollo.ast/SourceLocation { // com.apollographql.apollo.ast/SourceLocation|null[0] constructor (kotlin/Int, kotlin/Int, kotlin/Int, kotlin/Int, kotlin/String?) // com.apollographql.apollo.ast/SourceLocation.|(kotlin.Int;kotlin.Int;kotlin.Int;kotlin.Int;kotlin.String?){}[0] @@ -1581,7 +1624,7 @@ final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.as final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/toSchema(): com.apollographql.apollo.ast/Schema // com.apollographql.apollo.ast/toSchema|toSchema@com.apollographql.apollo.ast.GQLDocument(){}[0] final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/validateAsExecutable(com.apollographql.apollo.ast/Schema): com.apollographql.apollo.ast/ExecutableValidationResult // com.apollographql.apollo.ast/validateAsExecutable|validateAsExecutable@com.apollographql.apollo.ast.GQLDocument(com.apollographql.apollo.ast.Schema){}[0] final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/validateAsSchema(): com.apollographql.apollo.ast/GQLResult // com.apollographql.apollo.ast/validateAsSchema|validateAsSchema@com.apollographql.apollo.ast.GQLDocument(){}[0] -final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/validateAsSchema(com.apollographql.apollo.ast.internal/SchemaValidationOptions): com.apollographql.apollo.ast/GQLResult // com.apollographql.apollo.ast/validateAsSchema|validateAsSchema@com.apollographql.apollo.ast.GQLDocument(com.apollographql.apollo.ast.internal.SchemaValidationOptions){}[0] +final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/validateAsSchema(com.apollographql.apollo.ast/SchemaValidationOptions): com.apollographql.apollo.ast/GQLResult // com.apollographql.apollo.ast/validateAsSchema|validateAsSchema@com.apollographql.apollo.ast.GQLDocument(com.apollographql.apollo.ast.SchemaValidationOptions){}[0] final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/validateAsSchemaAndAddApolloDefinition(): com.apollographql.apollo.ast/GQLResult // com.apollographql.apollo.ast/validateAsSchemaAndAddApolloDefinition|validateAsSchemaAndAddApolloDefinition@com.apollographql.apollo.ast.GQLDocument(){}[0] final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/withBuiltinDefinitions(): com.apollographql.apollo.ast/GQLDocument // com.apollographql.apollo.ast/withBuiltinDefinitions|withBuiltinDefinitions@com.apollographql.apollo.ast.GQLDocument(){}[0] final fun (com.apollographql.apollo.ast/GQLDocument).com.apollographql.apollo.ast/withoutBuiltinDefinitions(): com.apollographql.apollo.ast/GQLDocument // com.apollographql.apollo.ast/withoutBuiltinDefinitions|withoutBuiltinDefinitions@com.apollographql.apollo.ast.GQLDocument(){}[0] diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/SchemaValidationOptions.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/SchemaValidationOptions.kt new file mode 100644 index 00000000000..93ab1461d2c --- /dev/null +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/SchemaValidationOptions.kt @@ -0,0 +1,96 @@ +package com.apollographql.apollo.ast + +import com.apollographql.apollo.annotations.ApolloDeprecatedSince +import com.apollographql.apollo.annotations.ApolloExperimental + + +/** + * @param addKotlinLabsDefinitions automatically import the kotlin_labs definitions, even if no `@link` is present. If [excludeCacheDirectives] is `true`, cache related directives are excluded. + * @param foreignSchemas a list of known [ForeignSchema] that may or may not be imported depending on the `@link` directives + * @param excludeCacheDirectives whether to exclude cache related directives when auto-importing the kotlin_labs definitions. Has no effect if [addKotlinLabsDefinitions] is `false`. + * @param computeKeyFields whether to compute cache key fields. Can be false when using the Apollo Cache compiler plugin to avoid unneeded computation. + */ +@ApolloExperimental +class SchemaValidationOptions +@Deprecated("This constructor was exposed by mistake and will be removed in a future version.", level = DeprecationLevel.ERROR) +@ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) +constructor( + val addKotlinLabsDefinitions: Boolean, + val foreignSchemas: List, + val excludeCacheDirectives: Boolean, + val computeKeyFields: Boolean, + val mergeOptions: MergeOptions, +) { + class Builder { + var addKotlinLabsDefinitions: Boolean = false + val foreignSchemas: MutableList = mutableListOf() + var excludeCacheDirectives: Boolean = false + var computeKeyFields: Boolean = true + var mergeOptions: MergeOptions = MergeOptions.Default + + fun addKotlinLabsDefinitions(addKotlinLabsDefinitions: Boolean) = apply { + this.addKotlinLabsDefinitions = addKotlinLabsDefinitions + } + + fun foreignSchemas(schemas: List): Builder = apply { + foreignSchemas.clear() + foreignSchemas.addAll(schemas) + } + + fun addForeignSchema(schema: ForeignSchema): Builder = apply { + foreignSchemas.add(schema) + } + + fun excludeCacheDirectives(excludeCacheDirectives: Boolean) = apply { + this.excludeCacheDirectives = excludeCacheDirectives + } + + fun computeKeyFields(computeKeyFields: Boolean) = apply { + this.computeKeyFields = computeKeyFields + } + + fun mergeOptions(mergeOptions: MergeOptions) = apply { + this.mergeOptions = mergeOptions + } + + fun build(): SchemaValidationOptions { + @Suppress("DEPRECATION_ERROR") + return SchemaValidationOptions( + addKotlinLabsDefinitions = addKotlinLabsDefinitions, + foreignSchemas = foreignSchemas, + excludeCacheDirectives = excludeCacheDirectives, + computeKeyFields = computeKeyFields, + mergeOptions = mergeOptions + ) + } + } + + @Deprecated("This constructor was exposed by mistake and will be removed in a future version.", level = DeprecationLevel.ERROR) + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + @Suppress("DEPRECATION_ERROR") + constructor( + addKotlinLabsDefinitions: Boolean, + foreignSchemas: List, + excludeCacheDirectives: Boolean, + ) : this( + addKotlinLabsDefinitions = addKotlinLabsDefinitions, + foreignSchemas = foreignSchemas, + excludeCacheDirectives = excludeCacheDirectives, + computeKeyFields = true, + mergeOptions = MergeOptions.Default, + ) + + @Deprecated("This constructor was exposed by mistake and will be removed in a future version.", level = DeprecationLevel.ERROR) + @ApolloDeprecatedSince(ApolloDeprecatedSince.Version.v5_0_0) + @Suppress("DEPRECATION_ERROR") + constructor( + addKotlinLabsDefinitions: Boolean, + foreignSchemas: List, + ) : this( + addKotlinLabsDefinitions = addKotlinLabsDefinitions, + foreignSchemas = foreignSchemas, + excludeCacheDirectives = false, + computeKeyFields = true, + mergeOptions = MergeOptions.Default, + ) +} \ No newline at end of file diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/api.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/api.kt index 9170bd3604e..04fba443aaa 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/api.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/api.kt @@ -9,7 +9,6 @@ import com.apollographql.apollo.ast.internal.ExecutableValidationScope import com.apollographql.apollo.ast.internal.LexerException import com.apollographql.apollo.ast.internal.Parser import com.apollographql.apollo.ast.internal.ParserException -import com.apollographql.apollo.ast.internal.SchemaValidationOptions import com.apollographql.apollo.ast.internal.validateSchema import okio.BufferedSource import okio.use @@ -224,7 +223,7 @@ fun BufferedSource.parseAsGQLSelections( */ @ApolloExperimental fun GQLDocument.validateAsSchema(): GQLResult { - return validateSchema(definitions, SchemaValidationOptions(false, emptyList())) + return validateSchema(definitions, SchemaValidationOptions.Builder().build()) } @ApolloExperimental @@ -236,10 +235,10 @@ fun GQLDocument.validateAsSchema(validationOptions: SchemaValidationOptions): GQ fun GQLDocument.validateAsSchemaAndAddApolloDefinition(): GQLResult { return validateSchema( definitions, - SchemaValidationOptions( - true, - builtinForeignSchemas() - ) + SchemaValidationOptions.Builder() + .addKotlinLabsDefinitions(true) + .foreignSchemas(builtinForeignSchemas()) + .build() ) } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gqldocument.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gqldocument.kt index d0d7600f80f..9c24fd51858 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gqldocument.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/gqldocument.kt @@ -63,11 +63,10 @@ fun GQLDocument.toSchema(): Schema = validateAsSchema().getOrThrow() @ApolloExperimental class MergeOptions( - val allowAddingDirectivesToExistingFieldDefinitions: Boolean, - val allowFieldNullabilityModification: Boolean + val allowMergingFieldDefinitions: Boolean, ) { companion object { - val Default: MergeOptions = MergeOptions(false, false) + val Default: MergeOptions = MergeOptions(false) } } diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt index d10aa1541f2..3e19d93cf9f 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/ExtensionsMerger.kt @@ -75,6 +75,57 @@ internal class ExtensionsMerger(private val definitions: List, in return GQLResult(newDefinitions, issues) } + + fun mergeFieldDefinition( + issues: MutableList, + existing: GQLFieldDefinition, + incoming: GQLFieldDefinition, + ): GQLFieldDefinition? { + // Cannot change the type + if (!areEqual(existing.type, incoming.type)) { + issues.add(OtherValidationIssue("Cannot merge field '${incoming.name}': wrong type '${incoming.type.toUtf8()}' (expected: '${existing.type.toUtf8()}')", incoming.sourceLocation)) + return null + } + + if (incoming.description != null) { + issues.add(OtherValidationIssue("Cannot merge field '${incoming.name}': descriptions cannot be merged", incoming.sourceLocation)) + } + + return existing.copy( + directives = mergeDirectives(existing.directives, incoming.directives), + arguments = mergeArguments(existing.arguments, incoming.arguments) + ) + } +} + +private fun ExtensionsMerger.mergeArguments( + existingDefinitions: List, + incomingDefinitions: List, +): List { + val newArguments = mutableListOf() + newArguments.addAll(existingDefinitions) + incomingDefinitions.forEach { incoming -> + val index = newArguments.indexOfFirst { arg -> arg.name == incoming.name } + if (index != -1) { + val existing = newArguments[index] + if (!areEqual(existing.type, incoming.type)) { + issues.add(OtherValidationIssue("Cannot merge argument '${incoming.name}': wrong type '${incoming.type.toUtf8()}' (expected: '${existing.type.toUtf8()}')", incoming.sourceLocation)) + } + if (incoming.description != null) { + issues.add(OtherValidationIssue("Cannot merge argument '${incoming.name}': descriptions cannot be merged", incoming.sourceLocation)) + } + if (incoming.defaultValue != null) { + issues.add(OtherValidationIssue("Cannot merge argument '${incoming.name}': default values cannot be merged", incoming.sourceLocation)) + } + newArguments[index] = existing.copy( + directives = mergeDirectives(existing.directives, incoming.directives) + ) + } else { + newArguments.add(incoming) + } + } + + return newArguments } private fun ExtensionsMerger.mergeUnion( @@ -327,40 +378,15 @@ private fun ExtensionsMerger.mergeFields( // field doesn't exist, add it result.add(newFieldDefinition) } else { - if (!mergeOptions.allowFieldNullabilityModification && !mergeOptions.allowAddingDirectivesToExistingFieldDefinitions) { + if (!mergeOptions.allowMergingFieldDefinitions) { issues.add(OtherValidationIssue("There is already a field definition named `${newFieldDefinition.name}` for this type", newFieldDefinition.sourceLocation)) return@forEach } - val existingFieldDefinition = result[index] - if (!areEqual(newFieldDefinition.arguments, existingFieldDefinition.arguments)) { - issues.add(OtherValidationIssue("Cannot merge field definition `${newFieldDefinition.name}`: its arguments do not match the arguments of the original field definition", newFieldDefinition.sourceLocation)) - return@forEach + val mergedFieldDefinition = mergeFieldDefinition(issues, result[index], newFieldDefinition) + if (mergedFieldDefinition != null) { + result[index] = mergedFieldDefinition } - - if (mergeOptions.allowFieldNullabilityModification) { - if (!newFieldDefinition.type.isCompatibleWith(existingFieldDefinition.type)) { - issues.add(OtherValidationIssue("Cannot merge field definition `${newFieldDefinition.name}`: its type is not compatible with the original type.", newFieldDefinition.sourceLocation)) - return@forEach - } - } else { - if (newFieldDefinition.type.toUtf8() != existingFieldDefinition.type.toUtf8()) { - issues.add(OtherValidationIssue("Cannot merge field definition`${newFieldDefinition.name}`: they have different types.", newFieldDefinition.sourceLocation)) - return@forEach - } - } - if (!mergeOptions.allowAddingDirectivesToExistingFieldDefinitions) { - if (newFieldDefinition.directives.isNotEmpty()) { - issues.add(OtherValidationIssue("Cannot add directives to existing field definition `${newFieldDefinition.name}`", newFieldDefinition.sourceLocation)) - return@forEach - } - } - - /* - * No need to validate repeated directives, this is done later on by schema validation. - */ - val newDirectives = existingFieldDefinition.directives + newFieldDefinition.directives - result[index] = existingFieldDefinition.copy(type = newFieldDefinition.type, directives = newDirectives) } } @@ -403,18 +429,29 @@ private fun GQLType.isCompatibleWith(other: GQLType): Boolean { } } -private fun areEqual(a: List, b: List): Boolean { - if (a.size != b.size) { - return false - } +private fun areEqual(a: GQLType, b: GQLType): Boolean { + when (a) { + is GQLListType -> { + if (b !is GQLNonNullType) { + return false + } + return areEqual(a.type, b.type) + } - a.forEachIndexed { index, aDefinition -> - if (aDefinition.toUtf8() != b[index].toUtf8()) { - return false + is GQLNamedType -> { + if (b !is GQLNamedType) { + return false + } + return a.name == b.name } - } - return true + is GQLNonNullType -> { + if (b !is GQLNonNullType) { + return false + } + return areEqual(a.type, b.type) + } + } } private inline fun ExtensionsMerger.mergeUniquesOrThrow( diff --git a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt index 52a9206b899..dc5475bd47c 100644 --- a/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt +++ b/libraries/apollo-ast/src/commonMain/kotlin/com/apollographql/apollo/ast/internal/SchemaValidationScope.kt @@ -1,5 +1,6 @@ package com.apollographql.apollo.ast.internal +import com.apollographql.apollo.annotations.ApolloDeprecatedSince import com.apollographql.apollo.annotations.ApolloExperimental import com.apollographql.apollo.ast.DirectiveRedefinition import com.apollographql.apollo.ast.ForeignSchema @@ -36,6 +37,7 @@ import com.apollographql.apollo.ast.NoQueryType import com.apollographql.apollo.ast.OtherValidationIssue import com.apollographql.apollo.ast.Schema import com.apollographql.apollo.ast.Schema.Companion.TYPE_POLICY +import com.apollographql.apollo.ast.SchemaValidationOptions import com.apollographql.apollo.ast.SourceLocation import com.apollographql.apollo.ast.autoLinkedKotlinLabsForeignSchema import com.apollographql.apollo.ast.autoLinkedKotlinLabsForeignSchemaNoCache @@ -49,41 +51,6 @@ import com.apollographql.apollo.ast.rawType import com.apollographql.apollo.ast.transform2 import com.apollographql.apollo.ast.withBuiltinDefinitions -/** - * @param addKotlinLabsDefinitions automatically import the kotlin_labs definitions, even if no `@link` is present. If [excludeCacheDirectives] is `true`, cache related directives are excluded. - * @param foreignSchemas a list of known [ForeignSchema] that may or may not be imported depending on the `@link` directives - * @param excludeCacheDirectives whether to exclude cache related directives when auto-importing the kotlin_labs definitions. Has no effect if [addKotlinLabsDefinitions] is `false`. - * @param computeKeyFields whether to compute cache key fields. Can be false when using the Apollo Cache compiler plugin to avoid unneeded computation. - */ -@ApolloExperimental -class SchemaValidationOptions( - val addKotlinLabsDefinitions: Boolean, - val foreignSchemas: List, - val excludeCacheDirectives: Boolean, - val computeKeyFields: Boolean, -) { - constructor( - addKotlinLabsDefinitions: Boolean, - foreignSchemas: List, - excludeCacheDirectives: Boolean, - ) : this( - addKotlinLabsDefinitions = addKotlinLabsDefinitions, - foreignSchemas = foreignSchemas, - excludeCacheDirectives = excludeCacheDirectives, - computeKeyFields = true, - ) - - constructor( - addKotlinLabsDefinitions: Boolean, - foreignSchemas: List, - ) : this( - addKotlinLabsDefinitions = addKotlinLabsDefinitions, - foreignSchemas = foreignSchemas, - excludeCacheDirectives = false, - computeKeyFields = true, - ) -} - private fun ForeignSchema.asNonPrefixedImport(): LinkedSchema { return LinkedSchema(this, definitions, definitions.map { (it as GQLNamed).definitionName() }.associateBy { it }, null) } @@ -331,7 +298,7 @@ internal fun validateSchema(definitions: List, options: SchemaVal * Moving forward, extensions merging should probably be done first thing as a separate step, before any validation and/or linking of foreign schemas. */ val dedupedDefinitions = listOfNotNull(schemaDefinition) + directiveDefinitions.values + typeDefinitions.values - val mergedDefinitions = ExtensionsMerger(dedupedDefinitions + typeSystemExtensions, MergeOptions(false, true)).merge().getOrThrow() + val mergedDefinitions = ExtensionsMerger(dedupedDefinitions + typeSystemExtensions, options.mergeOptions).merge().getOrThrow() val mergedScope = DefaultValidationScope( typeDefinitions = mergedDefinitions.filterIsInstance().associateBy { it.name }, @@ -445,7 +412,8 @@ private fun List.getLinkedSchemas( val schemaExtensions = this val linkedSchemas = mutableListOf() - val foreignSchema = ForeignSchema("link", "v1.0", linkDefinitions()) + val foreignSchema = + ForeignSchema("link", "v1.0", linkDefinitions()) /** * Link the @link definitions using a very specific import for Import and Purpose to avoid clashing with user directives. diff --git a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt index 26fab81eb3d..9d8d1cfa83c 100644 --- a/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt +++ b/libraries/apollo-ast/src/commonTest/kotlin/com/apollographql/apollo/graphql/ast/test/SchemaTest.kt @@ -5,7 +5,7 @@ package com.apollographql.apollo.graphql.ast.test import com.apollographql.apollo.annotations.ApolloExperimental import com.apollographql.apollo.ast.ForeignSchema import com.apollographql.apollo.ast.builtinForeignSchemas -import com.apollographql.apollo.ast.internal.SchemaValidationOptions +import com.apollographql.apollo.ast.SchemaValidationOptions import com.apollographql.apollo.ast.internal.toSemanticSdl import com.apollographql.apollo.ast.parseAsGQLDocument import com.apollographql.apollo.ast.toGQLDocument @@ -54,10 +54,10 @@ class SchemaTest { """.trimIndent() schemaString.toGQLDocument().validateAsSchema( - SchemaValidationOptions( - addKotlinLabsDefinitions = false, - foreignSchemas = listOf(cacheControlSchema) - ) + SchemaValidationOptions.Builder() + .addKotlinLabsDefinitions(false) + .addForeignSchema(cacheControlSchema) + .build() ).getOrThrow() } @@ -73,10 +73,10 @@ class SchemaTest { """.trimIndent() val schema = schemaString.toGQLDocument().validateAsSchema( - SchemaValidationOptions( - addKotlinLabsDefinitions = false, - foreignSchemas = listOf(cacheControlSchema) - ) + SchemaValidationOptions.Builder() + .addKotlinLabsDefinitions(false) + .addForeignSchema(cacheControlSchema) + .build() ).getOrThrow() assertEquals("cacheControl", schema.originalDirectiveName("cacheControl")) @@ -98,9 +98,8 @@ class SchemaTest { """.trimIndent() val schema = schemaString.toGQLDocument().validateAsSchema( - SchemaValidationOptions( - addKotlinLabsDefinitions = false, - foreignSchemas = listOf( + SchemaValidationOptions.Builder() + .foreignSchemas(listOf( cacheControlSchema, ForeignSchema("example", "v0.1", listOf("directive @example on FIELD_DEFINITION".parseAsGQLDocument().getOrThrow().definitions.single()) @@ -109,10 +108,11 @@ class SchemaTest { listOf("directive @example2 on FIELD_DEFINITION".parseAsGQLDocument().getOrThrow().definitions.single()) ) ) - ) + ).build() ).getOrThrow() - assertEquals("cacheControl", schema.originalDirectiveName("cacheControl")) + assertEquals("cacheControl", schema.originalDirectiveName("cacheControl") + ) assertEquals("example", schema.originalDirectiveName("example")) } @@ -128,10 +128,9 @@ class SchemaTest { """.trimIndent() val result = schemaString.toGQLDocument().validateAsSchema( - SchemaValidationOptions( - addKotlinLabsDefinitions = false, - foreignSchemas = listOf(cacheControlSchema) - ) + SchemaValidationOptions.Builder() + .addForeignSchema(cacheControlSchema) + .build() ) assertEquals(1, result.issues.size) @@ -150,10 +149,9 @@ class SchemaTest { """.trimIndent() val result = schemaString.toGQLDocument().validateAsSchema( - SchemaValidationOptions( - addKotlinLabsDefinitions = false, - foreignSchemas = listOf(cacheControlSchema) - ) + SchemaValidationOptions.Builder() + .addForeignSchema(cacheControlSchema) + .build() ) assertEquals(1, result.issues.size) @@ -204,12 +202,9 @@ class SchemaTest { extend schema @catchByDefault(to: THROW) """.trimIndent() schemaString.toGQLDocument().validateAsSchema( - SchemaValidationOptions( - foreignSchemas = builtinForeignSchemas(), - addKotlinLabsDefinitions = false, - excludeCacheDirectives = true, - computeKeyFields = false - ) + SchemaValidationOptions.Builder() + .foreignSchemas(builtinForeignSchemas()) + .build() ).getOrThrow() } } diff --git a/libraries/apollo-ast/src/jvmTest/kotlin/com/apollographql/apollo/graphql/ast/test/TypeExtensionsMergeTest.kt b/libraries/apollo-ast/src/jvmTest/kotlin/com/apollographql/apollo/graphql/ast/test/TypeExtensionsMergeTest.kt index 12b267ab7bf..de85955ce96 100644 --- a/libraries/apollo-ast/src/jvmTest/kotlin/com/apollographql/apollo/graphql/ast/test/TypeExtensionsMergeTest.kt +++ b/libraries/apollo-ast/src/jvmTest/kotlin/com/apollographql/apollo/graphql/ast/test/TypeExtensionsMergeTest.kt @@ -1,70 +1,221 @@ package com.apollographql.apollo.graphql.ast.test -import com.apollographql.apollo.ast.GQLObjectTypeDefinition +import com.apollographql.apollo.ast.Issue import com.apollographql.apollo.ast.MergeOptions import com.apollographql.apollo.ast.mergeExtensions -import com.apollographql.apollo.ast.toMergedGQLDocument +import com.apollographql.apollo.ast.parseAsGQLDocument import com.apollographql.apollo.ast.toGQLDocument import com.apollographql.apollo.ast.toUtf8 import org.intellij.lang.annotations.Language import kotlin.test.Test import kotlin.test.assertEquals -import kotlin.test.assertTrue class TypeExtensionsMergeTest { + @Test - fun simpleTest() { + fun cannotChangeFieldType() { @Language("graphqls") val sdl = """ type Query { random: Int - list: [String] - required: Int! - } - + } extend type Query { - random: Int! - list: [String!]! - required: Int - new: Float + random: String } """.trimIndent() - val result = sdl.toGQLDocument().toMergedGQLDocument(MergeOptions(false, true)) - .definitions - .single() as GQLObjectTypeDefinition + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).issues.apply { + assertEquals(1, size) + assertEquals("5:3 Cannot merge field 'random': wrong type 'String' (expected: 'Int')", get(0).pretty()) + } + } + @Test + fun cannotAddDescriptionToType() { + @Language("graphqls") + val sdl = """ + type Query { + random: Int + } + + "The root query" + extend type Query + """.trimIndent() - assertEquals("Int!", result.fields.first { it.name == "random" }.type.toUtf8()) - assertEquals("[String!]!", result.fields.first { it.name == "list" }.type.toUtf8()) - assertEquals("Int", result.fields.first { it.name == "required" }.type.toUtf8()) - assertEquals("Float", result.fields.first { it.name == "new" }.type.toUtf8()) + // Note how this is a parsing issue, not validation + sdl.parseAsGQLDocument().issues.apply { + assertEquals(1, size) + assertEquals("6:1 Type system extensions cannot have a description", get(0).pretty()) + } } @Test - fun errors() { + fun cannotAddDescriptionToExistingField() { @Language("graphqls") val sdl = """ type Query { random: Int - list: [String] - required: Int! + } + + extend type Query { + "A new field" + random: Int } + """.trimIndent() + + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).issues.apply { + assertEquals(1, size) + assertEquals("6:3 Cannot merge field 'random': descriptions cannot be merged", get(0).pretty()) + } + } + + @Test + fun cannotAddDescriptionToExistingArgument() { + @Language("graphqls") + val sdl = """ + type Query { + random(id: ID!): Int + } - directive @custom on FIELD_DEFINITION + extend type Query { + random("A new argument" id: ID!): Int + } + """.trimIndent() + + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).issues.apply { + assertEquals(1, size) + assertEquals("6:10 Cannot merge argument 'id': descriptions cannot be merged", get(0).pretty()) + } + } + + @Test + fun cannotAddDefaultValueToExistingArgument() { + @Language("graphqls") + val sdl = """ + type Query { + random(id: ID!): Int + } extend type Query { - random: String - list(arg: String): [String!]! - required: Int @custom + random(id: ID! = "42"): Int } """.trimIndent() - val issues = sdl.toGQLDocument().mergeExtensions(MergeOptions(false, true)).issues + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).issues.apply { + assertEquals(1, size) + assertEquals("6:10 Cannot merge argument 'id': default values cannot be merged", get(0).pretty()) + } + } + + @Test + fun addDirectiveToExistingField() { + @Language("graphqls") + val sdl = """ + type Query { + random: Int + } + extend type Query { + random: Int @deprecated + } + """.trimIndent() + + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).getOrThrow().toUtf8().apply { + assertEquals(""" + type Query { + random: Int @deprecated + } + + """.trimIndent(), this ) + } + } + + @Test + fun addArgumentToExistingField() { + @Language("graphqls") + val sdl = """ + type Query { + random: Int + } + extend type Query { + random(id: ID!): Int + } + """.trimIndent() + + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).getOrThrow().toUtf8().apply { + assertEquals(""" + type Query { + random(id: ID!): Int + } + + """.trimIndent(), this ) + } + } + + private fun Issue.pretty(): String = "${sourceLocation?.line}:${sourceLocation?.column} $message" + + @Test + fun addDirectiveToInterfaceField() { + @Language("graphqls") + val sdl = """ + interface Foo { + random: Int + } + extend interface Foo { + random: Int @deprecated + } + """.trimIndent() + + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).getOrThrow().toUtf8().apply { + assertEquals(""" + interface Foo { + random: Int @deprecated + } + + """.trimIndent(), this ) + } + } + + @Test + fun fieldDescriptionIsPreserved() { + @Language("graphqls") + val sdl = """ + type Query { + "A random field" + random: Int + } + extend type Query { + random: Int @deprecated + } + """.trimIndent() + + sdl.toGQLDocument().mergeExtensions(MergeOptions(true)).getOrThrow().toUtf8().apply { + assertEquals(""" + type Query { + ""${'"'} + A random field + ""${'"'} + random: Int @deprecated + } + + """.trimIndent(), this ) + } + } + + @Test + fun byDefaultCannotExtendField() { + @Language("graphqls") + val sdl = """ + type Query { + random: Int + } + extend type Query { + random: Int @deprecated + } + """.trimIndent() - assertEquals(3, issues.size) - assertTrue(issues[0].message.contains("its type is not compatible with the original type")) - assertTrue(issues[1].message.contains("its arguments do not match the arguments of the original field definition")) - assertTrue(issues[2].message.contains("Cannot add directives to existing field definition")) + sdl.toGQLDocument().mergeExtensions(MergeOptions(false)).issues.apply { + assertEquals(1, size) + assertEquals("5:3 There is already a field definition named `random` for this type", first().pretty()) + } } } diff --git a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ApolloCompiler.kt b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ApolloCompiler.kt index 502aa40d931..07f807a25d7 100644 --- a/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ApolloCompiler.kt +++ b/libraries/apollo-compiler/src/main/kotlin/com/apollographql/apollo/compiler/ApolloCompiler.kt @@ -9,11 +9,12 @@ import com.apollographql.apollo.ast.GQLOperationDefinition import com.apollographql.apollo.ast.GQLSchemaDefinition import com.apollographql.apollo.ast.GQLTypeDefinition import com.apollographql.apollo.ast.Issue +import com.apollographql.apollo.ast.MergeOptions import com.apollographql.apollo.ast.ParserOptions import com.apollographql.apollo.ast.QueryDocumentMinifier import com.apollographql.apollo.ast.builtinForeignSchemas import com.apollographql.apollo.ast.checkEmpty -import com.apollographql.apollo.ast.internal.SchemaValidationOptions +import com.apollographql.apollo.ast.SchemaValidationOptions import com.apollographql.apollo.ast.parseAsGQLDocument import com.apollographql.apollo.ast.pretty import com.apollographql.apollo.ast.toGQLDocument @@ -175,21 +176,22 @@ object ApolloCompiler { } val result = schemaDocument.validateAsSchema( - validationOptions = SchemaValidationOptions( + validationOptions = SchemaValidationOptions.Builder() /** * TODO: switch to false */ - addKotlinLabsDefinitions = true, - foreignSchemas = builtinForeignSchemas() + foreignSchemas, + .addKotlinLabsDefinitions(true) + .foreignSchemas(builtinForeignSchemas() + foreignSchemas) /** * If the cache compiler plugin is present and provides the cache directives, don't automatically import the cache related directives to avoid a conflict */ - excludeCacheDirectives = cacheCompilerPluginHasCacheDirectives, + .excludeCacheDirectives(cacheCompilerPluginHasCacheDirectives) /** * If the cache compiler plugin is present and computes key fields, don't compute them to save redundant work */ - computeKeyFields = !cacheCompilerPluginComputesKeys, - ) + .computeKeyFields(!cacheCompilerPluginComputesKeys) + .mergeOptions(MergeOptions(true)) + .build() ) // TODO: allow the user to override the severities for schema validation