Repository: apollographql/apollo-rs
Pull request #739 (2023-11-13 15:01:50)

Helper features for `Name` and `Type`

SimonSapin: CI fails on macOS :thinking: 
```
error: failed to run custom build command for `libfuzzer-sys v0.4.7`
[…]
  cargo:warning=libfuzzer/FuzzerDefs.h:14:10: fatal error: 'cassert' file not found
[…]
  error occurred: Command "c++" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-gdwarf-2" "-fno-omit-frame-pointer" "-m64" "--target=x86_64-apple-darwin" "-mmacosx-version-min=10.7" "-Wall" "-Wextra" "-std=c++17" "-fno-omit-frame-pointer" "-w" "-o" "/Users/distiller/project/target/debug/build/libfuzzer-sys-82fe81b55dd69567/out/libfuzzer/FuzzerTracePC.o" "-c" "libfuzzer/FuzzerTracePC.cpp" with args "c++" did not execute successfully (status code exit status: 1).
```
SimonSapin: This also happens on main: https://github.com/apollographql/apollo-rs/issues/740

Patch:
diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md
index ec651aa..bc433cc 100644
--- a/crates/apollo-compiler/CHANGELOG.md
+++ b/crates/apollo-compiler/CHANGELOG.md
@@ -17,6 +17,22 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 ## Maintenance
 ## Documentation-->
 
+# [x.x.x] (unreleased) - 2023-mm-dd
+
+## Features
+
+- **Helper features for `Name` and `Type` - [SimonSapin], [pull/739]:**
+  * The `name!` macro also accepts an identifier: 
+    `name!(Query)` and `name!("Query")` create equivalent `Name` values.
+  * `InvalidNameError` now contain a public `NodeStr` for the input string that is invalid,
+    and implements `Display`, `Debug`, and `Error` traits.
+  * Add `TryFrom` conversion to `Name` from `NodeStr`, `&NodeStr`, `&str`, `String`, and `&String`.
+  * Add a `ty!` macro to build a static `ast::Type` using GraphQL-like syntax.
+
+[SimonSapin]: https://github.com/SimonSapin
+[pull/739]: https://github.com/apollographql/apollo-rs/pull/739
+
+
 # [1.0.0-beta.6](https://crates.io/crates/apollo-compiler/1.0.0-beta.6) - 2023-11-10
 
 ## BREAKING
