diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md index 15e03e5ae..7e463fc95 100644 --- a/crates/apollo-compiler/CHANGELOG.md +++ b/crates/apollo-compiler/CHANGELOG.md @@ -104,11 +104,24 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Features -- **Add `parse_and_validate` constructors for `Schema` and `ExecutableDocument` - [SimonSapin], +- **Add `parse_and_validate` constructors for `Schema` and `ExecutableDocument` - [SimonSapin], [pull/752]:** when mutating isn’t needed after parsing, this returns an immutable `Valid<_>` value in one step. -- **Serialize multi-line strings as block strings- [SimonSapin], [pull/724]** + +- **Implement serde `Serialize` and `Deserialize` for some AST types - [SimonSapin], [pull/760]:** + * `Node` + * `NodeStr` + * `Name` + * `IntValue` + * `FloatValue` + * `Value` + * `Type` + Source locations are not preserved through serialization. + +- **Add `ast::Definition::as_*() -> Option<&_>` methods for each variant - [SimonSapin], [pull/760]** + +- **Serialize (to GraphQL) multi-line strings as block strings - [SimonSapin], [pull/724]:** Example before: ```graphql "Example\n\nDescription description description" @@ -129,6 +142,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm [issue/751]: https://github.com/apollographql/apollo-rs/issues/751 [pull/724]: https://github.com/apollographql/apollo-rs/pull/724 [pull/752]: https://github.com/apollographql/apollo-rs/pull/752 +[pull/760]: https://github.com/apollographql/apollo-rs/pull/760 # [1.0.0-beta.7](https://crates.io/crates/apollo-compiler/1.0.0-beta.7) - 2023-11-17 diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs index 1cfae1446..adacb114e 100644 --- a/crates/apollo-compiler/src/ast/from_cst.rs +++ b/crates/apollo-compiler/src/ast/from_cst.rs @@ -768,7 +768,7 @@ impl Convert for cst::Name { let loc = NodeLocation::new(file_id, self.syntax()); let token = &self.syntax().first_token()?; let str = token.text(); - debug_assert!(ast::Name::is_valid(str)); + debug_assert!(ast::Name::valid_syntax(str)); Some(ast::Name(crate::NodeStr::new_parsed(str, loc))) } } diff --git a/crates/apollo-compiler/src/ast/impls.rs b/crates/apollo-compiler/src/ast/impls.rs index 845a5dbde..2140fcd41 100644 --- a/crates/apollo-compiler/src/ast/impls.rs +++ b/crates/apollo-compiler/src/ast/impls.rs @@ -263,6 +263,142 @@ impl Definition { } } + pub fn as_operation_definition(&self) -> Option<&Node> { + if let Self::OperationDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_fragment_definition(&self) -> Option<&Node> { + if let Self::FragmentDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_directive_definition(&self) -> Option<&Node> { + if let Self::DirectiveDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_schema_definition(&self) -> Option<&Node> { + if let Self::SchemaDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_scalar_type_definition(&self) -> Option<&Node> { + if let Self::ScalarTypeDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_object_type_definition(&self) -> Option<&Node> { + if let Self::ObjectTypeDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_interface_type_definition(&self) -> Option<&Node> { + if let Self::InterfaceTypeDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_union_type_definition(&self) -> Option<&Node> { + if let Self::UnionTypeDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_enum_type_definition(&self) -> Option<&Node> { + if let Self::EnumTypeDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_input_object_type_definition(&self) -> Option<&Node> { + if let Self::InputObjectTypeDefinition(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_schema_extension(&self) -> Option<&Node> { + if let Self::SchemaExtension(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_scalar_type_extension(&self) -> Option<&Node> { + if let Self::ScalarTypeExtension(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_object_type_extension(&self) -> Option<&Node> { + if let Self::ObjectTypeExtension(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_interface_type_extension(&self) -> Option<&Node> { + if let Self::InterfaceTypeExtension(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_union_type_extension(&self) -> Option<&Node> { + if let Self::UnionTypeExtension(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_enum_type_extension(&self) -> Option<&Node> { + if let Self::EnumTypeExtension(def) = self { + Some(def) + } else { + None + } + } + + pub fn as_input_object_type_extension(&self) -> Option<&Node> { + if let Self::InputObjectTypeExtension(def) = self { + Some(def) + } else { + None + } + } + serialize_method!(); } @@ -972,6 +1108,104 @@ impl fmt::Debug for FloatValue { } } +impl<'de> serde::Deserialize<'de> for IntValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + const EXPECTING: &str = "a string in GraphQL IntValue syntax"; + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = IntValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(EXPECTING) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if IntValue::valid_syntax(v) { + Ok(IntValue(v.to_owned())) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(v), &EXPECTING)) + } + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + if IntValue::valid_syntax(&v) { + Ok(IntValue(v)) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(&v), &EXPECTING)) + } + } + } + deserializer.deserialize_string(Visitor) + } +} + +impl<'de> serde::Deserialize<'de> for FloatValue { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + const EXPECTING: &str = "a string in GraphQL FloatValue syntax"; + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = FloatValue; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(EXPECTING) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if FloatValue::valid_syntax(v) { + Ok(FloatValue(v.to_owned())) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(v), &EXPECTING)) + } + } + + fn visit_string(self, v: String) -> Result + where + E: serde::de::Error, + { + if FloatValue::valid_syntax(&v) { + Ok(FloatValue(v)) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(&v), &EXPECTING)) + } + } + } + deserializer.deserialize_string(Visitor) + } +} + +impl serde::Serialize for IntValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +impl serde::Serialize for FloatValue { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + impl fmt::Display for FloatOverflowError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("value magnitude too large to be converted to `f64`") @@ -1298,7 +1532,7 @@ impl, V: Into>> From<(N, V)> for Node { /// ```compile_fail /// # use apollo_compiler::name; /// // error[E0080]: evaluation of constant value failed -/// // assertion failed: ::apollo_compiler::ast::Name::is_valid(\"è_é\") +/// // assertion failed: ::apollo_compiler::ast::Name::valid_syntax(\"è_é\") /// let invalid = name!("è_é"); /// ``` #[macro_export] @@ -1307,7 +1541,7 @@ macro_rules! name { $crate::name!(stringify!($value)) }; ($value: expr) => {{ - const _: () = { assert!($crate::ast::Name::is_valid($value)) }; + const _: () = { assert!($crate::ast::Name::valid_syntax($value)) }; $crate::ast::Name::new_unchecked($crate::NodeStr::from_static(&$value)) }}; } @@ -1316,7 +1550,7 @@ impl Name { /// Creates a new `Name` if the given value is a valid GraphQL name. pub fn new(value: impl Into) -> Result { let value = value.into(); - if Self::is_valid(&value) { + if Self::valid_syntax(&value) { Ok(Self::new_unchecked(value)) } else { Err(InvalidNameError(value)) @@ -1334,7 +1568,7 @@ impl Name { /// Returns whether the given string is a valid GraphQL name. /// /// - pub const fn is_valid(value: &str) -> bool { + pub const fn valid_syntax(value: &str) -> bool { let bytes = value.as_bytes(); let Some(&first) = bytes.first() else { return false; @@ -1373,6 +1607,44 @@ impl Name { } } +impl<'de> serde::Deserialize<'de> for Name { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + const EXPECTING: &str = "a string in GraphQL Name syntax"; + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = Name; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(EXPECTING) + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + if Name::valid_syntax(v) { + Ok(Name(v.into())) + } else { + Err(E::invalid_value(serde::de::Unexpected::Str(v), &EXPECTING)) + } + } + } + deserializer.deserialize_str(Visitor) + } +} + +impl serde::Serialize for Name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self) + } +} + impl TryFrom for Name { type Error = InvalidNameError; diff --git a/crates/apollo-compiler/src/ast/mod.rs b/crates/apollo-compiler/src/ast/mod.rs index 77f19015d..61fc2976a 100644 --- a/crates/apollo-compiler/src/ast/mod.rs +++ b/crates/apollo-compiler/src/ast/mod.rs @@ -275,7 +275,7 @@ pub struct VariableDefinition { pub directives: DirectiveList, } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub enum Type { Named(NamedType), NonNullNamed(NamedType), @@ -337,7 +337,7 @@ pub struct InlineFragment { pub selection_set: Vec, } -#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub enum Value { Null, Enum(Name), diff --git a/crates/apollo-compiler/src/node.rs b/crates/apollo-compiler/src/node.rs index 6afcf6a57..e21343662 100644 --- a/crates/apollo-compiler/src/node.rs +++ b/crates/apollo-compiler/src/node.rs @@ -27,6 +27,8 @@ use std::sync::OnceLock; /// /// `Node` cannot implement [`Borrow`][std::borrow::Borrow] because `Node as Hash` /// produces a result (the hash of the cached hash) different from `T as Hash`. +#[derive(serde::Deserialize)] +#[serde(from = "T")] pub struct Node(triomphe::Arc>); struct NodeInner { @@ -284,3 +286,12 @@ impl fmt::Debug for NodeLocation { ) } } + +impl serde::Serialize for Node { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + T::serialize(self, serializer) + } +} diff --git a/crates/apollo-compiler/src/node_str.rs b/crates/apollo-compiler/src/node_str.rs index 5d4608149..f2553b4e9 100644 --- a/crates/apollo-compiler/src/node_str.rs +++ b/crates/apollo-compiler/src/node_str.rs @@ -329,3 +329,36 @@ impl From<&'_ Self> for NodeStr { value.clone() } } + +impl serde::Serialize for NodeStr { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.as_str()) + } +} + +impl<'de> serde::Deserialize<'de> for NodeStr { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct Visitor; + impl<'de> serde::de::Visitor<'de> for Visitor { + type Value = NodeStr; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Ok(v.into()) + } + } + deserializer.deserialize_str(Visitor) + } +} diff --git a/crates/apollo-compiler/src/schema/mod.rs b/crates/apollo-compiler/src/schema/mod.rs index fe81e6261..5bb3d8355 100644 --- a/crates/apollo-compiler/src/schema/mod.rs +++ b/crates/apollo-compiler/src/schema/mod.rs @@ -411,7 +411,6 @@ impl Schema { /// of all types in the schema. /// If that is repeated for multiple interfaces, /// gathering them all at once amorticizes that cost. - #[doc(hidden)] // use the Salsa query instead pub fn implementers_map(&self) -> HashMap> { let mut map = HashMap::>::new(); for (ty_name, ty) in &self.types { diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs index b5060e10b..030fff91a 100644 --- a/crates/apollo-compiler/tests/main.rs +++ b/crates/apollo-compiler/tests/main.rs @@ -8,6 +8,7 @@ mod misc; mod node_str; mod parser; mod schema; +mod serde; mod validation; #[path = "../examples/rename.rs"] diff --git a/crates/apollo-compiler/tests/serde.rs b/crates/apollo-compiler/tests/serde.rs new file mode 100644 index 000000000..be89a9ec1 --- /dev/null +++ b/crates/apollo-compiler/tests/serde.rs @@ -0,0 +1,229 @@ +use apollo_compiler::ast; +use apollo_compiler::ty; +use expect_test::expect; + +#[test] +fn test_serde_value() { + let input = r#" + query($var: I = { + null_field: null, + enum_field: EXAMPLE, + var_field: $var, + string_field: "example" + float_field: 1.5 + int_field: 47 + list_field: [1, 2, 3] + }) { + selection + } + "#; + let value = ast::Document::parse(input, "input.graphql") + .unwrap() + .definitions[0] + .as_operation_definition() + .unwrap() + .variables[0] + .default_value + .clone() + .unwrap(); + let graphql = value.to_string(); + let expected_graphql = expect![[r#" + { + null_field: null, + enum_field: EXAMPLE, + var_field: $var, + string_field: "example", + float_field: 1.5, + int_field: 47, + list_field: [ + 1, + 2, + 3, + ], + }"#]]; + expected_graphql.assert_eq(&graphql); + let json = serde_json::to_string_pretty(&value).unwrap(); + let expected_json = expect![[r#" + { + "Object": [ + [ + "null_field", + "Null" + ], + [ + "enum_field", + { + "Enum": "EXAMPLE" + } + ], + [ + "var_field", + { + "Variable": "var" + } + ], + [ + "string_field", + { + "String": "example" + } + ], + [ + "float_field", + { + "Float": "1.5" + } + ], + [ + "int_field", + { + "Int": "47" + } + ], + [ + "list_field", + { + "List": [ + { + "Int": "1" + }, + { + "Int": "2" + }, + { + "Int": "3" + } + ] + } + ] + ] + }"#]]; + expected_json.assert_eq(&json); + let enum_value = value.as_object().unwrap()[1].1.as_enum().unwrap(); + assert!(enum_value.location().is_some()); + + let value_deserialized: ast::Value = serde_json::from_str(&json).unwrap(); + let expected_debug = expect![[r#" + Object( + [ + ( + "null_field", + Null, + ), + ( + "enum_field", + Enum( + "EXAMPLE", + ), + ), + ( + "var_field", + Variable( + "var", + ), + ), + ( + "string_field", + String( + "example", + ), + ), + ( + "float_field", + Float( + 1.5, + ), + ), + ( + "int_field", + Int( + 47, + ), + ), + ( + "list_field", + List( + [ + Int( + 1, + ), + Int( + 2, + ), + Int( + 3, + ), + ], + ), + ), + ], + ) + "#]]; + expected_debug.assert_debug_eq(&value_deserialized); + assert_eq!(*value, value_deserialized); + assert_eq!(graphql, value_deserialized.to_string()); + let enum_value = value_deserialized.as_object().unwrap()[1] + .1 + .as_enum() + .unwrap(); + // Locations are not preserved through serialization + assert!(enum_value.location().is_none()); +} + +#[test] +fn test_serde_type() { + let ty_1 = ty!(a); + let ty_2 = ty!([[a!]]!); + expect!["a"].assert_eq(&ty_1.to_string()); + expect!["[[a!]]!"].assert_eq(&ty_2.to_string()); + expect![[r#" + Named( + "a", + ) + "#]] + .assert_debug_eq(&ty_1); + expect![[r#" + NonNullList( + List( + NonNullNamed( + "a", + ), + ), + ) + "#]] + .assert_debug_eq(&ty_2); + let json_1 = serde_json::to_string(&ty_1).unwrap(); + let json_2 = serde_json::to_string(&ty_2).unwrap(); + expect![[r#"{"Named":"a"}"#]].assert_eq(&json_1); + expect![[r#"{"NonNullList":{"List":{"NonNullNamed":"a"}}}"#]].assert_eq(&json_2); + let ty_1_deserialized: ast::Type = serde_json::from_str(&json_1).unwrap(); + let ty_2_deserialized: ast::Type = serde_json::from_str(&json_2).unwrap(); + assert_eq!(ty_1, ty_1_deserialized); + assert_eq!(ty_2, ty_2_deserialized); +} + +#[test] +fn test_serde_deserialization_errors() { + #[track_caller] + fn assert_err(input: &str, expected: expect_test::Expect) { + expected.assert_eq(&serde_json::from_str::(input).err().unwrap().to_string()); + } + assert_err::( + r#""1nvalid""#, + expect![[ + r#"invalid value: string "1nvalid", expected a string in GraphQL Name syntax at line 1 column 9"# + ]], + ); + assert_err::( + r#""+3""#, + expect![[ + r#"invalid value: string "+3", expected a string in GraphQL IntValue syntax at line 1 column 4"# + ]], + ); + assert_err::( + r#""+3.5""#, + expect![[ + r#"invalid value: string "+3.5", expected a string in GraphQL FloatValue syntax at line 1 column 6"# + ]], + ); +}