diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md index f9452b0ca..3403978e7 100644 --- a/crates/apollo-compiler/CHANGELOG.md +++ b/crates/apollo-compiler/CHANGELOG.md @@ -17,6 +17,31 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Maintenance ## Documentation--> +# [1.32.0](https://crates.io/crates/apollo-compiler/1.32.0) - 2026-03-25 + +## Features + +- **Implement `@oneOf` input objects - [issue/882].** + Adds full support for the [`@oneOf` RFC](https://github.com/graphql/graphql-spec/pull/825) + as defined in the GraphQL draft specification (§3.10.1 OneOf Input Objects). + + - `directive @oneOf on INPUT_OBJECT` is now a built-in directive definition. + - `__Type.isOneOf: Boolean!` introspection field is now exposed for all types + (returns `true` only for `@oneOf` input objects). + - New schema validation rules (enforced in `Schema::parse_and_validate`): + - All fields of a `@oneOf` input object must be nullable. + - No field of a `@oneOf` input object may have a default value. + - New executable-document validation rules (enforced in `ExecutableDocument::parse_and_validate`): + - A literal `@oneOf` input object value must supply exactly one field, + and that field's value must be non-null. + - A variable used as the sole value of a `@oneOf` field must be declared + with a non-null type (e.g. `String!`, not `String`). + - Runtime input coercion (`coerce_variable_values`) now also enforces the + "exactly one non-null field" invariant for `@oneOf` types. + - `InputObjectType::is_one_of() -> bool` convenience method added. + +[issue/882]: https://github.com/apollographql/apollo-rs/issues/882 + # [1.31.1](https://crates.io/crates/apollo-compiler/1.31.1) - 2026-02-20 ## Fixes diff --git a/crates/apollo-compiler/Cargo.toml b/crates/apollo-compiler/Cargo.toml index ad08d262d..c942f3fd9 100644 --- a/crates/apollo-compiler/Cargo.toml +++ b/crates/apollo-compiler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-compiler" -version = "1.31.1" # When bumping, also update README.md +version = "1.32.0" # When bumping, also update README.md authors = ["Irina Shestak "] license = "MIT OR Apache-2.0" repository = "https://github.com/apollographql/apollo-rs" diff --git a/crates/apollo-compiler/src/built_in_types.graphql b/crates/apollo-compiler/src/built_in_types.graphql index 7cb6f92f3..c36c07528 100644 --- a/crates/apollo-compiler/src/built_in_types.graphql +++ b/crates/apollo-compiler/src/built_in_types.graphql @@ -32,6 +32,8 @@ type __Type { ofType: __Type # may be non-null for custom SCALAR, otherwise null. specifiedByURL: String + # always non-null; true only for @oneOf INPUT_OBJECT types. + isOneOf: Boolean! } "An enum describing what kind of type a given `__Type` is." @@ -154,6 +156,9 @@ directive @deprecated( reason: String = "No longer supported" ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE +"Indicates an Input Object is a OneOf Input Object." +directive @oneOf on INPUT_OBJECT + "Exposes a URL that specifies the behavior of this scalar." directive @specifiedBy( "The URL that specifies the behavior of this scalar." diff --git a/crates/apollo-compiler/src/introspection/resolvers.rs b/crates/apollo-compiler/src/introspection/resolvers.rs index fb05b36d1..028c171e5 100644 --- a/crates/apollo-compiler/src/introspection/resolvers.rs +++ b/crates/apollo-compiler/src/introspection/resolvers.rs @@ -224,6 +224,9 @@ impl ObjectValue for TypeDefResolver<'_> { .and_then(|arg| arg.as_str()), )) } + "isOneOf" => Ok(ResolvedValue::leaf( + matches!(self.def, schema::ExtendedType::InputObject(def) if def.is_one_of()), + )), _ => Err(self.unknown_field_error(info)), } } @@ -261,6 +264,7 @@ impl ObjectValue for TypeResolver<'_> { "enumValues" => Ok(ResolvedValue::null()), "inputFields" => Ok(ResolvedValue::null()), "specifiedByURL" => Ok(ResolvedValue::null()), + "isOneOf" => Ok(ResolvedValue::leaf(false)), _ => Err(self.unknown_field_error(info)), } } diff --git a/crates/apollo-compiler/src/resolvers/input_coercion.rs b/crates/apollo-compiler/src/resolvers/input_coercion.rs index 511584d1c..c93aa6eaf 100644 --- a/crates/apollo-compiler/src/resolvers/input_coercion.rs +++ b/crates/apollo-compiler/src/resolvers/input_coercion.rs @@ -178,6 +178,36 @@ fn coerce_variable_value( location: None, }); } + // @oneOf: exactly one non-null field must be provided at runtime. + // https://spec.graphql.org/draft/#sec-OneOf-Input-Objects + if ty_def.is_one_of() { + let provided_count = object + .keys() + .filter(|k| ty_def.fields.contains_key(k.as_str())) + .count(); + if provided_count != 1 { + return Err(InputCoercionError::ValueError { + message: format!( + "@oneOf input object '{ty_name}' must specify exactly one key, \ + but {provided_count} {} given", + if provided_count == 1 { "was" } else { "were" } + ), + location: None, + }); + } + if let Some((field_name, field_value)) = object.iter().next() { + if field_value.is_null() { + return Err(InputCoercionError::ValueError { + message: format!( + "@oneOf input object '{ty_name}' field '{}' \ + must be non-null", + field_name.as_str() + ), + location: None, + }); + } + } + } let mut object = object.clone(); for (field_name, field_def) in &ty_def.fields { if let Some(field_value) = object.get_mut(field_name.as_str()) { @@ -407,6 +437,41 @@ fn coerce_argument_value( )); return Err(PropagateNull); } + // @oneOf: exactly one non-null field must be provided at runtime. + // https://spec.graphql.org/draft/#sec-OneOf-Input-Objects + if ty_def.is_one_of() { + let provided_count = object + .iter() + .filter(|(k, _)| ty_def.fields.contains_key(k)) + .count(); + if provided_count != 1 { + ctx.errors.push(GraphQLError::field_error( + format!( + "@oneOf input object '{ty_name}' must specify exactly one key, \ + but {provided_count} {} given", + if provided_count == 1 { "was" } else { "were" } + ), + path, + value.location(), + &ctx.document.sources, + )); + return Err(PropagateNull); + } + if let Some((field_name, field_value)) = object.iter().next() { + if field_value.is_null() { + ctx.errors.push(GraphQLError::field_error( + format!( + "@oneOf input object '{ty_name}' field '{field_name}' \ + must be non-null" + ), + path, + value.location(), + &ctx.document.sources, + )); + return Err(PropagateNull); + } + } + } #[allow(clippy::map_identity)] // `map` converts `&(k, v)` to `(&k, &v)` let object: HashMap<_, _> = object.iter().map(|(k, v)| (k, v)).collect(); let mut coerced_object = JsonMap::new(); @@ -597,4 +662,99 @@ mod tests { ) .unwrap_err(); } + + // ----------------------------------------------------------------------- + // @oneOf runtime coercion tests + // https://spec.graphql.org/draft/#sec-OneOf-Input-Objects + // ----------------------------------------------------------------------- + + fn one_of_schema_and_doc() -> (Valid, Valid) { + let schema = Schema::parse_and_validate( + r#" + type Query { + search(filter: SearchFilter): String + } + input SearchFilter @oneOf { + byName: String + byId: Int + } + "#, + "schema.graphql", + ) + .unwrap(); + let doc = ExecutableDocument::parse_and_validate( + &schema, + "query ($filter: SearchFilter) { search(filter: $filter) }", + "op.graphql", + ) + .unwrap(); + (schema, doc) + } + + #[test] + fn one_of_coercion_valid_single_field() { + let (schema, doc) = one_of_schema_and_doc(); + let variables = serde_json_bytes::json!({ "filter": { "byName": "alice" } }); + coerce_variable_values( + &schema, + doc.operations.anonymous.as_ref().unwrap(), + variables.as_object().unwrap(), + ) + .expect("single non-null field should be accepted"); + } + + fn one_of_error_message(err: InputCoercionError) -> String { + match err { + InputCoercionError::ValueError { message, .. } => message, + InputCoercionError::SuspectedValidationBug(b) => b.message, + } + } + + #[test] + fn one_of_coercion_rejects_zero_fields() { + let (schema, doc) = one_of_schema_and_doc(); + let variables = serde_json_bytes::json!({ "filter": {} }); + let err = coerce_variable_values( + &schema, + doc.operations.anonymous.as_ref().unwrap(), + variables.as_object().unwrap(), + ) + .unwrap_err(); + let msg = one_of_error_message(err); + assert!( + msg.contains("must specify exactly one key"), + "unexpected error: {msg}" + ); + } + + #[test] + fn one_of_coercion_rejects_multiple_fields() { + let (schema, doc) = one_of_schema_and_doc(); + let variables = serde_json_bytes::json!({ "filter": { "byName": "alice", "byId": 1 } }); + let err = coerce_variable_values( + &schema, + doc.operations.anonymous.as_ref().unwrap(), + variables.as_object().unwrap(), + ) + .unwrap_err(); + let msg = one_of_error_message(err); + assert!( + msg.contains("must specify exactly one key"), + "unexpected error: {msg}" + ); + } + + #[test] + fn one_of_coercion_rejects_null_field_value() { + let (schema, doc) = one_of_schema_and_doc(); + let variables = serde_json_bytes::json!({ "filter": { "byName": null } }); + let err = coerce_variable_values( + &schema, + doc.operations.anonymous.as_ref().unwrap(), + variables.as_object().unwrap(), + ) + .unwrap_err(); + let msg = one_of_error_message(err); + assert!(msg.contains("must be non-null"), "unexpected error: {msg}"); + } } diff --git a/crates/apollo-compiler/src/schema/mod.rs b/crates/apollo-compiler/src/schema/mod.rs index 5bffe95ec..8ed2acd16 100644 --- a/crates/apollo-compiler/src/schema/mod.rs +++ b/crates/apollo-compiler/src/schema/mod.rs @@ -1015,6 +1015,11 @@ impl EnumType { } impl InputObjectType { + /// Returns true if this is a OneOf Input Object (has the `@oneOf` directive). + pub fn is_one_of(&self) -> bool { + self.directives.get("oneOf").is_some() + } + /// Iterate over the `origins` of all components /// /// The order of the returned set is unspecified but deterministic diff --git a/crates/apollo-compiler/src/validation/diagnostics.rs b/crates/apollo-compiler/src/validation/diagnostics.rs index 26d6b7a73..e09a64df3 100644 --- a/crates/apollo-compiler/src/validation/diagnostics.rs +++ b/crates/apollo-compiler/src/validation/diagnostics.rs @@ -292,8 +292,39 @@ pub(crate) enum DiagnosticData { "{describe} cannot be named `{name}` as names starting with two underscores are reserved" )] ReservedName { name: Name, describe: &'static str }, + #[error("`{coordinate}` field of a @oneOf input object must be nullable")] + OneOfInputObjectFieldNonNull { + coordinate: TypeAttributeCoordinate, + definition_location: Option, + }, + #[error( + "@oneOf input object `{name}` must specify exactly one key, but {provided} {} given", + if *provided == 1 { "was" } else { "were" } + )] + OneOfInputObjectWrongNumberOfFields { name: Name, provided: usize }, + #[error("`{name}.{field}` value for @oneOf input object must be non-null")] + OneOfInputObjectNullField { name: Name, field: Name }, + #[error("`{coordinate}` field of a @oneOf input object must not have a default value")] + OneOfInputObjectFieldHasDefault { + coordinate: TypeAttributeCoordinate, + default_location: Option, + }, + #[error( + "variable `${variable}` is of type `{variable_type}` \ + but must be non-nullable to be used for @oneOf input object `{name}` field `{field}`" + )] + OneOfInputObjectNullableVariable { + name: Name, + field: Name, + variable: Name, + variable_type: Node, + }, } +/// Shared help text for the two @oneOf schema-definition errors. +const ONE_OF_FIELD_REQUIREMENTS: &str = + "Fields of a @oneOf input object must all be nullable and must not have default values."; + impl DiagnosticData { pub(crate) fn report(&self, main_location: Option, report: &mut CliReport) { match self { @@ -721,6 +752,66 @@ impl DiagnosticData { DiagnosticData::ReservedName { name, .. } => { report.with_label_opt(name.location(), "Pick a different name here"); } + DiagnosticData::OneOfInputObjectFieldNonNull { + coordinate, + definition_location, + } => { + report.with_label_opt( + *definition_location, + format_args!("field `{coordinate}` defined here"), + ); + report.with_label_opt(main_location, "remove the `!` to make this field nullable"); + report.with_help(ONE_OF_FIELD_REQUIREMENTS); + } + DiagnosticData::OneOfInputObjectWrongNumberOfFields { name, provided } => { + report.with_label_opt( + main_location, + format_args!( + "{provided} {} provided", + if *provided == 1 { + "field was" + } else { + "fields were" + } + ), + ); + report.with_help(format_args!( + "@oneOf input object `{name}` requires exactly one non-null field." + )); + } + DiagnosticData::OneOfInputObjectNullField { name, field } => { + report.with_label_opt(main_location, "this value is null"); + report.with_help(format_args!( + "@oneOf input object `{name}` field `{field}` must be non-null." + )); + } + DiagnosticData::OneOfInputObjectFieldHasDefault { + coordinate, + default_location, + } => { + report.with_label_opt( + *default_location, + format_args!("default value for `{coordinate}` defined here"), + ); + report.with_label_opt(main_location, "remove the default value"); + report.with_help(ONE_OF_FIELD_REQUIREMENTS); + } + DiagnosticData::OneOfInputObjectNullableVariable { + name, + field, + variable, + variable_type, + } => { + report.with_label_opt( + main_location, + format_args!( + "variable `${variable}` has type `{variable_type}`, which is nullable" + ), + ); + report.with_help(format_args!( + "use `{variable_type}!` to make this variable non-nullable for @oneOf input object `{name}` field `{field}`." + )); + } } } diff --git a/crates/apollo-compiler/src/validation/input_object.rs b/crates/apollo-compiler/src/validation/input_object.rs index 0ec56c4fd..e50d11271 100644 --- a/crates/apollo-compiler/src/validation/input_object.rs +++ b/crates/apollo-compiler/src/validation/input_object.rs @@ -1,5 +1,6 @@ use crate::ast; use crate::collections::HashMap; +use crate::coordinate::TypeAttributeCoordinate; use crate::schema::validation::BuiltInScalars; use crate::schema::InputObjectType; use crate::validation::diagnostics::DiagnosticData; @@ -100,6 +101,38 @@ pub(crate) fn validate_input_object_definition( } } + // @oneOf input objects: all fields must be nullable and must not have default values. + // https://spec.graphql.org/draft/#sec-OneOf-Input-Objects + if input_object.is_one_of() { + for (field_name, field) in &input_object.fields { + if field.ty.is_non_null() { + diagnostics.push( + field.location(), + DiagnosticData::OneOfInputObjectFieldNonNull { + coordinate: TypeAttributeCoordinate { + ty: input_object.name.clone(), + attribute: field_name.clone(), + }, + definition_location: field.location(), + }, + ); + } + if field.default_value.is_some() { + let default_location = field.default_value.as_ref().and_then(|v| v.location()); + diagnostics.push( + field.location(), + DiagnosticData::OneOfInputObjectFieldHasDefault { + coordinate: TypeAttributeCoordinate { + ty: input_object.name.clone(), + attribute: field_name.clone(), + }, + default_location, + }, + ); + } + } + } + // Fields in an Input Object Definition must be unique // // Returns Unique Definition error. diff --git a/crates/apollo-compiler/src/validation/mod.rs b/crates/apollo-compiler/src/validation/mod.rs index b2409c184..29aa6a9e8 100644 --- a/crates/apollo-compiler/src/validation/mod.rs +++ b/crates/apollo-compiler/src/validation/mod.rs @@ -314,6 +314,13 @@ impl DiagnosticData { EmptyMemberSet { .. } => "EmptyMemberSet", EmptyInputValueSet { .. } => "EmptyInputValueSet", ReservedName { .. } => "ReservedName", + OneOfInputObjectFieldNonNull { .. } => "OneOfInputObjectFieldNonNull", + OneOfInputObjectWrongNumberOfFields { .. } => { + "OneOfInputObjectWrongNumberOfFields" + } + OneOfInputObjectNullField { .. } => "OneOfInputObjectNullField", + OneOfInputObjectFieldHasDefault { .. } => "OneOfInputObjectFieldHasDefault", + OneOfInputObjectNullableVariable { .. } => "OneOfInputObjectNullableVariable", }) } Details::ExecutableBuildError(error) => Some(match error { @@ -512,6 +519,26 @@ impl DiagnosticData { EmptyMemberSet { .. } => None, EmptyInputValueSet { .. } => None, ReservedName { .. } => None, + OneOfInputObjectFieldNonNull { coordinate, .. } => Some(format!( + r#"OneOf input field "{coordinate}" must be nullable."# + )), + OneOfInputObjectWrongNumberOfFields { name, .. } => Some(format!( + r#"OneOf Input Object "{name}" must specify exactly one key."# + )), + OneOfInputObjectNullField { name, field } => Some(format!( + r#"Field "{name}.{field}" used for OneOf Input Object must be non-null."# + )), + OneOfInputObjectFieldHasDefault { coordinate, .. } => Some(format!( + r#"OneOf input field "{coordinate}" cannot have a default value."# + )), + OneOfInputObjectNullableVariable { + name, + field, + variable, + variable_type, + } => Some(format!( + r#"Variable "${variable}" is of type "{variable_type}" but must be non-nullable to be used for OneOf Input Object "{name}" field "{field}"."# + )), } } Details::ExecutableBuildError(error) => match error { diff --git a/crates/apollo-compiler/src/validation/value.rs b/crates/apollo-compiler/src/validation/value.rs index 308701a4c..48d34a61f 100644 --- a/crates/apollo-compiler/src/validation/value.rs +++ b/crates/apollo-compiler/src/validation/value.rs @@ -219,6 +219,48 @@ pub(crate) fn value_of_correct_type( ); } + // @oneOf: exactly one non-null field must be provided. + // https://spec.graphql.org/draft/#sec-OneOf-Input-Objects + if input_obj.is_one_of() { + if obj.len() != 1 { + diagnostics.push( + arg_value.location(), + DiagnosticData::OneOfInputObjectWrongNumberOfFields { + name: input_obj.name.clone(), + provided: obj.len(), + }, + ); + } else if let Some((field_name, field_val)) = obj.first() { + if field_val.is_null() { + diagnostics.push( + field_val.location(), + DiagnosticData::OneOfInputObjectNullField { + name: input_obj.name.clone(), + field: field_name.clone(), + }, + ); + } else if let ast::Value::Variable(var_name) = &**field_val { + // The field value is a variable — the variable must be declared + // non-null. An undefined variable is left to the UndefinedVariable + // rule; we only flag nullable variables that are definitely known. + // https://spec.graphql.org/draft/#sec-All-Variable-Usages-are-Allowed + if let Some(var_def) = var_defs.iter().find(|v| v.name == *var_name) { + if !var_def.ty.is_non_null() { + diagnostics.push( + field_val.location(), + DiagnosticData::OneOfInputObjectNullableVariable { + name: input_obj.name.clone(), + field: field_name.clone(), + variable: var_name.clone(), + variable_type: var_def.ty.clone(), + }, + ); + } + } + } + } + } + input_obj.fields.iter().for_each(|(input_name, f)| { let ty = &f.ty; let is_missing = !obj.iter().any(|(value_name, ..)| input_name == value_name); diff --git a/crates/apollo-compiler/test_data/diagnostics/0050_directives_in_invalid_locations.txt b/crates/apollo-compiler/test_data/diagnostics/0050_directives_in_invalid_locations.txt index 29e93faa3..c592c9852 100644 --- a/crates/apollo-compiler/test_data/diagnostics/0050_directives_in_invalid_locations.txt +++ b/crates/apollo-compiler/test_data/diagnostics/0050_directives_in_invalid_locations.txt @@ -5,11 +5,11 @@ Error: skip directive is not supported for VARIABLE_DEFINITION location │ ───────┬─────── │ ╰───────── directive cannot be used on VARIABLE_DEFINITION │ - ├─[ built_in.graphql:137:1 ] + ├─[ built_in.graphql:139:1 ] │ - 137 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." + 139 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." ┆ ┆ - 141 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 143 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ @@ -22,11 +22,11 @@ Error: skip directive is not supported for QUERY location │ ────────┬─────── │ ╰───────── directive cannot be used on QUERY │ - ├─[ built_in.graphql:137:1 ] + ├─[ built_in.graphql:139:1 ] │ - 137 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." + 139 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." ┆ ┆ - 141 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 143 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ @@ -39,11 +39,11 @@ Error: deprecated directive is not supported for FIELD location │ ─────┬───── │ ╰─────── directive cannot be used on FIELD │ - ├─[ built_in.graphql:149:1 ] + ├─[ built_in.graphql:151:1 ] │ - 149 │ ╭─▶ "Marks an element of a GraphQL schema as no longer supported." + 151 │ ╭─▶ "Marks an element of a GraphQL schema as no longer supported." ┆ ┆ - 155 │ ├─▶ ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE + 157 │ ├─▶ ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE │ │ │ ╰─────────────────────────────────────────────────────────────────────────────────────── directive defined here │ @@ -108,11 +108,11 @@ Error: skip directive is not supported for MUTATION location │ ───────┬─────── │ ╰───────── directive cannot be used on MUTATION │ - ├─[ built_in.graphql:137:1 ] + ├─[ built_in.graphql:139:1 ] │ - 137 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." + 139 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." ┆ ┆ - 141 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 143 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ @@ -125,11 +125,11 @@ Error: skip directive is not supported for INTERFACE location │ ───────┬─────── │ ╰───────── directive cannot be used on INTERFACE │ - ├─[ built_in.graphql:137:1 ] + ├─[ built_in.graphql:139:1 ] │ - 137 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." + 139 │ ╭─▶ "Directs the executor to skip this field or fragment when the `if` argument is true." ┆ ┆ - 141 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 143 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ @@ -155,11 +155,11 @@ Error: include directive is not supported for INPUT_OBJECT location │ ─────────┬──────── │ ╰────────── directive cannot be used on INPUT_OBJECT │ - ├─[ built_in.graphql:143:1 ] + ├─[ built_in.graphql:145:1 ] │ - 143 │ ╭─▶ "Directs the executor to include this field or fragment only when the `if` argument is true." + 145 │ ╭─▶ "Directs the executor to include this field or fragment only when the `if` argument is true." ┆ ┆ - 147 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 149 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ @@ -172,11 +172,11 @@ Error: include directive is not supported for INPUT_FIELD_DEFINITION location │ ─────────┬──────── │ ╰────────── directive cannot be used on INPUT_FIELD_DEFINITION │ - ├─[ built_in.graphql:143:1 ] + ├─[ built_in.graphql:145:1 ] │ - 143 │ ╭─▶ "Directs the executor to include this field or fragment only when the `if` argument is true." + 145 │ ╭─▶ "Directs the executor to include this field or fragment only when the `if` argument is true." ┆ ┆ - 147 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 149 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ @@ -228,11 +228,11 @@ Error: deprecated directive is not supported for OBJECT location │ ─────┬───── │ ╰─────── directive cannot be used on OBJECT │ - ├─[ built_in.graphql:149:1 ] + ├─[ built_in.graphql:151:1 ] │ - 149 │ ╭─▶ "Marks an element of a GraphQL schema as no longer supported." + 151 │ ╭─▶ "Marks an element of a GraphQL schema as no longer supported." ┆ ┆ - 155 │ ├─▶ ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE + 157 │ ├─▶ ) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION | ENUM_VALUE │ │ │ ╰─────────────────────────────────────────────────────────────────────────────────────── directive defined here │ @@ -245,11 +245,11 @@ Error: specifiedBy directive is not supported for ARGUMENT_DEFINITION location │ ────────────────────────────┬─────────────────────────── │ ╰───────────────────────────── directive cannot be used on ARGUMENT_DEFINITION │ - ├─[ built_in.graphql:157:1 ] + ├─[ built_in.graphql:162:1 ] │ - 157 │ ╭─▶ "Exposes a URL that specifies the behavior of this scalar." + 162 │ ╭─▶ "Exposes a URL that specifies the behavior of this scalar." ┆ ┆ - 161 │ ├─▶ ) on SCALAR + 166 │ ├─▶ ) on SCALAR │ │ │ ╰───────────────── directive defined here │ @@ -262,11 +262,11 @@ Error: include directive is not supported for SCHEMA location │ ─────────┬──────── │ ╰────────── directive cannot be used on SCHEMA │ - ├─[ built_in.graphql:143:1 ] + ├─[ built_in.graphql:145:1 ] │ - 143 │ ╭─▶ "Directs the executor to include this field or fragment only when the `if` argument is true." + 145 │ ╭─▶ "Directs the executor to include this field or fragment only when the `if` argument is true." ┆ ┆ - 147 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + 149 │ ├─▶ ) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT │ │ │ ╰──────────────────────────────────────────────────── directive defined here │ diff --git a/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt b/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt index 927e56ba2..8fc5710a5 100644 --- a/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt +++ b/crates/apollo-compiler/test_data/diagnostics/0054_argument_not_provided.txt @@ -44,10 +44,10 @@ Error: the required argument `@skip(if:)` is not provided │ ──┬── │ ╰──── missing value for argument `if` │ - ├─[ built_in.graphql:139:3 ] + ├─[ built_in.graphql:141:3 ] │ - 139 │ ╭─▶ "Skipped when true." - 140 │ ├─▶ if: Boolean! + 141 │ ╭─▶ "Skipped when true." + 142 │ ├─▶ if: Boolean! │ │ │ ╰──────────────────── argument defined here ─────╯ @@ -58,10 +58,10 @@ Error: the required argument `@include(if:)` is not provided │ ───────────┬────────── │ ╰──────────── missing value for argument `if` │ - ├─[ built_in.graphql:145:3 ] + ├─[ built_in.graphql:147:3 ] │ - 145 │ ╭─▶ "Included when true." - 146 │ ├─▶ if: Boolean! + 147 │ ╭─▶ "Included when true." + 148 │ ├─▶ if: Boolean! │ │ │ ╰──────────────────── argument defined here ─────╯ diff --git a/crates/apollo-compiler/test_data/diagnostics/0087_fragment_type_condition_on_composite_types.txt b/crates/apollo-compiler/test_data/diagnostics/0087_fragment_type_condition_on_composite_types.txt index f3d6c44bd..631386c15 100644 --- a/crates/apollo-compiler/test_data/diagnostics/0087_fragment_type_condition_on_composite_types.txt +++ b/crates/apollo-compiler/test_data/diagnostics/0087_fragment_type_condition_on_composite_types.txt @@ -25,9 +25,9 @@ Error: type `Int` does not have a field `name` │ ──┬─ │ ╰─── field `name` selected here │ - ├─[ built_in.graphql:166:8 ] + ├─[ built_in.graphql:171:8 ] │ - 166 │ scalar Int + 171 │ scalar Int │ ─┬─ │ ╰─── type `Int` defined here │ @@ -51,9 +51,9 @@ Error: type `Int` does not have a field `name` │ ──┬─ │ ╰─── field `name` selected here │ - ├─[ built_in.graphql:166:8 ] + ├─[ built_in.graphql:171:8 ] │ - 166 │ scalar Int + 171 │ scalar Int │ ─┬─ │ ╰─── type `Int` defined here │ @@ -77,9 +77,9 @@ Error: type `Int` does not have a field `name` │ ──┬─ │ ╰─── field `name` selected here │ - ├─[ built_in.graphql:166:8 ] + ├─[ built_in.graphql:171:8 ] │ - 166 │ scalar Int + 171 │ scalar Int │ ─┬─ │ ╰─── type `Int` defined here │ diff --git a/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt b/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt index 1a3b6b5f3..a48986aa9 100644 --- a/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt +++ b/crates/apollo-compiler/test_data/diagnostics/0102_invalid_string_values.txt @@ -499,9 +499,9 @@ Error: expected value of type Boolean!, found a string │ ──┬── │ ╰──── provided value is a string │ - ├─[ built_in.graphql:146:7 ] + ├─[ built_in.graphql:148:7 ] │ - 146 │ if: Boolean! + 148 │ if: Boolean! │ ────┬─── │ ╰───── expected type declared here as Boolean! ─────╯ @@ -512,9 +512,9 @@ Error: expected value of type Boolean!, found an enum │ ──┬─ │ ╰─── provided value is an enum │ - ├─[ built_in.graphql:140:7 ] + ├─[ built_in.graphql:142:7 ] │ - 140 │ if: Boolean! + 142 │ if: Boolean! │ ────┬─── │ ╰───── expected type declared here as Boolean! ─────╯ diff --git a/crates/apollo-compiler/test_data/introspection/response_full.json b/crates/apollo-compiler/test_data/introspection/response_full.json index a738fa35d..6fafea19b 100644 --- a/crates/apollo-compiler/test_data/introspection/response_full.json +++ b/crates/apollo-compiler/test_data/introspection/response_full.json @@ -325,6 +325,22 @@ }, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "isOneOf", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null } ], "inputFields": null, @@ -1281,6 +1297,14 @@ } ] }, + { + "name": "oneOf", + "description": "Indicates an Input Object is a OneOf Input Object.", + "locations": [ + "INPUT_OBJECT" + ], + "args": [] + }, { "name": "specifiedBy", "description": "Exposes a URL that specifies the behavior of this scalar.", diff --git a/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt b/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt index bff4e50a5..6ffe7dcce 100644 --- a/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt +++ b/crates/apollo-compiler/test_data/ok/0001_annonymous_operation_definition.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt b/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt index 87d082ac8..4b16b9d8c 100644 --- a/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt +++ b/crates/apollo-compiler/test_data/ok/0002_multiple_named_operation_definitions.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt b/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt index 159fe7f15..1212bcfb9 100644 --- a/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt +++ b/crates/apollo-compiler/test_data/ok/0003_schema_definition_with_custom_operation_types.txt @@ -35,6 +35,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt index 94f6bc506..c30ea3d92 100644 --- a/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt +++ b/crates/apollo-compiler/test_data/ok/0004_schema_with_custom_scalars.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt b/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt index 830517a5a..bec5cbfc5 100644 --- a/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt +++ b/crates/apollo-compiler/test_data/ok/0005_schema_with_valid_enum_definitions.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt index cf9a54466..cac518d72 100644 --- a/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt +++ b/crates/apollo-compiler/test_data/ok/0006_schema_with_valid_union.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt b/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt index a873699f0..caf2b33ce 100644 --- a/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt +++ b/crates/apollo-compiler/test_data/ok/0007_schema_with_interface_definition.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt index 41ef98a16..369a2440c 100644 --- a/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt +++ b/crates/apollo-compiler/test_data/ok/0008_schema_with_directive_definition.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "delegateField": 37..109 @10 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt index 9fa284cac..fd4c4ffbf 100644 --- a/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt +++ b/crates/apollo-compiler/test_data/ok/0009_schema_with_input_object.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt b/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt index 499976190..ae8a0f4c7 100644 --- a/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt +++ b/crates/apollo-compiler/test_data/ok/0010_operation_with_defined_fields.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "join__field": 315..377 @12 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt b/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt index 03e1f4085..5d41ef532 100644 --- a/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt +++ b/crates/apollo-compiler/test_data/ok/0011_fragment_spreads_in_fragment_definitions.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt b/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt index 3baa1f761..97231383e 100644 --- a/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt +++ b/crates/apollo-compiler/test_data/ok/0012_introspection_query.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { @@ -296,7 +297,7 @@ ExecutableDocument { selections: [ Field( 178..182 @14 Field { - definition: 4183..4196 @1 FieldDefinition { + definition: 4265..4278 @1 FieldDefinition { description: None, name: "name", arguments: [], @@ -317,7 +318,7 @@ ExecutableDocument { ), Field( 189..200 @14 Field { - definition: 4199..4218 @1 FieldDefinition { + definition: 4281..4300 @1 FieldDefinition { description: None, name: "description", arguments: [], @@ -338,7 +339,7 @@ ExecutableDocument { ), Field( 207..216 @14 Field { - definition: 4221..4255 @1 FieldDefinition { + definition: 4303..4337 @1 FieldDefinition { description: None, name: "locations", arguments: [], @@ -361,18 +362,18 @@ ExecutableDocument { ), Field( 223..259 @14 Field { - definition: 4258..4316 @1 FieldDefinition { + definition: 4340..4398 @1 FieldDefinition { description: None, name: "args", arguments: [ - 4263..4297 @1 InputValueDefinition { + 4345..4379 @1 InputValueDefinition { description: None, name: "includeDeprecated", - ty: 4282..4289 @1 Named( + ty: 4364..4371 @1 Named( "Boolean", ), default_value: Some( - 4292..4297 @1 Boolean( + 4374..4379 @1 Boolean( false, ), ), @@ -529,7 +530,7 @@ ExecutableDocument { selections: [ Field( 370..374 @14 Field { - definition: 2906..2919 @1 FieldDefinition { + definition: 2988..3001 @1 FieldDefinition { description: None, name: "name", arguments: [], @@ -550,7 +551,7 @@ ExecutableDocument { ), Field( 379..390 @14 Field { - definition: 2922..2941 @1 FieldDefinition { + definition: 3004..3023 @1 FieldDefinition { description: None, name: "description", arguments: [], @@ -571,18 +572,18 @@ ExecutableDocument { ), Field( 395..427 @14 Field { - definition: 2944..3002 @1 FieldDefinition { + definition: 3026..3084 @1 FieldDefinition { description: None, name: "args", arguments: [ - 2949..2983 @1 InputValueDefinition { + 3031..3065 @1 InputValueDefinition { description: None, name: "includeDeprecated", - ty: 2968..2975 @1 Named( + ty: 3050..3057 @1 Named( "Boolean", ), default_value: Some( - 2978..2983 @1 Boolean( + 3060..3065 @1 Boolean( false, ), ), @@ -615,7 +616,7 @@ ExecutableDocument { ), Field( 432..461 @14 Field { - definition: 3005..3018 @1 FieldDefinition { + definition: 3087..3100 @1 FieldDefinition { description: None, name: "type", arguments: [], @@ -643,7 +644,7 @@ ExecutableDocument { ), Field( 466..478 @14 Field { - definition: 3021..3043 @1 FieldDefinition { + definition: 3103..3125 @1 FieldDefinition { description: None, name: "isDeprecated", arguments: [], @@ -664,7 +665,7 @@ ExecutableDocument { ), Field( 483..500 @14 Field { - definition: 3046..3071 @1 FieldDefinition { + definition: 3128..3153 @1 FieldDefinition { description: None, name: "deprecationReason", arguments: [], @@ -804,7 +805,7 @@ ExecutableDocument { selections: [ Field( 621..625 @14 Field { - definition: 3692..3705 @1 FieldDefinition { + definition: 3774..3787 @1 FieldDefinition { description: None, name: "name", arguments: [], @@ -825,7 +826,7 @@ ExecutableDocument { ), Field( 630..641 @14 Field { - definition: 3708..3727 @1 FieldDefinition { + definition: 3790..3809 @1 FieldDefinition { description: None, name: "description", arguments: [], @@ -846,7 +847,7 @@ ExecutableDocument { ), Field( 646..658 @14 Field { - definition: 3730..3752 @1 FieldDefinition { + definition: 3812..3834 @1 FieldDefinition { description: None, name: "isDeprecated", arguments: [], @@ -867,7 +868,7 @@ ExecutableDocument { ), Field( 663..680 @14 Field { - definition: 3755..3780 @1 FieldDefinition { + definition: 3837..3862 @1 FieldDefinition { description: None, name: "deprecationReason", arguments: [], @@ -931,7 +932,7 @@ ExecutableDocument { selections: [ Field( 764..768 @14 Field { - definition: 3271..3284 @1 FieldDefinition { + definition: 3353..3366 @1 FieldDefinition { description: None, name: "name", arguments: [], @@ -952,7 +953,7 @@ ExecutableDocument { ), Field( 771..782 @14 Field { - definition: 3287..3306 @1 FieldDefinition { + definition: 3369..3388 @1 FieldDefinition { description: None, name: "description", arguments: [], @@ -973,7 +974,7 @@ ExecutableDocument { ), Field( 785..804 @14 Field { - definition: 3309..3322 @1 FieldDefinition { + definition: 3391..3404 @1 FieldDefinition { description: None, name: "type", arguments: [], @@ -1001,9 +1002,9 @@ ExecutableDocument { ), Field( 807..819 @14 Field { - definition: 3325..3429 @1 FieldDefinition { + definition: 3407..3511 @1 FieldDefinition { description: Some( - 3325..3406 @1 "A GraphQL-formatted string representing the default value for this input value.", + 3407..3488 @1 "A GraphQL-formatted string representing the default value for this input value.", ), name: "defaultValue", arguments: [], diff --git a/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt b/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt index 4dd78b27d..76ee06db6 100644 --- a/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt +++ b/crates/apollo-compiler/test_data/ok/0013_operation_with_used_variable_in_fragment.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0014_float_values.txt b/crates/apollo-compiler/test_data/ok/0014_float_values.txt index beb7a1792..5568bb7ff 100644 --- a/crates/apollo-compiler/test_data/ok/0014_float_values.txt +++ b/crates/apollo-compiler/test_data/ok/0014_float_values.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0015_supergraph.txt b/crates/apollo-compiler/test_data/ok/0015_supergraph.txt index 408fd4f70..7fcd3dba6 100644 --- a/crates/apollo-compiler/test_data/ok/0015_supergraph.txt +++ b/crates/apollo-compiler/test_data/ok/0015_supergraph.txt @@ -59,6 +59,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "core": 155..209 @17 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt b/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt index 960cc6e56..20d8df11d 100644 --- a/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt +++ b/crates/apollo-compiler/test_data/ok/0016_same_variables_in_multiple_operations.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt b/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt index 4f4951906..05aa212e3 100644 --- a/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt +++ b/crates/apollo-compiler/test_data/ok/0017_variables_are_input_types.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt b/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt index 695bb79b7..3617f3f47 100644 --- a/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt +++ b/crates/apollo-compiler/test_data/ok/0018_non_clashing_names.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "A": 124..146 @20 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0019_extensions.txt b/crates/apollo-compiler/test_data/ok/0019_extensions.txt index f5bf3dd84..3de707a15 100644 --- a/crates/apollo-compiler/test_data/ok/0019_extensions.txt +++ b/crates/apollo-compiler/test_data/ok/0019_extensions.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt index 8c76ec718..66423b109 100644 --- a/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt +++ b/crates/apollo-compiler/test_data/ok/0020_merge_identical_fields.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt index 71c523bdc..8cb08bef5 100644 --- a/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt +++ b/crates/apollo-compiler/test_data/ok/0021_merge_identical_fields_with_arguments.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt index 3be41e667..abd7c9cd5 100644 --- a/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt +++ b/crates/apollo-compiler/test_data/ok/0022_merge_differing_fields_and_args.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt b/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt index aafcc5fcb..6e160fb96 100644 --- a/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt +++ b/crates/apollo-compiler/test_data/ok/0024_used_variables_in_directives.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt b/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt index f651a2d0c..c5b3c2895 100644 --- a/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt +++ b/crates/apollo-compiler/test_data/ok/0025_unique_directives.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "repeatable": 0..41 @26 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt b/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt index a4e1edf06..a62cb19cf 100644 --- a/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt +++ b/crates/apollo-compiler/test_data/ok/0026_type_introspection.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { @@ -221,7 +222,7 @@ ExecutableDocument { selections: [ Field( 163..167 @27 Field { - definition: 2906..2919 @1 FieldDefinition { + definition: 2988..3001 @1 FieldDefinition { description: None, name: "name", arguments: [], @@ -242,7 +243,7 @@ ExecutableDocument { ), Field( 174..201 @27 Field { - definition: 3005..3018 @1 FieldDefinition { + definition: 3087..3100 @1 FieldDefinition { description: None, name: "type", arguments: [], diff --git a/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt b/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt index 20f5211d1..b8c31c9d2 100644 --- a/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt +++ b/crates/apollo-compiler/test_data/ok/0027_typename_introspection_in_object.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt b/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt index c8331cb1c..10e3d80f5 100644 --- a/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt +++ b/crates/apollo-compiler/test_data/ok/0028_typename_introspection_in_union.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt b/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt index 7d57527f3..a8d8392e3 100644 --- a/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt +++ b/crates/apollo-compiler/test_data/ok/0029_used_variable_in_list_and_input.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt b/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt index 07ef7fb39..6097dd3ec 100644 --- a/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt +++ b/crates/apollo-compiler/test_data/ok/0030_cyclical_nullable_input_objects.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt b/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt index 6847ae9d9..19162242e 100644 --- a/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt +++ b/crates/apollo-compiler/test_data/ok/0031_fragment_spread_possible.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt b/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt index 9f3c46567..ed05425b0 100644 --- a/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt +++ b/crates/apollo-compiler/test_data/ok/0032_valid_of_correct_type.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt b/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt index b4027ad2a..2d6b72845 100644 --- a/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt +++ b/crates/apollo-compiler/test_data/ok/0033_valid_variable_usage.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt b/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt index 4f11aca46..542c67029 100644 --- a/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt +++ b/crates/apollo-compiler/test_data/ok/0034_built_in_directive_redefinition.txt @@ -66,6 +66,7 @@ Schema { "SCHEMA", ], }, + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt b/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt index 729248bfa..95bd04617 100644 --- a/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt +++ b/crates/apollo-compiler/test_data/ok/0035_implicit_schema_definition_with_query_type.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt b/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt index 6ca829eb0..5a90d7f82 100644 --- a/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt +++ b/crates/apollo-compiler/test_data/ok/0036_implicit_schema_definition_with_several_default_types.txt @@ -30,6 +30,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt b/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt index 530156fd2..0fef2a44a 100644 --- a/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt +++ b/crates/apollo-compiler/test_data/ok/0037_implicit_schema_extension_with_directive.txt @@ -39,6 +39,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "dir": 52..76 @38 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0038_argument_default.txt b/crates/apollo-compiler/test_data/ok/0038_argument_default.txt index c1cc70b94..317c4c605 100644 --- a/crates/apollo-compiler/test_data/ok/0038_argument_default.txt +++ b/crates/apollo-compiler/test_data/ok/0038_argument_default.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "defer": 0..94 @39 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0039_string_literals.txt b/crates/apollo-compiler/test_data/ok/0039_string_literals.txt index 7d2c99882..7fbd98432 100644 --- a/crates/apollo-compiler/test_data/ok/0039_string_literals.txt +++ b/crates/apollo-compiler/test_data/ok/0039_string_literals.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt b/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt index 78526ef57..24cdcb9b3 100644 --- a/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt +++ b/crates/apollo-compiler/test_data/ok/0040_field_merging_issue_755.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0041_unquoted_string_for_custom_scalar.txt b/crates/apollo-compiler/test_data/ok/0041_unquoted_string_for_custom_scalar.txt index 2a31b08f8..33d8ca4ce 100644 --- a/crates/apollo-compiler/test_data/ok/0041_unquoted_string_for_custom_scalar.txt +++ b/crates/apollo-compiler/test_data/ok/0041_unquoted_string_for_custom_scalar.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0042_used_variable_in_operation_directive.txt b/crates/apollo-compiler/test_data/ok/0042_used_variable_in_operation_directive.txt index 345d516d6..67cc757b9 100644 --- a/crates/apollo-compiler/test_data/ok/0042_used_variable_in_operation_directive.txt +++ b/crates/apollo-compiler/test_data/ok/0042_used_variable_in_operation_directive.txt @@ -35,6 +35,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "x": 0..56 @43 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0115_interface_definition_with_extension_defines_field.txt b/crates/apollo-compiler/test_data/ok/0115_interface_definition_with_extension_defines_field.txt index 651b8c703..d0f4b7fb3 100644 --- a/crates/apollo-compiler/test_data/ok/0115_interface_definition_with_extension_defines_field.txt +++ b/crates/apollo-compiler/test_data/ok/0115_interface_definition_with_extension_defines_field.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0116_interface_without_implementations.txt b/crates/apollo-compiler/test_data/ok/0116_interface_without_implementations.txt index 7336d7d8c..742f2d0e1 100644 --- a/crates/apollo-compiler/test_data/ok/0116_interface_without_implementations.txt +++ b/crates/apollo-compiler/test_data/ok/0116_interface_without_implementations.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0117_subscription_conditions_not_at_root.txt b/crates/apollo-compiler/test_data/ok/0117_subscription_conditions_not_at_root.txt index 5824191ac..c83d5d26a 100644 --- a/crates/apollo-compiler/test_data/ok/0117_subscription_conditions_not_at_root.txt +++ b/crates/apollo-compiler/test_data/ok/0117_subscription_conditions_not_at_root.txt @@ -30,6 +30,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), }, types: { diff --git a/crates/apollo-compiler/test_data/ok/0118_directive_with_nested_input_types.txt b/crates/apollo-compiler/test_data/ok/0118_directive_with_nested_input_types.txt index ac643af0c..40ef809aa 100644 --- a/crates/apollo-compiler/test_data/ok/0118_directive_with_nested_input_types.txt +++ b/crates/apollo-compiler/test_data/ok/0118_directive_with_nested_input_types.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "custom": 0..59 @47 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/test_data/ok/0119_directive_with_argument_type_collisions.txt b/crates/apollo-compiler/test_data/ok/0119_directive_with_argument_type_collisions.txt index aa297d43f..38d8050b4 100644 --- a/crates/apollo-compiler/test_data/ok/0119_directive_with_argument_type_collisions.txt +++ b/crates/apollo-compiler/test_data/ok/0119_directive_with_argument_type_collisions.txt @@ -25,6 +25,7 @@ Schema { "skip": built_in_directive!("skip"), "include": built_in_directive!("include"), "deprecated": built_in_directive!("deprecated"), + "oneOf": built_in_directive!("oneOf"), "specifiedBy": built_in_directive!("specifiedBy"), "custom": 0..54 @48 DirectiveDefinition { description: None, diff --git a/crates/apollo-compiler/tests/validation/field_merging.rs b/crates/apollo-compiler/tests/validation/field_merging.rs index b21bd3053..98ace3047 100644 --- a/crates/apollo-compiler/tests/validation/field_merging.rs +++ b/crates/apollo-compiler/tests/validation/field_merging.rs @@ -76,8 +76,7 @@ const GRAPHQL_JS_TEST_SCHEMA: &str = r#" stringListField: [String] } - # TODO oneOf not supported in apollo-rs - input OneOfInput { # @oneOf + input OneOfInput @oneOf { stringField: String intField: Int } diff --git a/crates/apollo-compiler/tests/validation/ignore_builtin_redefinition.rs b/crates/apollo-compiler/tests/validation/ignore_builtin_redefinition.rs index 26a93045c..0c0b6a9f8 100644 --- a/crates/apollo-compiler/tests/validation/ignore_builtin_redefinition.rs +++ b/crates/apollo-compiler/tests/validation/ignore_builtin_redefinition.rs @@ -141,9 +141,9 @@ fn handles_built_in_type_redefinition() { .errors; let expected = expect![[r#" Error: the type `__Directive` is defined multiple times in the schema - ╭─[ built_in.graphql:87:6 ] + ╭─[ built_in.graphql:89:6 ] │ - 87 │ type __Directive { + 89 │ type __Directive { │ ─────┬───── │ ╰─────── previous definition of `__Directive` here │ @@ -186,9 +186,9 @@ fn handles_built_in_type_redefinition() { │ Help: remove or rename one of the definitions, or use `extend` ────╯ Error: the type `__TypeKind` is defined multiple times in the schema - ╭─[ built_in.graphql:38:6 ] + ╭─[ built_in.graphql:40:6 ] │ - 38 │ enum __TypeKind { + 40 │ enum __TypeKind { │ ─────┬──── │ ╰────── previous definition of `__TypeKind` here │ @@ -201,9 +201,9 @@ fn handles_built_in_type_redefinition() { │ Help: remove or rename one of the definitions, or use `extend` ────╯ Error: the type `__Field` is defined multiple times in the schema - ╭─[ built_in.graphql:58:6 ] + ╭─[ built_in.graphql:60:6 ] │ - 58 │ type __Field { + 60 │ type __Field { │ ───┬─── │ ╰───── previous definition of `__Field` here │ @@ -216,9 +216,9 @@ fn handles_built_in_type_redefinition() { │ Help: remove or rename one of the definitions, or use `extend` ────╯ Error: the type `__InputValue` is defined multiple times in the schema - ╭─[ built_in.graphql:68:6 ] + ╭─[ built_in.graphql:70:6 ] │ - 68 │ type __InputValue { + 70 │ type __InputValue { │ ──────┬───── │ ╰─────── previous definition of `__InputValue` here │ @@ -231,9 +231,9 @@ fn handles_built_in_type_redefinition() { │ Help: remove or rename one of the definitions, or use `extend` ────╯ Error: the type `__EnumValue` is defined multiple times in the schema - ╭─[ built_in.graphql:79:6 ] + ╭─[ built_in.graphql:81:6 ] │ - 79 │ type __EnumValue { + 81 │ type __EnumValue { │ ─────┬───── │ ╰─────── previous definition of `__EnumValue` here │ diff --git a/crates/apollo-compiler/tests/validation/mod.rs b/crates/apollo-compiler/tests/validation/mod.rs index e5b5ee182..601b2861e 100644 --- a/crates/apollo-compiler/tests/validation/mod.rs +++ b/crates/apollo-compiler/tests/validation/mod.rs @@ -2,6 +2,7 @@ mod field_merging; mod ignore_builtin_redefinition; mod interface; mod object; +mod one_of; mod operation; mod recursion; mod types; diff --git a/crates/apollo-compiler/tests/validation/one_of.rs b/crates/apollo-compiler/tests/validation/one_of.rs new file mode 100644 index 000000000..68f54b1c9 --- /dev/null +++ b/crates/apollo-compiler/tests/validation/one_of.rs @@ -0,0 +1,562 @@ +//! Validation tests for `@oneOf` input objects. +//! +//! Covers all rules in GraphQL spec §3.10.1 (OneOf Input Objects) and the corresponding +//! executable-document rules in §5.6.3 (Input Object Field Values) and the variable-usage +//! rule that treats a @oneOf field position as non-null. +//! +//! Test parity target: graphql-js `ValuesOfCorrectTypeRule-test.ts` (oneOf section) +//! and `type-system/definition-test.ts` (oneOf section). +//! +//! Spec reference: +use apollo_compiler::introspection; +use apollo_compiler::request::coerce_variable_values; +use apollo_compiler::response::JsonMap; +use apollo_compiler::validation::Valid; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Schema; +use expect_test::expect; +use std::sync::OnceLock; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn schema_with_one_of() -> &'static Valid { + static SCHEMA: OnceLock> = OnceLock::new(); + SCHEMA.get_or_init(|| { + Schema::parse_and_validate( + r#" + type Query { oneOfField(arg: OneOfInput): String } + input OneOfInput @oneOf { + stringField: String + intField: Int + } + "#, + "schema.graphql", + ) + .expect("schema should be valid") + }) +} + +// --------------------------------------------------------------------------- +// Schema-level validation — spec §3.10.1 rule 5a: fields must be nullable +// --------------------------------------------------------------------------- + +#[test] +fn valid_one_of_with_nullable_fields() { + Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { + a: String + b: Int + } + "#, + "schema.graphql", + ) + .expect("valid @oneOf schema should compile"); +} + +#[test] +fn invalid_one_of_field_non_null() { + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { + a: String! + b: Int + } + "#, + "schema.graphql", + ) + .expect_err("non-null field in @oneOf should fail") + .errors; + + let expected = expect![[r#" + Error: `Foo.a` field of a @oneOf input object must be nullable + ╭─[ schema.graphql:4:13 ] + │ + 4 │ a: String! + │ ─────┬──── + │ ╰────── field `Foo.a` defined here + │ │ + │ ╰────── remove the `!` to make this field nullable + │ + │ Help: Fields of a @oneOf input object must all be nullable and must not have default values. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn invalid_one_of_multiple_non_null_fields() { + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { + a: String! + b: Int! + } + "#, + "schema.graphql", + ) + .expect_err("multiple non-null fields in @oneOf should fail") + .errors + .to_string(); + + assert!( + errors.contains("field of a @oneOf input object must be nullable"), + "unexpected errors: {errors}" + ); +} + +// --------------------------------------------------------------------------- +// Schema-level validation — spec §3.10.1 rule 5b: fields must not have defaults +// --------------------------------------------------------------------------- + +/// Mirrors graphql-js: "rejects fields with default values" +#[test] +fn invalid_one_of_field_has_default_value() { + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { + a: String = "hello" + b: Int + } + "#, + "schema.graphql", + ) + .expect_err("default-value field in @oneOf should fail") + .errors; + + let expected = expect![[r#" + Error: `Foo.a` field of a @oneOf input object must not have a default value + ╭─[ schema.graphql:4:13 ] + │ + 4 │ a: String = "hello" + │ ─────────┬─────┬─── + │ ╰─────────── remove the default value + │ │ + │ ╰───── default value for `Foo.a` defined here + │ + │ Help: Fields of a @oneOf input object must all be nullable and must not have default values. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn invalid_one_of_non_null_field_with_default() { + // Both rules (non-null AND default) fire independently. + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { + a: String! = "bad" + b: Int + } + "#, + "schema.graphql", + ) + .expect_err("non-null field with default in @oneOf should fail") + .errors + .to_string(); + + assert!( + errors.contains("field of a @oneOf input object must be nullable"), + "should flag non-null: {errors}" + ); + assert!( + errors.contains("must not have a default value"), + "should flag default: {errors}" + ); +} + +// --------------------------------------------------------------------------- +// Value coercion — valid cases (spec §5.6.3) +// --------------------------------------------------------------------------- + +#[test] +fn valid_one_of_single_string_field() { + let schema = schema_with_one_of(); + ExecutableDocument::parse_and_validate( + schema, + r#"{ oneOfField(arg: { stringField: "hello" }) }"#, + "query.graphql", + ) + .expect("exactly one non-null field should be valid"); +} + +#[test] +fn valid_one_of_single_int_field() { + let schema = schema_with_one_of(); + ExecutableDocument::parse_and_validate( + schema, + r#"{ oneOfField(arg: { intField: 42 }) }"#, + "query.graphql", + ) + .expect("exactly one non-null field should be valid"); +} + +// --------------------------------------------------------------------------- +// Value coercion — invalid cases (spec §5.6.3) +// --------------------------------------------------------------------------- + +#[test] +fn invalid_one_of_no_fields() { + let schema = schema_with_one_of(); + let errors = ExecutableDocument::parse_and_validate( + schema, + r#"{ oneOfField(arg: {}) }"#, + "query.graphql", + ) + .expect_err("zero fields should fail") + .errors; + + let expected = expect![[r#" + Error: @oneOf input object `OneOfInput` must specify exactly one key, but 0 were given + ╭─[ query.graphql:1:19 ] + │ + 1 │ { oneOfField(arg: {}) } + │ ─┬ + │ ╰── 0 fields were provided + │ + │ Help: @oneOf input object `OneOfInput` requires exactly one non-null field. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn invalid_one_of_multiple_fields() { + let schema = schema_with_one_of(); + let errors = ExecutableDocument::parse_and_validate( + schema, + r#"{ oneOfField(arg: { stringField: "a", intField: 1 }) }"#, + "query.graphql", + ) + .expect_err("two fields should fail") + .errors; + + let expected = expect![[r#" + Error: @oneOf input object `OneOfInput` must specify exactly one key, but 2 were given + ╭─[ query.graphql:1:19 ] + │ + 1 │ { oneOfField(arg: { stringField: "a", intField: 1 }) } + │ ────────────────┬──────────────── + │ ╰────────────────── 2 fields were provided + │ + │ Help: @oneOf input object `OneOfInput` requires exactly one non-null field. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn invalid_one_of_null_field() { + let schema = schema_with_one_of(); + let errors = ExecutableDocument::parse_and_validate( + schema, + r#"{ oneOfField(arg: { stringField: null }) }"#, + "query.graphql", + ) + .expect_err("null field should fail") + .errors; + + let expected = expect![[r#" + Error: `OneOfInput.stringField` value for @oneOf input object must be non-null + ╭─[ query.graphql:1:34 ] + │ + 1 │ { oneOfField(arg: { stringField: null }) } + │ ──┬─ + │ ╰─── this value is null + │ + │ Help: @oneOf input object `OneOfInput` field `stringField` must be non-null. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +// --------------------------------------------------------------------------- +// Variable usage in @oneOf fields +// +// Spec: a variable used as the sole value of a @oneOf field is in a +// "non-null position" regardless of the field's declared type — so the +// variable itself must be declared non-null. +// +// Mirrors graphql-js ValuesOfCorrectTypeRule-test.ts (oneOf section): +// "Forbids one nullable variable" +// "Allows exactly one non-nullable variable" +// --------------------------------------------------------------------------- + +/// graphql-js: "Forbids one nullable variable" +#[test] +fn invalid_one_of_nullable_variable() { + let schema = schema_with_one_of(); + let errors = ExecutableDocument::parse_and_validate( + schema, + r#"query Q($var: String) { oneOfField(arg: { stringField: $var }) }"#, + "query.graphql", + ) + .expect_err("nullable variable in @oneOf field should fail") + .errors; + + let expected = expect![[r#" + Error: variable `$var` is of type `String` but must be non-nullable to be used for @oneOf input object `OneOfInput` field `stringField` + ╭─[ query.graphql:1:56 ] + │ + 1 │ query Q($var: String) { oneOfField(arg: { stringField: $var }) } + │ ──┬─ + │ ╰─── variable `$var` has type `String`, which is nullable + │ + │ Help: use `String!` to make this variable non-nullable for @oneOf input object `OneOfInput` field `stringField`. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +/// graphql-js: "Allows exactly one non-nullable variable" +#[test] +fn valid_one_of_non_null_variable() { + let schema = schema_with_one_of(); + ExecutableDocument::parse_and_validate( + schema, + r#"query Q($var: String!) { oneOfField(arg: { stringField: $var }) }"#, + "query.graphql", + ) + .expect("non-null variable in @oneOf field should be valid"); +} + +/// An undefined variable must NOT produce a @oneOf-specific error — the +/// existing UndefinedVariable rule already covers it. +#[test] +fn invalid_one_of_undefined_variable_no_oneof_error() { + let schema = schema_with_one_of(); + let errors = ExecutableDocument::parse_and_validate( + schema, + r#"{ oneOfField(arg: { stringField: $undeclared }) }"#, + "query.graphql", + ) + .expect_err("undefined variable should fail") + .errors + .to_string(); + + // The UndefinedVariable rule fires. + assert!( + errors.contains("variable `$undeclared` is not defined"), + "expected undefined-variable error, got: {errors}" + ); + // The @oneOf nullable-variable rule must NOT fire for undefined vars. + assert!( + !errors.contains("must be non-nullable"), + "@oneOf rule must not fire for undefined variable: {errors}" + ); +} + +// --------------------------------------------------------------------------- +// Introspection — isOneOf field +// +// Spec §3.10.1 mandates that `__Type.isOneOf` returns true for OneOf Input +// Objects and false for all other types. +// --------------------------------------------------------------------------- + +#[test] +fn introspection_is_one_of_true() { + let schema = schema_with_one_of(); + + let query = r#" + { + oneOfType: __type(name: "OneOfInput") { isOneOf } + regularType: __type(name: "String") { isOneOf } + } + "#; + + let document = ExecutableDocument::parse_and_validate(schema, query, "query.graphql") + .expect("introspection query should be valid"); + + let operation = document.operations.get(None).unwrap(); + let variables = coerce_variable_values(schema, operation, &JsonMap::default()).unwrap(); + let response = introspection::partial_execute( + schema, + &schema.implementers_map(), + &document, + operation, + &variables, + ) + .expect("introspection should succeed"); + + // introspection::partial_execute wraps results in {"data": {...}}. + let json = serde_json::to_value(&response).unwrap(); + let data = &json["data"]; + + // OneOfInput has @oneOf → isOneOf must be true. + assert_eq!( + data["oneOfType"]["isOneOf"], + serde_json::Value::Bool(true), + "expected isOneOf=true for @oneOf type, got: {json}" + ); + + // String is not a @oneOf type → isOneOf must be false. + assert_eq!( + data["regularType"]["isOneOf"], + serde_json::Value::Bool(false), + "expected isOneOf=false for non-@oneOf type, got: {json}" + ); +} + +/// A regular (non-@oneOf) input type must report isOneOf=false. +#[test] +fn introspection_is_one_of_false_for_regular_input() { + let schema = Schema::parse_and_validate( + r#" + type Query { f(arg: RegularInput): String } + input RegularInput { a: String b: Int } + "#, + "schema.graphql", + ) + .expect("schema should be valid"); + + let query = r#"{ __type(name: "RegularInput") { isOneOf } }"#; + let document = ExecutableDocument::parse_and_validate(&schema, query, "query.graphql") + .expect("introspection query should be valid"); + + let operation = document.operations.get(None).unwrap(); + let variables = coerce_variable_values(&schema, operation, &JsonMap::default()).unwrap(); + let response = introspection::partial_execute( + &schema, + &schema.implementers_map(), + &document, + operation, + &variables, + ) + .expect("introspection should succeed"); + + let json = serde_json::to_value(&response).unwrap(); + assert_eq!( + json["data"]["__type"]["isOneOf"], + serde_json::Value::Bool(false), + "regular input object should have isOneOf=false, got: {json}" + ); +} + +// --------------------------------------------------------------------------- +// Schema extensions — @oneOf must survive and be validated through extensions +// --------------------------------------------------------------------------- + +#[test] +fn extending_oneof_type_with_nullable_field_is_valid() { + // Adding a nullable field to a @oneOf type via extension is valid. + Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { a: String } + extend input Foo { b: Int } + "#, + "schema.graphql", + ) + .expect("extending a @oneOf type with a nullable field should be valid"); +} + +#[test] +fn extending_oneof_type_with_nonnull_field_is_invalid() { + // Adding a non-null field via extension must be rejected — the @oneOf + // constraint on the base type applies to all fields regardless of where + // they are introduced. + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { a: String } + extend input Foo { b: Int! } + "#, + "schema.graphql", + ) + .expect_err("non-null field added via extension should be invalid"); + let expected = expect![[r#" + Error: `Foo.b` field of a @oneOf input object must be nullable + ╭─[ schema.graphql:4:28 ] + │ + 4 │ extend input Foo { b: Int! } + │ ───┬─── + │ ╰───── field `Foo.b` defined here + │ │ + │ ╰───── remove the `!` to make this field nullable + │ + │ Help: Fields of a @oneOf input object must all be nullable and must not have default values. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn extending_oneof_type_with_default_value_is_invalid() { + // Adding a field with a default value via extension must also be rejected. + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo @oneOf { a: String } + extend input Foo { b: Int = 0 } + "#, + "schema.graphql", + ) + .expect_err("field with default added via extension should be invalid"); + let expected = expect![[r#" + Error: `Foo.b` field of a @oneOf input object must not have a default value + ╭─[ schema.graphql:4:28 ] + │ + 4 │ extend input Foo { b: Int = 0 } + │ ─────┬───┬ + │ ╰────── remove the default value + │ │ + │ ╰── default value for `Foo.b` defined here + │ + │ Help: Fields of a @oneOf input object must all be nullable and must not have default values. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} + +#[test] +fn adding_oneof_via_extension_with_valid_base_type_is_valid() { + // A regular input type whose fields are all nullable and have no defaults + // can have @oneOf added via extension. + Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo { a: String b: Int } + extend input Foo @oneOf + "#, + "schema.graphql", + ) + .expect("adding @oneOf via extension to a compatible input type should be valid"); +} + +#[test] +fn adding_oneof_via_extension_with_nonnull_field_in_base_is_invalid() { + // If the base type already has a non-null field, applying @oneOf via + // extension must be rejected because the merged type would violate the + // @oneOf field-nullability rule. + let errors = Schema::parse_and_validate( + r#" + type Query { f: String } + input Foo { a: String! b: Int } + extend input Foo @oneOf + "#, + "schema.graphql", + ) + .expect_err("@oneOf extension on type with non-null field should be invalid"); + let expected = expect![[r#" + Error: `Foo.a` field of a @oneOf input object must be nullable + ╭─[ schema.graphql:3:21 ] + │ + 3 │ input Foo { a: String! b: Int } + │ ─────┬──── + │ ╰────── field `Foo.a` defined here + │ │ + │ ╰────── remove the `!` to make this field nullable + │ + │ Help: Fields of a @oneOf input object must all be nullable and must not have default values. + ───╯ + "#]]; + expected.assert_eq(&errors.to_string()); +} diff --git a/crates/apollo-compiler/tests/validation/types.rs b/crates/apollo-compiler/tests/validation/types.rs index 3eb6ee6d6..942282920 100644 --- a/crates/apollo-compiler/tests/validation/types.rs +++ b/crates/apollo-compiler/tests/validation/types.rs @@ -75,8 +75,7 @@ const GRAPHQL_JS_TEST_SCHEMA: &str = r#" stringListField: [String] } - # TODO oneOf not supported in apollo-rs - input OneOfInput { # @oneOf + input OneOfInput @oneOf { stringField: String intField: Int } @@ -348,9 +347,9 @@ mod invalid_string_values { │ ┬ │ ╰── provided value is an integer │ - ├─[ schema.graphql:80:29 ] + ├─[ schema.graphql:79:29 ] │ - 80 │ stringArgField(stringArg: String): String + 79 │ stringArgField(stringArg: String): String │ ───┬── │ ╰──── expected type declared here as String ────╯ @@ -376,9 +375,9 @@ mod invalid_string_values { │ ─┬─ │ ╰─── provided value is a float │ - ├─[ schema.graphql:80:29 ] + ├─[ schema.graphql:79:29 ] │ - 80 │ stringArgField(stringArg: String): String + 79 │ stringArgField(stringArg: String): String │ ───┬── │ ╰──── expected type declared here as String ────╯ @@ -404,9 +403,9 @@ mod invalid_string_values { │ ──┬─ │ ╰─── provided value is a boolean │ - ├─[ schema.graphql:80:29 ] + ├─[ schema.graphql:79:29 ] │ - 80 │ stringArgField(stringArg: String): String + 79 │ stringArgField(stringArg: String): String │ ───┬── │ ╰──── expected type declared here as String ────╯ @@ -432,9 +431,9 @@ mod invalid_string_values { │ ─┬─ │ ╰─── provided value is an enum │ - ├─[ schema.graphql:80:29 ] + ├─[ schema.graphql:79:29 ] │ - 80 │ stringArgField(stringArg: String): String + 79 │ stringArgField(stringArg: String): String │ ───┬── │ ╰──── expected type declared here as String ────╯ @@ -465,9 +464,9 @@ mod invalid_int_values { │ ─┬─ │ ╰─── provided value is a string │ - ├─[ schema.graphql:78:23 ] + ├─[ schema.graphql:77:23 ] │ - 78 │ intArgField(intArg: Int): String + 77 │ intArgField(intArg: Int): String │ ─┬─ │ ╰─── expected type declared here as Int ────╯ @@ -515,9 +514,9 @@ mod invalid_int_values { │ ─┬─ │ ╰─── provided value is an enum │ - ├─[ schema.graphql:78:23 ] + ├─[ schema.graphql:77:23 ] │ - 78 │ intArgField(intArg: Int): String + 77 │ intArgField(intArg: Int): String │ ─┬─ │ ╰─── expected type declared here as Int ────╯ @@ -543,9 +542,9 @@ mod invalid_int_values { │ ─┬─ │ ╰─── provided value is a float │ - ├─[ schema.graphql:78:23 ] + ├─[ schema.graphql:77:23 ] │ - 78 │ intArgField(intArg: Int): String + 77 │ intArgField(intArg: Int): String │ ─┬─ │ ╰─── expected type declared here as Int ────╯ @@ -571,9 +570,9 @@ mod invalid_int_values { │ ──┬── │ ╰──── provided value is a float │ - ├─[ schema.graphql:78:23 ] + ├─[ schema.graphql:77:23 ] │ - 78 │ intArgField(intArg: Int): String + 77 │ intArgField(intArg: Int): String │ ─┬─ │ ╰─── expected type declared here as Int ────╯ @@ -604,9 +603,9 @@ mod invalid_float_values { │ ───┬─── │ ╰───── provided value is a string │ - ├─[ schema.graphql:83:27 ] + ├─[ schema.graphql:82:27 ] │ - 83 │ floatArgField(floatArg: Float): String + 82 │ floatArgField(floatArg: Float): String │ ──┬── │ ╰──── expected type declared here as Float ────╯ @@ -632,9 +631,9 @@ mod invalid_float_values { │ ──┬─ │ ╰─── provided value is a boolean │ - ├─[ schema.graphql:83:27 ] + ├─[ schema.graphql:82:27 ] │ - 83 │ floatArgField(floatArg: Float): String + 82 │ floatArgField(floatArg: Float): String │ ──┬── │ ╰──── expected type declared here as Float ────╯ @@ -660,9 +659,9 @@ mod invalid_float_values { │ ─┬─ │ ╰─── provided value is an enum │ - ├─[ schema.graphql:83:27 ] + ├─[ schema.graphql:82:27 ] │ - 83 │ floatArgField(floatArg: Float): String + 82 │ floatArgField(floatArg: Float): String │ ──┬── │ ╰──── expected type declared here as Float ────╯ @@ -693,9 +692,9 @@ mod invalid_boolean_values { │ ┬ │ ╰── provided value is an integer │ - ├─[ schema.graphql:81:31 ] + ├─[ schema.graphql:80:31 ] │ - 81 │ booleanArgField(booleanArg: Boolean): String + 80 │ booleanArgField(booleanArg: Boolean): String │ ───┬─── │ ╰───── expected type declared here as Boolean ────╯ @@ -721,9 +720,9 @@ mod invalid_boolean_values { │ ─┬─ │ ╰─── provided value is a float │ - ├─[ schema.graphql:81:31 ] + ├─[ schema.graphql:80:31 ] │ - 81 │ booleanArgField(booleanArg: Boolean): String + 80 │ booleanArgField(booleanArg: Boolean): String │ ───┬─── │ ╰───── expected type declared here as Boolean ────╯ @@ -749,9 +748,9 @@ mod invalid_boolean_values { │ ───┬── │ ╰──── provided value is a string │ - ├─[ schema.graphql:81:31 ] + ├─[ schema.graphql:80:31 ] │ - 81 │ booleanArgField(booleanArg: Boolean): String + 80 │ booleanArgField(booleanArg: Boolean): String │ ───┬─── │ ╰───── expected type declared here as Boolean ────╯ @@ -777,9 +776,9 @@ mod invalid_boolean_values { │ ──┬─ │ ╰─── provided value is an enum │ - ├─[ schema.graphql:81:31 ] + ├─[ schema.graphql:80:31 ] │ - 81 │ booleanArgField(booleanArg: Boolean): String + 80 │ booleanArgField(booleanArg: Boolean): String │ ───┬─── │ ╰───── expected type declared here as Boolean ────╯ @@ -810,9 +809,9 @@ mod invalid_id_values { │ ─┬─ │ ╰─── provided value is a float │ - ├─[ schema.graphql:84:21 ] + ├─[ schema.graphql:83:21 ] │ - 84 │ idArgField(idArg: ID): String + 83 │ idArgField(idArg: ID): String │ ─┬ │ ╰── expected type declared here as ID ────╯ @@ -838,9 +837,9 @@ mod invalid_id_values { │ ──┬─ │ ╰─── provided value is a boolean │ - ├─[ schema.graphql:84:21 ] + ├─[ schema.graphql:83:21 ] │ - 84 │ idArgField(idArg: ID): String + 83 │ idArgField(idArg: ID): String │ ─┬ │ ╰── expected type declared here as ID ────╯ @@ -866,9 +865,9 @@ mod invalid_id_values { │ ────┬──── │ ╰────── provided value is an enum │ - ├─[ schema.graphql:84:21 ] + ├─[ schema.graphql:83:21 ] │ - 84 │ idArgField(idArg: ID): String + 83 │ idArgField(idArg: ID): String │ ─┬ │ ╰── expected type declared here as ID ────╯ @@ -1132,9 +1131,9 @@ mod invalid_list_values { │ ┬ │ ╰── provided value is an integer │ - ├─[ schema.graphql:85:37 ] + ├─[ schema.graphql:84:37 ] │ - 85 │ stringListArgField(stringListArg: [String]): String + 84 │ stringListArgField(stringListArg: [String]): String │ ────┬─── │ ╰───── expected type declared here as String ────╯ @@ -1160,9 +1159,9 @@ mod invalid_list_values { │ ┬ │ ╰── provided value is an integer │ - ├─[ schema.graphql:85:37 ] + ├─[ schema.graphql:84:37 ] │ - 85 │ stringListArgField(stringListArg: [String]): String + 84 │ stringListArgField(stringListArg: [String]): String │ ────┬─── │ ╰───── expected type declared here as [String] ────╯ @@ -1327,9 +1326,9 @@ mod invalid_non_nullable_values { │ ──┬── │ ╰──── provided value is a string │ - ├─[ schema.graphql:89:34 ] + ├─[ schema.graphql:88:34 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ──┬─ │ ╰─── expected type declared here as Int! ────╯ @@ -1340,9 +1339,9 @@ mod invalid_non_nullable_values { │ ──┬── │ ╰──── provided value is a string │ - ├─[ schema.graphql:89:22 ] + ├─[ schema.graphql:88:22 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ──┬─ │ ╰─── expected type declared here as Int! ────╯ @@ -1368,9 +1367,9 @@ mod invalid_non_nullable_values { │ ────────────┬──────────── │ ╰────────────── missing value for argument `req2` │ - ├─[ schema.graphql:89:28 ] + ├─[ schema.graphql:88:28 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ─────┬──── │ ╰────── argument defined here ────╯ @@ -1381,9 +1380,9 @@ mod invalid_non_nullable_values { │ ──┬── │ ╰──── provided value is a string │ - ├─[ schema.graphql:89:22 ] + ├─[ schema.graphql:88:22 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ──┬─ │ ╰─── expected type declared here as Int! ────╯ @@ -1409,9 +1408,9 @@ mod invalid_non_nullable_values { │ ────────────┬─────────── │ ╰───────────── missing value for argument `req1` │ - ├─[ schema.graphql:89:16 ] + ├─[ schema.graphql:88:16 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ─────┬──── │ ╰────── argument defined here ────╯ @@ -1422,9 +1421,9 @@ mod invalid_non_nullable_values { │ ────────────┬─────────── │ ╰───────────── missing value for argument `req2` │ - ├─[ schema.graphql:89:28 ] + ├─[ schema.graphql:88:28 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ─────┬──── │ ╰────── argument defined here ────╯ @@ -1435,9 +1434,9 @@ mod invalid_non_nullable_values { │ ──┬─ │ ╰─── provided value is null │ - ├─[ schema.graphql:89:22 ] + ├─[ schema.graphql:88:22 ] │ - 89 │ multipleReqs(req1: Int!, req2: Int!): String + 88 │ multipleReqs(req1: Int!, req2: Int!): String │ ──┬─ │ ╰─── expected type declared here as Int! ────╯ @@ -1738,9 +1737,9 @@ mod directive_arguments { │ ──┬── │ ╰──── provided value is a string │ - ├─[ built_in.graphql:146:7 ] + ├─[ built_in.graphql:148:7 ] │ - 146 │ if: Boolean! + 148 │ if: Boolean! │ ────┬─── │ ╰───── expected type declared here as Boolean! ─────╯ @@ -1751,9 +1750,9 @@ mod directive_arguments { │ ──┬─ │ ╰─── provided value is an enum │ - ├─[ built_in.graphql:140:7 ] + ├─[ built_in.graphql:142:7 ] │ - 140 │ if: Boolean! + 142 │ if: Boolean! │ ────┬─── │ ╰───── expected type declared here as Boolean! ─────╯ diff --git a/crates/apollo-smith/CHANGELOG.md b/crates/apollo-smith/CHANGELOG.md index c9352c1ed..edb49d6bd 100644 --- a/crates/apollo-smith/CHANGELOG.md +++ b/crates/apollo-smith/CHANGELOG.md @@ -18,6 +18,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Maintenance ## Documentation --> +# [0.15.3](https://crates.io/crates/apollo-smith/0.15.3) - 2026-03-25 + +## Features + +- **Generate `@oneOf` input objects during structure-aware fuzzing.** + `DocumentBuilder::input_object_type_definition` now has a ~1-in-5 chance + of applying `@oneOf` to a generated input type. When it does, every field + is forced nullable and stripped of default values, so the generated document + always satisfies the spec invariants. A new `Ty::as_nullable` helper strips + the outermost `NonNull` wrapper from an arbitrary type. + # [0.15.2](https://crates.io/crates/apollo-smith/0.15.2) - 2025-11-10 ## Fixes diff --git a/crates/apollo-smith/Cargo.toml b/crates/apollo-smith/Cargo.toml index 866260eb5..93261b811 100644 --- a/crates/apollo-smith/Cargo.toml +++ b/crates/apollo-smith/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "apollo-smith" -version = "0.15.2" # When bumping, also update README.md +version = "0.15.3" # When bumping, also update README.md edition = "2021" authors = ["Benjamin Coenen "] license = "MIT OR Apache-2.0" diff --git a/crates/apollo-smith/src/input_object.rs b/crates/apollo-smith/src/input_object.rs index b23d39d74..219309435 100644 --- a/crates/apollo-smith/src/input_object.rs +++ b/crates/apollo-smith/src/input_object.rs @@ -141,11 +141,33 @@ impl DocumentBuilder<'_> { .unwrap_or(false) .then(|| self.description()) .transpose()?; - let fields = self.input_values_def()?; + + // Randomly apply @oneOf to this input object (~1-in-5 chance). When + // we do, enforce the spec constraints on every field: all must be + // nullable and none may carry a default value. + let is_one_of: bool = self.u.int_in_range(0..=4usize).unwrap_or(1) == 0; + let mut fields = self.input_values_def()?; + if is_one_of { + for field in &mut fields { + field.ty = field.ty.clone().into_nullable(); + field.default_value = None; + } + } + + let mut directives = self.directives(DirectiveLocation::InputObject)?; + if is_one_of { + directives.insert( + Name::new(String::from("oneOf")), + Directive { + name: Name::new(String::from("oneOf")), + arguments: Vec::new(), + }, + ); + } Ok(InputObjectTypeDef { description, - directives: self.directives(DirectiveLocation::InputObject)?, + directives, name, extend, fields, diff --git a/crates/apollo-smith/src/snapshot_tests.rs b/crates/apollo-smith/src/snapshot_tests.rs index 3f948b638..c6e1a922b 100644 --- a/crates/apollo-smith/src/snapshot_tests.rs +++ b/crates/apollo-smith/src/snapshot_tests.rs @@ -46,7 +46,7 @@ fn snapshot_tests() { A1 } - input A2 { + input A2 @oneOf { A0: A1 A1: A1 } @@ -88,7 +88,7 @@ fn snapshot_tests() { A1 } - input A2 { + input A2 @oneOf { A0: A1 A1: A1 } @@ -225,7 +225,7 @@ fn snapshot_tests() { A1 } - input A21 { + input A21 @oneOf { A0: A20 A1: A20 } @@ -364,7 +364,7 @@ fn snapshot_tests() { A1 } - input A21 { + input A21 @oneOf { A0: A20 A1: A20 } diff --git a/crates/apollo-smith/src/ty.rs b/crates/apollo-smith/src/ty.rs index fc41983e4..c182b4ca9 100644 --- a/crates/apollo-smith/src/ty.rs +++ b/crates/apollo-smith/src/ty.rs @@ -86,6 +86,14 @@ impl Ty { pub(crate) fn is_builtin(&self) -> bool { BUILTIN_SCALAR_NAMES.contains(&Ty::Named(self.name().clone())) } + + /// Strip the outermost `NonNull` wrapper, if present, making this type nullable. + pub(crate) fn into_nullable(self) -> Self { + match self { + Ty::NonNull(inner) => *inner, + other => other, + } + } } impl DocumentBuilder<'_> { diff --git a/fuzz/.gitignore b/fuzz/.gitignore index d91ce691d..77642e06e 100644 --- a/fuzz/.gitignore +++ b/fuzz/.gitignore @@ -1,2 +1,3 @@ /artifacts /corpus +/coverage diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 945004450..52f75ff66 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -58,3 +58,15 @@ name = "coordinate" path = "fuzz_targets/coordinate.rs" test = false doc = false + +[[bin]] +name = "one_of" +path = "fuzz_targets/one_of.rs" +test = false +doc = false + +[[bin]] +name = "one_of_exec" +path = "fuzz_targets/one_of_exec.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/one_of.rs b/fuzz/fuzz_targets/one_of.rs new file mode 100644 index 000000000..5151c6846 --- /dev/null +++ b/fuzz/fuzz_targets/one_of.rs @@ -0,0 +1,65 @@ +//! Structure-aware fuzz target for `@oneOf` input object invariants. +//! +//! Uses `apollo-smith` to generate *valid* GraphQL documents (schema + +//! executable) from raw bytes, then asserts that every `@oneOf` input type +//! in the resulting schema satisfies the spec invariants: +//! +//! 1. All fields are nullable. +//! 2. No field has a default value. +//! 3. `InputObjectType::is_one_of()` agrees with directive presence. +//! +//! Because the input is always a structurally valid document, the fuzzer +//! spends its budget exploring interesting schema shapes rather than +//! discarding malformed SDL — giving much better coverage of the validation +//! logic than a plain-text target would. +#![no_main] +use apollo_compiler::Schema; +use apollo_rs_fuzz::generate_valid_document; +use libfuzzer_sys::fuzz_target; +use log::debug; + +fuzz_target!(|data: &[u8]| { + let _ = env_logger::try_init(); + + let Ok(doc) = generate_valid_document(data) else { + return; + }; + debug!("{doc}"); + + let Ok(schema) = Schema::parse_and_validate(&doc, "fuzz.graphql") else { + return; + }; + + for (type_name, ty) in &schema.types { + let apollo_compiler::schema::ExtendedType::InputObject(input_obj) = ty else { + continue; + }; + + let has_directive = input_obj.directives.get("oneOf").is_some(); + let is_one_of = input_obj.is_one_of(); + + // Invariant: is_one_of() must agree with directive presence. + assert_eq!( + has_directive, is_one_of, + "is_one_of() disagrees with @oneOf directive presence for type `{type_name}`" + ); + + if is_one_of { + for (field_name, field) in &input_obj.fields { + // Invariant 1: all fields of a @oneOf type must be nullable. + assert!( + !field.ty.is_non_null(), + "@oneOf type `{type_name}` field `{field_name}` must be nullable \ + but has type `{}`", + field.ty + ); + + // Invariant 2: no field may carry a default value. + assert!( + field.default_value.is_none(), + "@oneOf type `{type_name}` field `{field_name}` must not have a default value" + ); + } + } + } +}); diff --git a/fuzz/fuzz_targets/one_of_exec.rs b/fuzz/fuzz_targets/one_of_exec.rs new file mode 100644 index 000000000..c1c3e4ea9 --- /dev/null +++ b/fuzz/fuzz_targets/one_of_exec.rs @@ -0,0 +1,108 @@ +//! Executable-document fuzz target for `@oneOf` validation. +//! +//! The schema-invariant target (`one_of.rs`) exercises schema parsing and the +//! field-level rules in `validation/input_object.rs`. That approach cannot +//! reach the document-level rules in `validation/value.rs` because a single +//! text string rarely simultaneously satisfies schema AND document validity. +//! +//! This target fixes a rich @oneOf schema and fuzzes *only* the executable +//! document, directly exercising: +//! - `validation/value.rs` — @oneOf field-count, null, and variable rules +//! - `resolvers/input_coercion.rs` — runtime coercion of @oneOf values +//! +//! Schema positions covered: +//! - @oneOf as a query field argument +//! - @oneOf inside a list argument +//! - @oneOf nested inside a regular input object +//! - @oneOf as a mutation argument +//! - @oneOf as a subscription argument +//! - @oneOf as a directive argument on a field +#![no_main] +use apollo_compiler::validation::Valid; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Schema; +use libfuzzer_sys::fuzz_target; +use log::debug; +use std::sync::OnceLock; + +const SCHEMA_SDL: &str = r#" + type Query { + search(filter: SearchFilter): String + list(items: [ListFilter]): String + nested(arg: OuterInput): String + } + type Mutation { + create(input: CreateInput): String + } + type Subscription { + events(filter: EventFilter): String + } + + "Used as a direct field argument." + input SearchFilter @oneOf { + id: ID + name: String + score: Int + } + + "Used inside a list argument — each item is @oneOf." + input ListFilter @oneOf { + exact: String + prefix: String + } + + "Nests a @oneOf inside a regular input." + input OuterInput { + filter: SearchFilter + page: Int + } + + "Used as a mutation argument." + input CreateInput @oneOf { + fromId: ID + fromName: String + } + + "Used as a subscription argument." + input EventFilter @oneOf { + topic: String + id: ID + } +"#; + +fn schema() -> &'static Valid { + static SCHEMA: OnceLock> = OnceLock::new(); + SCHEMA.get_or_init(|| { + Schema::parse_and_validate(SCHEMA_SDL, "schema.graphql") + .expect("hardcoded @oneOf schema must be valid") + }) +} + +fuzz_target!(|data: &str| { + let _ = env_logger::try_init(); + debug!("{data}"); + + let schema = schema(); + + // Parse and validate the fuzz input as an executable document. + // We intentionally discard validation errors — we are looking for panics + // and for coverage of the @oneOf validation code paths. + let result = ExecutableDocument::parse_and_validate(schema, data, "fuzz.graphql"); + + // For valid documents, also exercise the runtime coercion path. + // Only attempt coercion when all variables are optional (no required + // variable that the empty map would reject), so we don't hit expected + // "missing required variable" errors. + if let Ok(doc) = result { + for op in doc.operations.iter() { + let all_optional = op + .variables + .iter() + .all(|v| !v.ty.is_non_null() && v.default_value.is_none()); + if all_optional { + let empty = apollo_compiler::response::JsonMap::default(); + let _ = apollo_compiler::request::coerce_variable_values(schema, op, &empty); + } + } + } +});