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#[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 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 if let (Some(query_type), Some(mut_type)) = (&self.query_type, &self.mutation_type)
393 && query_type.type_name == mut_type.type_name {
394 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 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 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#[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}