libgraphql_core/schema/
schema_builder.rs

1use crate::ast;
2use crate::file_reader;
3use crate::loc;
4use crate::operation::OperationKind;
5use crate::schema::Schema;
6use crate::schema::TypeValidationError;
7use crate::types::Directive;
8use crate::types::EnumTypeBuilder;
9use crate::types::GraphQLType;
10use crate::types::InterfaceTypeBuilder;
11use crate::types::InputObjectTypeBuilder;
12use crate::types::NamedGraphQLTypeRef;
13use crate::types::ObjectTypeBuilder;
14use crate::types::Parameter;
15use crate::types::ScalarTypeBuilder;
16use crate::types::TypesMapBuilder;
17use crate::types::UnionTypeBuilder;
18use std::collections::HashMap;
19use std::collections::HashSet;
20use std::path::Path;
21use std::path::PathBuf;
22use std::sync::OnceLock;
23use thiserror::Error;
24
25type Result<T> = std::result::Result<T, SchemaBuildError>;
26
27fn builtin_directive_names() -> &'static HashSet<&'static str> {
28    static NAMES: OnceLock<HashSet<&'static str>> = OnceLock::new();
29    NAMES.get_or_init(|| {
30        HashSet::from([
31            "skip",
32            "include",
33            "deprecated",
34            "specifiedBy",
35        ])
36    })
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub enum GraphQLOperationType {
41    Query,
42    Mutation,
43    Subscription,
44}
45
46/// Utility for building a [Schema].
47#[derive(Debug)]
48pub struct SchemaBuilder {
49    directive_defs: HashMap<String, Directive>,
50    enum_builder: EnumTypeBuilder,
51    inputobject_builder: InputObjectTypeBuilder,
52    interface_builder: InterfaceTypeBuilder,
53    query_type: Option<NamedTypeDefLocation>,
54    mutation_type: Option<NamedTypeDefLocation>,
55    object_builder: ObjectTypeBuilder,
56    scalar_builder: ScalarTypeBuilder,
57    subscription_type: Option<NamedTypeDefLocation>,
58    types_map_builder: TypesMapBuilder,
59    union_builder: UnionTypeBuilder,
60}
61impl SchemaBuilder {
62    pub fn build(mut self) -> Result<Schema> {
63        self.inject_missing_builtin_directives();
64
65        self.enum_builder.finalize(&mut self.types_map_builder)?;
66        self.inputobject_builder.finalize(&mut self.types_map_builder)?;
67        self.interface_builder.finalize(&mut self.types_map_builder)?;
68        self.object_builder.finalize(&mut self.types_map_builder)?;
69        self.scalar_builder.finalize(&mut self.types_map_builder)?;
70        self.union_builder.finalize(&mut self.types_map_builder)?;
71
72        // Fun side-quest: Check types eagerly while visiting them. When there's a possibility that
73        // a type error could be resolved (or manifested) later, track a
74        //self.check_types()?;
75        let types = self.types_map_builder.into_types_map()?;
76
77        let query_typedefloc =
78            if let Some(def) = self.query_type.take() {
79                def
80            } else {
81                match types.get("Query") {
82                    Some(GraphQLType::Object(obj_type)) => NamedTypeDefLocation {
83                        def_location: obj_type.def_location().clone(),
84                        type_name: "Query".to_string(),
85                    },
86                    _ => return Err(SchemaBuildError::NoQueryOperationTypeDefined),
87                }
88            };
89
90        let mutation_type =
91            if let Some(def) = self.mutation_type.take() {
92                Some(def)
93            } else {
94                match types.get("Mutation") {
95                    Some(GraphQLType::Object(obj_type)) => Some(NamedTypeDefLocation {
96                        def_location: obj_type.def_location().clone(),
97                        type_name: "Mutation".to_string(),
98                    }),
99                    _ => None,
100                }
101            };
102
103        let subscription_type =
104            if let Some(def) = self.subscription_type.take() {
105                Some(def)
106            } else {
107                match types.get("Subscription") {
108                    Some(GraphQLType::Object(obj_type)) => Some(NamedTypeDefLocation {
109                        def_location: obj_type.def_location().clone(),
110                        type_name: "Subscription".to_string(),
111                    }),
112                    _ => None,
113                }
114            };
115
116        Ok(Schema {
117            directive_defs: self.directive_defs,
118            query_type: NamedGraphQLTypeRef::new(
119                query_typedefloc.type_name,
120                query_typedefloc.def_location,
121            ),
122            mutation_type: mutation_type.map(|t| NamedGraphQLTypeRef::new(
123                t.type_name,
124                t.def_location,
125            )),
126            subscription_type: subscription_type.map(|t| NamedGraphQLTypeRef::new(
127                t.type_name,
128                t.def_location,
129            )),
130            types,
131        })
132    }
133
134    pub fn build_from_file(file_path: impl AsRef<Path>) -> Result<Schema> {
135        Self::from_file(file_path).and_then(|builder| builder.build())
136    }
137
138    pub fn build_from_ast(
139        file_path: Option<&Path>,
140        ast_doc: crate::ast::schema::Document,
141    ) -> Result<Schema> {
142        Self::from_ast(file_path, ast_doc).and_then(|builder| builder.build())
143    }
144
145    pub fn build_from_str(
146        file_path: Option<&Path>,
147        content: impl AsRef<str>,
148    ) -> Result<Schema> {
149        Self::from_str(file_path, content).and_then(|builder| builder.build())
150    }
151
152    pub fn from_ast(
153        file_path: Option<&Path>,
154        ast_doc: crate::ast::schema::Document,
155    ) -> Result<Self> {
156        Self::new().load_ast(file_path, ast_doc)
157    }
158
159    pub fn from_file(file_path: impl AsRef<Path>) -> Result<Self> {
160        Self::new()
161            .load_file(file_path)
162    }
163
164    pub fn from_str(
165        file_path: Option<&Path>,
166        content: impl AsRef<str>,
167    ) -> Result<Self> {
168        Self::new().load_str(file_path, content)
169    }
170
171    pub fn load_file(
172        self,
173        file_path: impl AsRef<Path>,
174    ) -> Result<Self> {
175        self.load_files(vec![file_path])
176    }
177
178    pub fn load_ast(
179        mut self,
180        file_path: Option<&Path>,
181        ast_doc: crate::ast::schema::Document,
182    ) -> Result<Self> {
183        for def in ast_doc.definitions {
184            self.visit_ast_def(file_path, def)?;
185        }
186        Ok(self)
187    }
188
189    pub fn load_files(
190        mut self,
191        file_paths: Vec<impl AsRef<Path>>,
192    ) -> Result<Self> {
193        for file_path in file_paths {
194            let file_path = file_path.as_ref();
195            let file_content = file_reader::read_content(file_path)
196                .map_err(|err| SchemaBuildError::SchemaFileReadError(
197                    Box::new(err),
198                ))?;
199            self = self.load_str(
200                Some(file_path),
201                file_content.as_str(),
202            )?;
203        }
204        Ok(self)
205    }
206
207    pub fn load_str(
208        self,
209        file_path: Option<&Path>,
210        content: impl AsRef<str>,
211    ) -> Result<Self> {
212        let ast_doc =
213            graphql_parser::schema::parse_schema::<String>(content.as_ref())
214                .map_err(|err| SchemaBuildError::ParseError {
215                    file: file_path.map(|p| p.to_path_buf()),
216                    err: err.to_string(),
217                })?.into_static();
218
219        self.load_ast(file_path, ast_doc)
220    }
221
222    pub fn new() -> Self {
223        let types_map_builder = TypesMapBuilder::new();
224
225        Self {
226            directive_defs: HashMap::new(),
227            enum_builder: EnumTypeBuilder::new(),
228            inputobject_builder: InputObjectTypeBuilder::new(),
229            interface_builder: InterfaceTypeBuilder::new(),
230            query_type: None,
231            mutation_type: None,
232            object_builder: ObjectTypeBuilder::new(),
233            scalar_builder: ScalarTypeBuilder::new(),
234            subscription_type: None,
235            types_map_builder,
236            union_builder: UnionTypeBuilder::new(),
237        }
238    }
239
240    fn inject_missing_builtin_directives(&mut self) {
241        if !self.directive_defs.contains_key("skip") {
242            self.directive_defs.insert("skip".to_string(), Directive::Skip);
243        }
244
245        if !self.directive_defs.contains_key("include") {
246            self.directive_defs.insert("include".to_string(), Directive::Include);
247        }
248
249        if !self.directive_defs.contains_key("deprecated") {
250            self.directive_defs.insert("deprecated".to_string(), Directive::Deprecated);
251        }
252
253        if !self.directive_defs.contains_key("specifiedBy") {
254            self.directive_defs.insert("specifiedBy".to_string(), Directive::SpecifiedBy);
255        }
256    }
257
258    fn visit_ast_def(
259        &mut self,
260        file_path: Option<&Path>,
261        def: ast::schema::Definition,
262    ) -> Result<()> {
263        use ast::schema::Definition;
264        match def {
265            Definition::SchemaDefinition(schema_def) =>
266                self.visit_ast_schemablock_def(file_path, schema_def),
267            Definition::TypeDefinition(type_def) =>
268                self.visit_ast_type_def(file_path, type_def),
269            Definition::TypeExtension(type_ext) =>
270                self.visit_ast_type_extension(file_path, type_ext),
271            Definition::DirectiveDefinition(directive_def) =>
272                self.visit_ast_directive_def(file_path, directive_def),
273        }
274    }
275
276    fn visit_ast_directive_def(
277        &mut self,
278        file_path: Option<&Path>,
279        def: ast::schema::DirectiveDefinition,
280    ) -> Result<()> {
281        let directivedef_srcloc = loc::SourceLocation::from_schema_ast_position(
282            file_path,
283            &def.position,
284        );
285
286        if builtin_directive_names().contains(def.name.as_str()) {
287            return Err(SchemaBuildError::RedefinitionOfBuiltinDirective {
288                directive_name: def.name,
289                location: directivedef_srcloc,
290            })?;
291        }
292
293        if def.name.starts_with("__") {
294            return Err(SchemaBuildError::InvalidDunderPrefixedDirectiveName {
295                def_location: directivedef_srcloc,
296                directive_name: def.name.to_string(),
297            });
298        }
299
300        if let Some(Directive::Custom {
301            def_location,
302            ..
303        }) = self.directive_defs.get(def.name.as_str()) {
304            return Err(SchemaBuildError::DuplicateDirectiveDefinition {
305                directive_name: def.name.clone(),
306                location1: def_location.to_owned(),
307                location2: directivedef_srcloc,
308            })?;
309        }
310
311        self.directive_defs.insert(def.name.to_string(), Directive::Custom {
312            def_location: directivedef_srcloc,
313            description: def.description.to_owned(),
314            name: def.name.to_string(),
315            params: def.arguments.iter().map(|input_val| (
316                input_val.name.to_string(),
317                Parameter::from_ast(
318                    file_path,
319                    input_val,
320                ),
321            )).collect()
322        });
323
324        Ok(())
325    }
326
327    fn visit_ast_schemablock_def(
328        &mut self,
329        file_path: Option<&Path>,
330        schema_def: ast::schema::SchemaDefinition,
331    ) -> Result<()> {
332        if let Some(type_name) = &schema_def.query {
333            let typedef_loc = NamedTypeDefLocation {
334                def_location: loc::SourceLocation::from_schema_ast_position(
335                    file_path,
336                    &schema_def.position,
337                ),
338                type_name: type_name.to_owned(),
339            };
340            if let Some(existing_typedef_loc) = &self.query_type {
341                return Err(SchemaBuildError::DuplicateOperationDefinition {
342                    operation: GraphQLOperationType::Query,
343                    location1: existing_typedef_loc.clone(),
344                    location2: typedef_loc,
345                })?;
346            }
347            self.query_type = Some(typedef_loc);
348        }
349
350        if let Some(type_name) = &schema_def.mutation {
351            let typedef_loc = NamedTypeDefLocation {
352                def_location: loc::SourceLocation::from_schema_ast_position(
353                    file_path,
354                    &schema_def.position,
355                ),
356                type_name: type_name.to_owned(),
357            };
358            if let Some(existing_typedef_loc) = &self.mutation_type {
359                return Err(SchemaBuildError::DuplicateOperationDefinition {
360                    operation: GraphQLOperationType::Mutation,
361                    location1: existing_typedef_loc.clone(),
362                    location2: typedef_loc,
363                })?;
364            }
365            self.mutation_type = Some(typedef_loc);
366        }
367
368        if let Some(type_name) = &schema_def.subscription {
369            let typedef_loc = NamedTypeDefLocation {
370                def_location: loc::SourceLocation::from_schema_ast_position(
371                    file_path,
372                    &schema_def.position,
373                ),
374                type_name: type_name.to_owned(),
375            };
376            if let Some(existing_typedef_loc) = &self.subscription_type {
377                return Err(SchemaBuildError::DuplicateOperationDefinition {
378                    operation: GraphQLOperationType::Subscription,
379                    location1: existing_typedef_loc.clone(),
380                    location2: typedef_loc,
381                })?;
382            }
383            self.subscription_type = Some(typedef_loc);
384        }
385
386        // As per spec:
387        //
388        // > The query, mutation, and subscription root types must all be
389        // > different types if provided.
390        //
391        // https://spec.graphql.org/October2021/#sel-FAHTRLCAACG0B57a
392        if let (Some(query_type), Some(mut_type)) = (&self.query_type, &self.mutation_type)
393            && query_type.type_name == mut_type.type_name {
394            // Query and Mutation operations use the same type
395            return Err(SchemaBuildError::NonUniqueOperationTypes {
396                reused_type_name: query_type.type_name.to_owned(),
397                operation1: OperationKind::Query,
398                operation1_loc: query_type.def_location.to_owned(),
399                operation2: OperationKind::Mutation,
400                operation2_loc: mut_type.def_location.to_owned(),
401            });
402        }
403
404        if let (Some(query_type), Some(sub_type)) = (&self.query_type, &self.subscription_type)
405            && query_type.type_name == sub_type.type_name {
406            // Query and Subscription operations use the same type
407            return Err(SchemaBuildError::NonUniqueOperationTypes {
408                reused_type_name: query_type.type_name.to_owned(),
409                operation1: OperationKind::Query,
410                operation1_loc: query_type.def_location.to_owned(),
411                operation2: OperationKind::Subscription,
412                operation2_loc: sub_type.def_location.to_owned(),
413            });
414        }
415
416        if let (Some(mut_type), Some(sub_type)) = (&self.mutation_type, &self.subscription_type)
417            && mut_type.type_name == sub_type.type_name {
418            // Subscription and Mutation operations use the same type
419            return Err(SchemaBuildError::NonUniqueOperationTypes {
420                reused_type_name: mut_type.type_name.to_owned(),
421                operation1: OperationKind::Mutation,
422                operation1_loc: mut_type.def_location.to_owned(),
423                operation2: OperationKind::Subscription,
424                operation2_loc: sub_type.def_location.to_owned(),
425            });
426        }
427
428        Ok(())
429    }
430
431    fn visit_ast_type_def(
432        &mut self,
433        file_path: Option<&Path>,
434        type_def: ast::schema::TypeDefinition,
435    ) -> Result<()> {
436        match type_def {
437            ast::schema::TypeDefinition::Enum(enum_def) =>
438                self.enum_builder.visit_type_def(
439                    &mut self.types_map_builder,
440                    file_path,
441                    &enum_def,
442                ),
443
444            ast::schema::TypeDefinition::InputObject(inputobj_def) =>
445                self.inputobject_builder.visit_type_def(
446                    &mut self.types_map_builder,
447                    file_path,
448                    &inputobj_def,
449                ),
450
451            ast::schema::TypeDefinition::Interface(iface_def) =>
452                self.interface_builder.visit_type_def(
453                    &mut self.types_map_builder,
454                    file_path,
455                    &iface_def,
456                ),
457
458            ast::schema::TypeDefinition::Scalar(scalar_def) =>
459                self.scalar_builder.visit_type_def(
460                    &mut self.types_map_builder,
461                    file_path,
462                    &scalar_def,
463                ),
464
465            ast::schema::TypeDefinition::Object(obj_def) =>
466                self.object_builder.visit_type_def(
467                    &mut self.types_map_builder,
468                    file_path,
469                    &obj_def,
470                ),
471
472            ast::schema::TypeDefinition::Union(union_def) =>
473                self.union_builder.visit_type_def(
474                    &mut self.types_map_builder,
475                    file_path,
476                    &union_def,
477                ),
478        }
479    }
480
481    fn visit_ast_type_extension(
482        &mut self,
483        file_path: Option<&Path>,
484        ext: ast::schema::TypeExtension,
485    ) -> Result<()> {
486        use ast::schema::TypeExtension;
487        match ext {
488            TypeExtension::Enum(enum_ext) =>
489                self.enum_builder.visit_type_extension(
490                    &mut self.types_map_builder,
491                    file_path,
492                    enum_ext,
493                ),
494
495            TypeExtension::InputObject(inputobj_ext) =>
496                self.inputobject_builder.visit_type_extension(
497                    &mut self.types_map_builder,
498                    file_path,
499                    inputobj_ext,
500                ),
501
502            TypeExtension::Interface(iface_ext) =>
503                self.interface_builder.visit_type_extension(
504                    &mut self.types_map_builder,
505                    file_path,
506                    iface_ext,
507                ),
508
509            TypeExtension::Object(obj_ext) =>
510                self.object_builder.visit_type_extension(
511                    &mut self.types_map_builder,
512                    file_path,
513                    obj_ext,
514                ),
515
516            TypeExtension::Scalar(scalar_ext) =>
517                self.scalar_builder.visit_type_extension(
518                    &mut self.types_map_builder,
519                    file_path,
520                    scalar_ext,
521                ),
522
523            TypeExtension::Union(union_ext) =>
524                self.union_builder.visit_type_extension(
525                    &mut self.types_map_builder,
526                    file_path,
527                    union_ext,
528                ),
529        }
530    }
531}
532impl Default for SchemaBuilder {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538#[derive(Debug, Error, PartialEq)]
539pub enum SchemaBuildError {
540    #[error("Multiple directives were defined with the same name")]
541    DuplicateDirectiveDefinition {
542        directive_name: String,
543        location1: loc::SourceLocation,
544        location2: loc::SourceLocation,
545    },
546
547    #[error("Multiple enum variants with the same name were defined on a single enum type")]
548    DuplicateEnumValueDefinition {
549        enum_name: String,
550        enum_def_location: loc::SourceLocation,
551        value_def1: loc::SourceLocation,
552        value_def2: loc::SourceLocation,
553    },
554
555    #[error("Multiple fields with the same name were defined on a single object type")]
556    DuplicateFieldNameDefinition {
557        type_name: String,
558        field_name: String,
559        field_def1: loc::SourceLocation,
560        field_def2: loc::SourceLocation,
561    },
562
563    #[error(
564        "The `{type_name}` type declares that it implements the \
565        `{duplicated_interface_name}` interface more than once"
566    )]
567    DuplicateInterfaceImplementsDeclaration {
568        def_location: loc::SourceLocation,
569        duplicated_interface_name: String,
570        type_name: String,
571    },
572
573    #[error("Multiple definitions of the same operation were defined")]
574    DuplicateOperationDefinition {
575        operation: GraphQLOperationType,
576        location1: NamedTypeDefLocation,
577        location2: NamedTypeDefLocation,
578    },
579
580    #[error("Multiple GraphQL types with the same name were defined")]
581    DuplicateTypeDefinition {
582        type_name: String,
583        def1: loc::SourceLocation,
584        def2: loc::SourceLocation,
585    },
586
587    #[error("A union type specifies the same type as a member multiple times")]
588    DuplicatedUnionMember {
589        type_name: String,
590        member1: loc::SourceLocation,
591        member2: loc::SourceLocation,
592    },
593
594    #[error("Enum types must define one or more unique variants")]
595    EnumWithNoVariants {
596        type_name: String,
597        location: loc::SourceLocation,
598    },
599
600    #[error("Attempted to extend a type that is not defined elsewhere")]
601    ExtensionOfUndefinedType {
602        type_name: String,
603        extension_location: loc::SourceLocation,
604    },
605
606    #[error("Attempted to extend a type using a name that corresponds to a different kind of type")]
607    InvalidExtensionType {
608        schema_type: GraphQLType,
609        extension_location: loc::SourceLocation,
610    },
611
612    #[error("Custom directive names must not start with `__`")]
613    InvalidDunderPrefixedDirectiveName {
614        def_location: loc::SourceLocation,
615        directive_name: String,
616    },
617
618    #[error("Field names must not start with `__`")]
619    InvalidDunderPrefixedFieldName {
620        location: loc::SourceLocation,
621        field_name: String,
622        type_name: String,
623    },
624
625    #[error("Parameter names must not start with `__`")]
626    InvalidDunderPrefixedParamName {
627        location: loc::SourceLocation,
628        field_name: String,
629        param_name: String,
630        type_name: String,
631    },
632
633    #[error("Type names must not start with `__`")]
634    InvalidDunderPrefixedTypeName {
635        def_location: loc::SourceLocation,
636        type_name: String,
637    },
638
639    #[error(
640        "Interface types may not declare that they implement themselves: The \
641        `{interface_name}` interface does just that"
642    )]
643    InvalidSelfImplementingInterface {
644        def_location: loc::SourceLocation,
645        interface_name: String,
646    },
647
648    #[error("Attempted to build a schema that has no Query operation type defined")]
649    NoQueryOperationTypeDefined,
650
651    #[error(
652        "The {operation1:?} and {operation2:?} root operation are defined with \
653        the same GraphQL type, but this is not allowed in GraphQL. All root \
654        operations must be defined with different types."
655    )]
656    NonUniqueOperationTypes {
657        reused_type_name: String,
658        operation1: OperationKind,
659        operation1_loc: loc::SourceLocation,
660        operation2: OperationKind,
661        operation2_loc: loc::SourceLocation
662    },
663
664    #[error("Error parsing schema string")]
665    ParseError {
666        file: Option<PathBuf>,
667        err: String,
668    },
669
670    #[error("Attempted to redefine a builtin directive")]
671    RedefinitionOfBuiltinDirective {
672        directive_name: String,
673        location: loc::SourceLocation,
674    },
675
676    #[error("Failure while trying to read a schema file from disk")]
677    SchemaFileReadError(Box<file_reader::ReadContentError>),
678
679    #[error(
680        "Encountered the following type-validation errors while building the \
681        schema:\n\n{}",
682        errors.iter()
683            .map(|s| format!("  * {s}"))
684            .collect::<Vec<_>>()
685            .join("\n"),
686    )]
687    TypeValidationErrors {
688        errors: Vec<TypeValidationError>,
689    },
690}
691
692/// Represents the file location of a given type's definition in the schema.
693#[derive(Clone, Debug, PartialEq)]
694pub struct NamedTypeDefLocation {
695    pub(crate) def_location: loc::SourceLocation,
696    pub(crate) type_name: String,
697}
698impl NamedTypeDefLocation {
699    pub fn def_location(&self) -> &loc::SourceLocation {
700        &self.def_location
701    }
702
703    pub fn type_name(&self) -> &str {
704        self.type_name.as_str()
705    }
706}