Repository: apollographql/apollo-rs
Pull request #118 (2021-11-22 18:57:51)

docs: provide examples on using miette and annotate_snippets with apollo-parser
As the bulk of the diagnostics work is still to be done as part of the semantic analysis story, I am hesitant to prematurely add a `Display` implementation to errors. After all, we are not sure what sort of context will be useful from a semantics and validation point of view! Also, because `apollo-parser` is meant to be used in different environments for different use cases, it's yet to be determined whether as ascii `Display` would be beneficial to everyone.

Instead, this commit documents two different ways to display parsing errors that `apollo-parser` returns: using [`miette`](https://docs.rs/miette/3.2.0/miette/) and [`annotate_snippets`](https://docs.rs/annotate-snippets/0.9.1/annotate_snippets/). For the moment, it'll be up to the implementors to determine their visual error output.


Patch:
diff --git a/crates/apollo-encoder/Cargo.toml b/crates/apollo-encoder/Cargo.toml
index a7e77b4..893be4c 100644
--- a/crates/apollo-encoder/Cargo.toml
+++ b/crates/apollo-encoder/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 authors = ["lrlna <shestak.irina@gmail.com>"]
-edition = "2018"
+edition = "2021"
 name = "apollo-encoder"
 version = "0.1.0"
 license = "MIT OR Apache-2.0"
diff --git a/crates/apollo-parser/Cargo.toml b/crates/apollo-parser/Cargo.toml
index 35368a3..f613afd 100644
--- a/crates/apollo-parser/Cargo.toml
+++ b/crates/apollo-parser/Cargo.toml
@@ -3,7 +3,7 @@ name = "apollo-parser"
 version = "0.1.0"
 authors = ["Irina Shestak <shestak.irina@gmail.com>"]
 description = "Saving this crate name for future parser use."
-edition = "2018"
+edition = "2021"
 license = "MIT OR Apache-2.0"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -12,7 +12,10 @@ license = "MIT OR Apache-2.0"
 rowan = "0.13.0-pre.7"
 
 [dev-dependencies]
+miette = { version = "3.2.0", features = ["fancy"] }
+thiserror = "1.0.30"
 pretty_assertions = "0.7.1"
+annotate-snippets = "0.9.1"
 expect-test = "1.1"
 unindent = "0.1.7"
 criterion = "0.3.0"
diff --git a/crates/apollo-parser/examples/annotate_snippet.rs b/crates/apollo-parser/examples/annotate_snippet.rs
new file mode 100644
index 0000000..7c2e59b
--- /dev/null
+++ b/crates/apollo-parser/examples/annotate_snippet.rs
@@ -0,0 +1,64 @@
+/// This example outlines using apollo-parser with
+/// [annotate_snippet](https://docs.rs/annotate-snippets/0.9.1/annotate_snippets/)
+/// used by rustlang.
+///
+/// This allows for a lot of control over how you would like your error output
+/// to look before your print them all out.
+use std::{fs, path::Path};
+
+use annotate_snippets::{
+    display_list::{DisplayList, FormatOptions},
+    snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
+};
+use apollo_parser::{ast, Parser};
+
+fn parse_schema() -> ast::Document {
+    let file = Path::new("crates/apollo-parser/examples/schema_with_errors.graphql");
+    let src = fs::read_to_string(file).expect("Could not read schema file.");
+    // this is a nice to have for errors for displaying error origin.
+    let file_name = file
+        .file_name()
+        .expect("Could not get file name.")
+        .to_str()
+        .expect("Could not get &str from file name.");
+    let parser = Parser::new(&src);
+    let ast = parser.parse();
+
+    // each err comes with the two pieces of data you need for diagnostics:
+    // - message (err.message())
+    // - index (err.index())
+    for err in ast.errors().into_iter() {
+        let snippet = Snippet {
+            title: Some(Annotation {
+                label: Some(err.message()),
+                id: None,
+                annotation_type: AnnotationType::Error,
+            }),
+            footer: vec![],
+            slices: vec![Slice {
+                source: &src,
+                line_start: 0,
+                origin: Some(file_name),
+                fold: false,
+                annotations: vec![SourceAnnotation {
+                    label: err.message(),
+                    annotation_type: AnnotationType::Error,
+                    range: (err.index(), err.index() + err.data().len()), // (start, end) of error token
+                }],
+            }],
+            opt: FormatOptions {
+                color: true,
+                ..Default::default()
+            },
+        };
+
+        let dl = DisplayList::from(snippet);
+        println!("{}\n\n", dl);
+    }
+
+    ast.document()
+}
+
+fn main() {
+    parse_schema();
+}
diff --git a/crates/apollo-parser/examples/miette.rs b/crates/apollo-parser/examples/miette.rs
new file mode 100644
index 0000000..389762b
--- /dev/null
+++ b/crates/apollo-parser/examples/miette.rs
@@ -0,0 +1,57 @@
+/// This example describes how to use `apollo-parser` with
+/// [`miette`](https://docs.rs/miette/3.2.0/miette) diagnostic library.
+///
+///
+use std::{fs, path::Path};
+
+use apollo_parser::{ast, Parser};
+use miette::{Diagnostic, NamedSource, Report, SourceSpan};
+use thiserror::Error;
+
+// If your application is using a bunch of other thiserror errors,
+// `ApolloParserError` can live within that enum and be responsible for just
+// `apollo-parser` errors. It should work really nicely together!
+#[derive(Error, Debug, Diagnostic)]
+#[error("{}", self.ty)]
+#[diagnostic(code("apollo-parser parsing error."))]
+struct ApolloParserError {
+    ty: String,
+    #[source_code]
+    src: NamedSource,
+    #[label("{}", self.ty)]
+    span: SourceSpan,
+}
+
+fn parse_schema() -> ast::Document {
+    let file = Path::new("crates/apollo-parser/examples/schema_with_errors.graphql");
+    let src = fs::read_to_string(file).expect("Could not read schema file.");
+    // This is really useful for display the src path within the diagnostic.
+    let file_name = file
+        .file_name()
+        .expect("Could not get file name.")
+        .to_str()
+        .expect("Could not get &str from file name.");
+
+    let parser = Parser::new(&src);
+    let ast = parser.parse();
+
+    // each err comes with the two pieces of data you need for diagnostics:
+    // - message (err.message())
+    // - index (err.index())
+    for err in ast.errors() {
+        // We need to create a report and print that individually, as the error
+        // slice can have many errors.
+        let err = Report::new(ApolloParserError {
+            src: NamedSource::new(file_name, src.clone()),
+            span: (err.index(), err.data().len()).into(), // (offset, length of error token)
+            ty: err.message().into(),
+        });
+        println!("{:?}", err);
+    }
+
+    ast.document()
+}
+
+fn main() {
+    parse_schema();
+}
diff --git a/crates/apollo-parser/examples/schema.graphql b/crates/apollo-parser/examples/schema.graphql
new file mode 100644
index 0000000..73fb817
--- /dev/null
+++ b/crates/apollo-parser/examples/schema.graphql
@@ -0,0 +1,20 @@
+directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
+"Exposes a URL that specifies the behaviour of this scalar."
+directive @specifiedBy("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR
+type Query {
+    "A simple type for getting started!"
+    hello: String
+    cats(cat: [String]! = ["Nori"]): [String]!
+}
+input BooleanQueryOperatorInput {
+    eq: Boolean
+    ne: Boolean
+    in: [Boolean]
+    nin: [Boolean]
+}
+enum CacheControlScope {
+    PUBLIC
+    PRIVATE
+}
+"The `Upload` scalar type represents a file upload."
+scalar Upload
\ No newline at end of file
diff --git a/crates/apollo-parser/examples/schema_with_errors.graphql b/crates/apollo-parser/examples/schema_with_errors.graphql
new file mode 100644
index 0000000..ebb9f22
--- /dev/null
+++ b/crates/apollo-parser/examples/schema_with_errors.graphql
@@ -0,0 +1,20 @@
+directive @cacheControl(maxAge: Int, scope: CacheControlScope) on FIELD_DEFINITION | OBJECT | INTERFACE
+"Exposes a URL that specifies the behaviour of this scalar."
+directive ("The URL that specifies the behaviour of this scalar." url: String!) on SCALAR
+type Query {
+    "A simple type for getting started!"
+    hello: String
+    cats(cat: [String]! = ): [String]!
+}
+input BooleanQueryOperatorInput {
+    eq: Boolean
+    ne: Boolean
+    in: [Boolean]
+    nin: [Boolean]
+}
+enum CacheControlScope {
+    PUBLIC
+    PRIVATE
+}
+"The `Upload` scalar type represents a file upload."
+scalar Upload
\ No newline at end of file
diff --git a/crates/apollo-parser/src/error.rs b/crates/apollo-parser/src/error.rs
index 5d7d7a9..931b949 100644
--- a/crates/apollo-parser/src/error.rs
+++ b/crates/apollo-parser/src/error.rs
@@ -26,6 +26,21 @@ impl Error {
             index,
         }
     }
+
+    /// Get a reference to the error's data.
+    pub fn data(&self) -> &str {
+        self.data.as_ref()
+    }
+
+    /// Get a reference to the error's index.
+    pub fn index(&self) -> usize {
+        self.index
+    }
+
+    /// Get a reference to the error's message.
+    pub fn message(&self) -> &str {
+        self.message.as_ref()
+    }
 }
 
 impl fmt::Debug for Error {
diff --git a/crates/apollo-parser/src/parser/grammar/enum_.rs b/crates/apollo-parser/src/parser/grammar/enum_.rs
index 8cce389..2383cef 100644
--- a/crates/apollo-parser/src/parser/grammar/enum_.rs
+++ b/crates/apollo-parser/src/parser/grammar/enum_.rs
@@ -60,7 +60,7 @@ pub(crate) fn enum_type_extension(p: &mut Parser) {
     }
 
     if !meets_requirements {
-        p.err("expected Directived or Enum Values Definition");
+        p.err("expected Directive or Enum Values Definition");
     }
 }
 