diff --git a/crates/apollo-compiler/src/ast/from_cst.rs b/crates/apollo-compiler/src/ast/from_cst.rs
index aaf5255..1c6e6cb 100644
--- a/crates/apollo-compiler/src/ast/from_cst.rs
+++ b/crates/apollo-compiler/src/ast/from_cst.rs
@@ -773,7 +773,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::check_syntax(str).is_ok());
+        debug_assert!(ast::Name::is_valid(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 113bbd7..1fbf84a 100644
--- a/crates/apollo-compiler/src/ast/impls.rs
+++ b/crates/apollo-compiler/src/ast/impls.rs
@@ -534,6 +534,33 @@ impl VariableDefinition {
     serialize_method!();
 }
 
+/// Create a static [`Type`] with GraphQL-like syntax
+///
+/// ```
+/// use apollo_compiler::ty;
+///
+/// assert_eq!(ty!(Obj).to_string(), "Obj");
+/// assert_eq!(ty!(Obj!).to_string(), "Obj!");
+/// assert_eq!(ty!([Obj]).to_string(), "[Obj]");
+/// assert_eq!(ty!([Obj]!).to_string(), "[Obj]!");
+/// assert_eq!(ty!([[[Obj ] !]]!).to_string(), "[[[Obj]!]]!");
+/// ```
+#[macro_export]
+macro_rules! ty {
+    ($name: ident) => {
+        $crate::ast::Type::Named($crate::name!($name))
+    };
+    ($name: ident !) => {
+        $crate::ast::Type::NonNullNamed($crate::name!($name))
+    };
+    ([ $($tt: tt)+ ]) => {
+        $crate::ast::Type::List(::std::boxed::Box::new($crate::ty!( $($tt)+ )))
+    };
+    ([ $($tt: tt)+ ]!) => {
+        $crate::ast::Type::NonNullList(::std::boxed::Box::new($crate::ty!( $($tt)+ )))
+    };
+}
+
 impl Type {
     /// Returns this type made non-null, if it isn’t already.
     pub fn non_null(self) -> Self {
@@ -1113,22 +1140,46 @@ impl<N: Into<Name>, V: Into<Node<Value>>> From<(N, V)> for Node<Argument> {
     }
 }
 
-/// Create a [`Name`] from a string literal, checked for validity at compile time.
+/// Create a [`Name`] from a string literal or identifier, checked for validity at compile time.
 ///
 /// A `Name` created this way does not own allocated heap memory or a reference counter,
 /// so cloning it is extremely cheap.
+///
+/// # Examples
+///
+/// ```
+/// use apollo_compiler::name;
+///
+/// assert_eq!(name!("Query").as_str(), "Query");
+/// assert_eq!(name!(Query).as_str(), "Query");
+/// ```
+///
+/// ```compile_fail
+/// # use apollo_compiler::name;
+/// // error[E0080]: evaluation of constant value failed
+/// // assertion failed: ::apollo_compiler::ast::Name::is_valid(\"è_é\")
+/// let invalid = name!("è_é");
+/// ```
 #[macro_export]
 macro_rules! name {
-    ($value: literal) => {{
-        const _: () = { assert!($crate::ast::Name::check_syntax($value).is_ok()) };
+    ($value: ident) => {
+        $crate::name!(stringify!($value))
+    };
+    ($value: expr) => {{
+        const _: () = { assert!($crate::ast::Name::is_valid($value)) };
         $crate::ast::Name::new_unchecked($crate::NodeStr::from_static(&$value))
     }};
 }
 
 impl Name {
     /// Creates a new `Name` if the given value is a valid GraphQL name.
-    pub fn new(value: &str) -> Result<Self, InvalidNameError> {
-        Self::check_syntax(value).map(|()| Self::new_unchecked(NodeStr::new(value)))
+    pub fn new(value: impl Into<NodeStr>) -> Result<Self, InvalidNameError> {
+        let value = value.into();
+        if Self::is_valid(&value) {
+            Ok(Self::new_unchecked(value))
+        } else {
+            Err(InvalidNameError(value))
+        }
     }
 
     /// Creates a new `Name` without validity checking.
@@ -1139,28 +1190,28 @@ impl Name {
         Self(value)
     }
 
-    /// Checks whether the given string is a valid GraphQL name.
+    /// Returns whether the given string is a valid GraphQL name.
     ///
     /// <https://spec.graphql.org/October2021/#Name>
-    pub const fn check_syntax(value: &str) -> Result<(), InvalidNameError> {
+    pub const fn is_valid(value: &str) -> bool {
         let bytes = value.as_bytes();
         let Some(&first) = bytes.first() else {
-            return Err(InvalidNameError {});
+            return false;
         };
         // `NameStart` and `NameContinue` are within the ASCII range,
         // so converting from `u8` to `char` here does not affect the result:
         if !Self::char_is_name_start(first as char) {
-            return Err(InvalidNameError {});
+            return false;
         }
         // TODO: iterator when available in const
         let mut i = 1;
         while i < bytes.len() {
             if !Self::char_is_name_continue(bytes[i] as char) {
-                return Err(InvalidNameError {});
+                return false;
             }
             i += 1
         }
-        Ok(())
+        true
     }
 
     /// <https://spec.graphql.org/October2021/#NameStart>
@@ -1181,6 +1232,46 @@ impl Name {
     }
 }
 
+impl TryFrom<NodeStr> for Name {
+    type Error = InvalidNameError;
+
+    fn try_from(value: NodeStr) -> Result<Self, Self::Error> {
+        Self::new(value)
+    }
+}
+
+impl TryFrom<&'_ NodeStr> for Name {
+    type Error = InvalidNameError;
+
+    fn try_from(value: &'_ NodeStr) -> Result<Self, Self::Error> {
+        Self::new(value)
+    }
+}
+
+impl TryFrom<&str> for Name {
+    type Error = InvalidNameError;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        Self::new(value)
+    }
+}
+
+impl TryFrom<String> for Name {
+    type Error = InvalidNameError;
+
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        Self::new(value)
+    }
+}
+
+impl TryFrom<&'_ String> for Name {
+    type Error = InvalidNameError;
+
+    fn try_from(value: &'_ String) -> Result<Self, Self::Error> {
+        Self::new(value)
+    }
+}
+
 impl std::ops::Deref for Name {
     type Target = NodeStr;
 
@@ -1243,6 +1334,12 @@ impl PartialOrd<&'_ str> for Name {
     }
 }
 
+impl fmt::Debug for InvalidNameError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
+
 impl<T: Extensible> TypeWithExtensions<T> {
     /// Iterate over elements of the base definition and its extensions.
     pub fn iter_all<'a, Item, DefIter, ExtIter>(
diff --git a/crates/apollo-compiler/src/ast/mod.rs b/crates/apollo-compiler/src/ast/mod.rs
index 3a61e13..b5d89c4 100644
--- a/crates/apollo-compiler/src/ast/mod.rs
+++ b/crates/apollo-compiler/src/ast/mod.rs
@@ -67,9 +67,9 @@ pub struct Name(NodeStr);
 /// Refers to the name of a GraphQL type defined elsewhere
 pub type NamedType = Name;
 
-#[derive(Clone, Eq, PartialEq)]
-#[non_exhaustive]
-pub struct InvalidNameError {}
+#[derive(Clone, Eq, PartialEq, thiserror::Error)]
+#[error("`{0}` is not a valid GraphQL name")]
+pub struct InvalidNameError(pub NodeStr);
 
 #[derive(Clone, Eq, PartialEq, Hash)]
 pub enum Definition {
diff --git a/crates/apollo-compiler/src/executable/from_ast.rs b/crates/apollo-compiler/src/executable/from_ast.rs
index c911d16..b16bf06 100644
--- a/crates/apollo-compiler/src/executable/from_ast.rs
+++ b/crates/apollo-compiler/src/executable/from_ast.rs
@@ -1,5 +1,5 @@
 use super::*;
-use crate::name;
+use crate::ty;
 
 pub(crate) struct BuildErrors {
     pub(crate) errors: Vec<BuildError>,
@@ -166,7 +166,7 @@ impl SelectionSet {
                             description: None,
                             name: ast.name.clone(),
                             arguments: Vec::new(),
-                            ty: Type::Named(name!("UNKNOWN")),
+                            ty: ty!(UNKNOWN),
                             directives: Default::default(),
                         }))
                     };
diff --git a/crates/apollo-compiler/src/schema/mod.rs b/crates/apollo-compiler/src/schema/mod.rs
index f1e45b2..740cf51 100644
--- a/crates/apollo-compiler/src/schema/mod.rs
+++ b/crates/apollo-compiler/src/schema/mod.rs
@@ -25,6 +25,7 @@ pub use crate::ast::{
     InputValueDefinition, Name, NamedType, Type, Value,
 };
 use crate::name;
+use crate::ty;
 use crate::validation::DiagnosticList;
 
 /// High-level representation of a GraphQL schema
@@ -485,7 +486,7 @@ impl Schema {
                     description: None,
                     name: name!("__typename"),
                     arguments: Vec::new(),
-                    ty: Type::Named(name!("String")).non_null(),
+                    ty: ty!(String!),
                     directives: ast::DirectiveList::new(),
                 }),
                 // __schema: __Schema!
@@ -493,7 +494,7 @@ impl Schema {
                     description: None,
                     name: name!("__schema"),
                     arguments: Vec::new(),
-                    ty: Type::Named(name!("__Schema")).non_null(),
+                    ty: ty!(__Schema!),
                     directives: ast::DirectiveList::new(),
                 }),
                 // __type(name: String!): __Type
