Skip to main content

libgraphql_parser/
parse_result.rs

1//! Result type for parsing operations that may produce partial results.
2
3use crate::GraphQLParseError;
4
5/// The result of a parsing operation.
6///
7/// Unlike `Result<T, E>`, `ParseResult` can contain both a partial AST and
8/// errors. This enables error recovery: the parser can report multiple errors
9/// while still producing as much AST as possible.
10///
11/// # Design Rationale
12///
13/// Traditional `Result<T, E>` forces a binary choice: either success with a
14/// value, or failure with an error. But GraphQL tooling often benefits from
15/// having both:
16///
17/// - **IDE integration**: Show syntax errors while still providing completions
18///   based on the partially-parsed document
19/// - **Batch error reporting**: Report all syntax errors in one pass rather
20///   than stopping at the first error
21/// - **Graceful degradation**: Process as much of a document as possible even
22///   when parts are invalid
23///
24/// # Accessing the AST
25///
26/// Two methods are provided for accessing the AST, depending on your use case:
27///
28/// - [`valid_ast()`](Self::valid_ast) - Returns the AST only if parsing was
29///   completely successful (no errors). Use this when you need guaranteed-valid
30///   input.
31///
32/// - [`ast()`](Self::ast) - Returns the AST if present, regardless of errors.
33///   Use this for tools that want best-effort results (formatters, IDE
34///   features, linters).
35///
36/// # Example
37///
38/// ```
39/// # use libgraphql_parser::ast;
40/// # use libgraphql_parser::GraphQLParser;
41/// #
42/// # let source = "type Query { foo: String }";
43/// # let parser = GraphQLParser::new(source);
44/// #
45/// # fn analyze_schema(schema: &ast::schema::Document) { }
46/// # fn provide_ide_completions(schema: &ast::schema::Document) { }
47/// #
48/// let result = parser.parse_schema_document();
49///
50/// // Strict mode: only accept fully valid documents
51/// if let Some(doc) = result.valid_ast() {
52///     analyze_schema(doc);
53/// }
54///
55/// // Best-effort mode: work with whatever we got
56/// if let Some(doc) = result.ast() {
57///     provide_ide_completions(doc);
58/// }
59///
60/// // Report any errors
61/// if result.has_errors() {
62///     for error in &result.errors {
63///         eprintln!("{}", error.format_detailed(Some(source)));
64///     }
65/// }
66/// ```
67#[derive(Debug)]
68pub struct ParseResult<TAst> {
69    /// The parsed AST, if parsing produced any result.
70    ///
71    /// This may be `Some` even when `errors` is non-empty, representing a
72    /// recovered/partial parse result.
73    ast: Option<TAst>,
74
75    /// Errors encountered during parsing.
76    ///
77    /// Empty if parsing was completely successful.
78    pub errors: Vec<GraphQLParseError>,
79}
80
81impl<TAst> ParseResult<TAst> {
82    /// Creates a successful parse result with no errors.
83    pub(crate) fn ok(ast: TAst) -> Self {
84        Self {
85            ast: Some(ast),
86            errors: Vec::new(),
87        }
88    }
89
90    /// Creates a failed parse result with errors but no AST.
91    #[cfg(test)]
92    pub(crate) fn err(errors: Vec<GraphQLParseError>) -> Self {
93        Self { ast: None, errors }
94    }
95
96    /// Creates a recovered parse result with both AST and errors.
97    ///
98    /// The AST was produced via error recovery and may be incomplete or contain
99    /// placeholder values.
100    pub(crate) fn recovered(ast: TAst, errors: Vec<GraphQLParseError>) -> Self {
101        Self {
102            ast: Some(ast),
103            errors,
104        }
105    }
106
107    /// Returns the AST only if parsing was completely successful (no errors).
108    ///
109    /// Use this when you need guaranteed-valid input, such as when compiling
110    /// a schema or executing a query.
111    ///
112    /// Returns `None` if:
113    /// - Parsing failed entirely (no AST produced)
114    /// - Parsing succeeded but with errors (recovered AST)
115    pub fn valid_ast(&self) -> Option<&TAst> {
116        if self.errors.is_empty() {
117            self.ast.as_ref()
118        } else {
119            None
120        }
121    }
122
123    /// Returns the AST if present, regardless of whether errors occurred.
124    ///
125    /// Use this for tools that want best-effort results:
126    /// - IDE features (completions, hover info)
127    /// - Formatters (format what we can parse)
128    /// - Linters (report issues even in partially-valid documents)
129    ///
130    /// Check [`has_errors()`](Self::has_errors) to determine if the AST was
131    /// produced via error recovery.
132    pub fn ast(&self) -> Option<&TAst> {
133        self.ast.as_ref()
134    }
135
136    /// Takes ownership of the AST only if parsing was completely successful.
137    ///
138    /// This is the consuming version of [`valid_ast()`](Self::valid_ast).
139    pub fn into_valid_ast(self) -> Option<TAst> {
140        if self.errors.is_empty() {
141            self.ast
142        } else {
143            None
144        }
145    }
146
147    /// Takes ownership of the AST regardless of errors.
148    ///
149    /// This is the consuming version of [`ast()`](Self::ast).
150    pub fn into_ast(self) -> Option<TAst> {
151        self.ast
152    }
153
154    /// Returns `true` if parsing was completely successful (has AST, no
155    /// errors).
156    pub fn is_ok(&self) -> bool {
157        self.ast.is_some() && self.errors.is_empty()
158    }
159
160    /// Returns `true` if any errors were encountered during parsing.
161    pub fn has_errors(&self) -> bool {
162        !self.errors.is_empty()
163    }
164
165    /// Formats all errors as a single string for display.
166    ///
167    /// # Arguments
168    /// - `source`: Optional source text for snippet extraction in error
169    ///   messages.
170    pub fn format_errors(&self, source: Option<&str>) -> String {
171        self.errors
172            .iter()
173            .map(|e| e.format_detailed(source))
174            .collect::<Vec<_>>()
175            .join("\n")
176    }
177}
178
179impl<TAst> From<ParseResult<TAst>> for Result<TAst, Vec<GraphQLParseError>> {
180    /// Converts to a standard `Result`, treating recovered ASTs as errors.
181    ///
182    /// Returns `Ok(ast)` only if there were no errors. Otherwise returns
183    /// `Err(errors)`, even if a recovered AST was available.
184    fn from(result: ParseResult<TAst>) -> Self {
185        if result.errors.is_empty() {
186            match result.ast {
187                Some(ast) => Ok(ast),
188                None => Err(Vec::new()),
189            }
190        } else {
191            Err(result.errors)
192        }
193    }
194}