diff --git a/crates/apollo-parser/src/parser/grammar/name.rs b/crates/apollo-parser/src/parser/grammar/name.rs
index 74f48f5..cc5973e 100644
--- a/crates/apollo-parser/src/parser/grammar/name.rs
+++ b/crates/apollo-parser/src/parser/grammar/name.rs
@@ -11,7 +11,7 @@ pub(crate) fn name(p: &mut Parser) {
             validate_name(p.peek_data().unwrap(), p);
             p.bump(SyntaxKind::IDENT);
         }
-        _ => p.err("expected a name"),
+        _ => p.err("expected a Name"),
     }
 }
 
diff --git a/crates/apollo-parser/src/parser/syntax_tree.rs b/crates/apollo-parser/src/parser/syntax_tree.rs
index 852aff9..2dae3fa 100644
--- a/crates/apollo-parser/src/parser/syntax_tree.rs
+++ b/crates/apollo-parser/src/parser/syntax_tree.rs
@@ -14,8 +14,8 @@ pub struct SyntaxTree {
 
 impl SyntaxTree {
     /// Get a reference to the syntax tree's errors.
-    pub fn errors(&self) -> &Vec<crate::Error> {
-        &self.errors
+    pub fn errors(&self) -> &[crate::Error] {
+        self.errors.as_ref()
     }
 
     /// Return the root typed `Document` node.
@@ -50,8 +50,6 @@ impl fmt::Debug for SyntaxTree {
             }
         }
 
-        // TODO @lrlna: needs a more elegant way of formatting Debug of Errors.
-        // Perhaps a separate struct with its own Debug implementation.
         fn print_err(f: &mut fmt::Formatter<'_>, errors: Vec<Error>) -> fmt::Result {
             for err in errors {
                 writeln!(f, "- {:?}", err)?;


Test patch:
diff --git a/crates/apollo-parser/test_data/parser/err/0006_enum_extension_with_missing_requirements.txt b/crates/apollo-parser/test_data/parser/err/0006_enum_extension_with_missing_requirements.txt
index fdbb562..db3186e 100644
--- a/crates/apollo-parser/test_data/parser/err/0006_enum_extension_with_missing_requirements.txt
+++ b/crates/apollo-parser/test_data/parser/err/0006_enum_extension_with_missing_requirements.txt
@@ -6,4 +6,4 @@
         - WHITESPACE@11..12 " "
         - NAME@12..21
             - IDENT@12..21 "Direction"
-- ERROR@21:21 "expected Directived or Enum Values Definition" EOF
+- ERROR@21:21 "expected Directive or Enum Values Definition" EOF