libgraphql_core/operation/
executable_document_builder.rs

1use crate::ast;
2use crate::file_reader;
3use crate::operation::ExecutableDocument;
4use crate::operation::FragmentBuilder;
5use crate::operation::FragmentBuildError;
6use crate::operation::FragmentRegistry;
7use crate::operation::Operation;
8use crate::operation::OperationBuilder;
9use crate::operation::OperationBuildError;
10use crate::schema::Schema;
11use std::path::Path;
12use std::sync::Arc;
13use thiserror::Error;
14
15type Result<T> = std::result::Result<T, Vec<ExecutableDocumentBuildError>>;
16
17pub struct ExecutableDocumentBuilder<'schema: 'fragreg, 'fragreg> {
18    fragment_registry: &'fragreg FragmentRegistry<'schema>,
19    operations: Vec<Operation<'schema, 'fragreg>>,
20    schema: &'schema Schema,
21}
22
23impl<'schema, 'fragreg> ExecutableDocumentBuilder<'schema, 'fragreg> {
24    pub fn build(self) -> Result<ExecutableDocument<'schema, 'fragreg>> {
25        Ok(ExecutableDocument {
26            fragment_registry: self.fragment_registry,
27            operations: self.operations,
28            schema: self.schema,
29        })
30    }
31
32    pub fn new(
33        schema: &'schema Schema,
34        fragment_registry: &'fragreg FragmentRegistry<'schema>,
35    ) -> Self {
36        Self {
37            fragment_registry,
38            operations: vec![],
39            schema,
40        }
41    }
42
43    pub fn from_ast(
44        schema: &'schema Schema,
45        fragment_registry: &'fragreg FragmentRegistry<'schema>,
46        ast: &ast::operation::Document,
47        file_path: Option<&Path>,
48    ) -> Result<Self> {
49        let mut frag_errors = vec![];
50        let mut op_build_errors = vec![];
51        let mut errors = vec![];
52        let mut operations = vec![];
53        for def in &ast.definitions {
54            use ast::operation::Definition as Def;
55            match def {
56                Def::Fragment(frag_def) => {
57                    // Validate that the fragment in the document matches the one in the registry
58                    let fragment_name = frag_def.name.as_str();
59
60                    // Build the fragment from the document
61                    let fragment = FragmentBuilder::from_ast(
62                        schema,
63                        fragment_registry,
64                        frag_def,
65                        file_path,
66                    ).and_then(|builder| builder.build());
67                    let fragment = match fragment {
68                        Ok(fragment) => fragment,
69                        Err(err) => {
70                            frag_errors.push(err);
71                            continue;
72                        }
73                    };
74
75                    // Check if fragment exists in registry
76                    let registry_frag =
77                        fragment_registry
78                            .fragments()
79                            .get(fragment_name);
80
81                    if let Some(registry_frag) = registry_frag
82                        && &fragment != registry_frag {
83                        // Validate that fragments match exactly
84                        // For now, we do a simple comparison - in a more complete implementation,
85                        // we would compare type condition, selection set, and directives
86                        errors.push(
87                            ExecutableDocumentBuildError::FragmentDefinitionMismatch {
88                                fragment_name: fragment_name.to_string(),
89                                document_location: fragment.def_location.clone(),
90                                registry_location: registry_frag.def_location.clone(),
91                            }
92                        );
93                    } else if registry_frag.is_none() {
94                        errors.push(
95                            ExecutableDocumentBuildError::FragmentNotInRegistry {
96                                fragment_name: fragment_name.to_string(),
97                                document_location: fragment.def_location.clone(),
98                            }
99                        );
100                    }
101                },
102
103                Def::Operation(op_def) => {
104                    // NOTE: We intentionally do not enforce the following rule
105                    //       defined in the GraphQL spec stating that if a
106                    //       document contains multiple operations, all
107                    //       operations in that document must be named:
108                    //
109                    //       https://spec.graphql.org/October2021/#sel-EAFPTCAACCkEr-Y
110                    //
111                    //       This is easy to detect if/when it's relevant to a
112                    //       use-case, but it's quite limiting to enforce at
113                    //       this layer for many other use-cases
114                    //       (e.g. batch processing tools, etc).
115                    let mut maybe_op = OperationBuilder::from_ast(
116                        schema,
117                        fragment_registry,
118                        op_def,
119                        file_path,
120                    ).and_then(|op_builder| op_builder.build());
121
122                    if let Err(errs) = &mut maybe_op {
123                        op_build_errors.append(errs);
124                        continue;
125                    }
126                    operations.push(maybe_op.unwrap())
127                },
128            }
129        }
130
131        if !frag_errors.is_empty() {
132            errors.push(
133                ExecutableDocumentBuildError::FragmentValidationErrors(
134                    frag_errors
135                )
136            )
137        }
138
139        if !op_build_errors.is_empty() {
140            errors.push(
141                ExecutableDocumentBuildError::OperationBuildErrors(
142                    op_build_errors
143                )
144            );
145        }
146
147        if !errors.is_empty() {
148            return Err(errors);
149        }
150
151        Ok(Self {
152            fragment_registry,
153            schema,
154            operations,
155        })
156    }
157
158    pub fn from_file(
159        schema: &'schema Schema,
160        fragment_registry: &'fragreg FragmentRegistry<'schema>,
161        file_path: impl AsRef<Path>,
162    ) -> Result<Self> {
163        let file_path = file_path.as_ref();
164        let file_content = file_reader::read_content(file_path)
165            .map_err(|e| ExecutableDocumentBuildError::ExecutableDocumentFileReadError(
166                Box::new(e),
167            ))?;
168        Self::from_str(schema, fragment_registry, file_content, Some(file_path))
169    }
170
171    pub fn from_str(
172        schema: &'schema Schema,
173        fragment_registry: &'fragreg FragmentRegistry<'schema>,
174        content: impl AsRef<str>,
175        file_path: Option<&Path>,
176    ) -> Result<Self> {
177        let ast_doc =
178            ast::operation::parse(content.as_ref())
179                .map_err(|e| vec![e.into()])?;
180        Self::from_ast(schema, fragment_registry, &ast_doc, file_path)
181    }
182}
183
184#[derive(Clone, Debug, Error)]
185pub enum ExecutableDocumentBuildError {
186    #[error(
187        "Failure while trying to read an executable document file from disk: $0"
188    )]
189    ExecutableDocumentFileReadError(Box<file_reader::ReadContentError>),
190
191    #[error(
192        "Encountered errors while building operations within this \
193        executable document: {0:?}",
194    )]
195    OperationBuildErrors(Vec<OperationBuildError>),
196
197    #[error("Error parsing executable document: $0")]
198    ParseError(Arc<ast::operation::ParseError>),
199
200    #[error(
201        "Fragment '{fragment_name}' is defined in the document but does not match \
202        the fragment in the registry"
203    )]
204    FragmentDefinitionMismatch {
205        fragment_name: String,
206        document_location: crate::loc::SourceLocation,
207        registry_location: crate::loc::SourceLocation,
208    },
209
210    #[error("Some fragments have validation errors: {0:?}")]
211    FragmentValidationErrors(Vec<FragmentBuildError>),
212
213    #[error(
214        "Fragment '{fragment_name}' is defined in the document but does not exist \
215        in the provided FragmentRegistry"
216    )]
217    FragmentNotInRegistry {
218        fragment_name: String,
219        document_location: crate::loc::SourceLocation,
220    },
221}
222impl std::convert::From<ast::operation::ParseError> for ExecutableDocumentBuildError {
223    fn from(value: ast::operation::ParseError) -> Self {
224        Self::ParseError(Arc::new(value))
225    }
226}
227impl std::convert::From<ExecutableDocumentBuildError> for Vec<ExecutableDocumentBuildError> {
228    fn from(value: ExecutableDocumentBuildError) -> Self {
229        vec![value]
230    }
231}