@@ -503,12 +504,12 @@ impl Schema {
                     arguments: vec![InputValueDefinition {
                         description: None,
                         name: name!("name"),
-                        ty: ast::Type::Named(name!("String")).non_null().into(),
+                        ty: ty!(String!).into(),
                         default_value: None,
                         directives: ast::DirectiveList::new(),
                     }
                     .into()],
-                    ty: Type::Named(name!("__Type")),
+                    ty: ty!(__Type),
                     directives: ast::DirectiveList::new(),
                 }),
             ]


Test patch:
diff --git a/crates/apollo-compiler/tests/snapshot_tests.rs b/crates/apollo-compiler/tests/snapshot_tests.rs
index a310d23..6cd1270 100644
--- a/crates/apollo-compiler/tests/snapshot_tests.rs
+++ b/crates/apollo-compiler/tests/snapshot_tests.rs
@@ -10,6 +10,7 @@
 use apollo_compiler::ast;
 use apollo_compiler::name;
 use apollo_compiler::schema;
+use apollo_compiler::ty;
 use apollo_compiler::DiagnosticList;
 use apollo_compiler::FileId;
 use apollo_compiler::Schema;
@@ -266,7 +267,7 @@ fn test_invalid_synthetic_node() {
                     description: Default::default(),
                     name: name!("field"),
                     arguments: Default::default(),
-                    ty: schema::Type::Named(name!("UndefinedType")),
+                    ty: ty!(UndefinedType),
                     directives: Default::default(),
                 }
                 .into(),