1mod arguments;
2mod directive;
3mod directive_definition;
4mod input_value_definition;
5mod value;
6
7use self::{arguments::*, value::*};
8use crate::{directives::*, federated_graph::*};
9use cynic_parser::{
10 common::WrappingType, executable as executable_ast, type_system as ast, values::ConstValue as ParserValue,
11};
12use directive::{
13 collect_definition_directives, collect_enum_value_directives, collect_field_directives,
14 collect_input_value_directives,
15};
16use directive_definition::ingest_directive_definition;
17use indexmap::IndexSet;
18use input_value_definition::ingest_input_value_definition;
19use std::{collections::HashMap, error::Error as StdError, fmt, ops::Range};
20
21const JOIN_GRAPH_DIRECTIVE_NAME: &str = "join__graph";
22pub(crate) const JOIN_GRAPH_ENUM_NAME: &str = "join__Graph";
23
24#[derive(Debug)]
25pub struct DomainError(pub(crate) String);
26
27impl fmt::Display for DomainError {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 f.write_str(&self.0)
30 }
31}
32
33impl StdError for DomainError {}
34
35#[derive(Default)]
36pub(crate) struct State<'a> {
37 graph: FederatedGraph,
38 extensions_loaded: bool,
39 extension_by_enum_value_str: HashMap<&'a str, ExtensionId>,
40
41 strings: IndexSet<String>,
42 query_type_name: Option<String>,
43 mutation_type_name: Option<String>,
44 subscription_type_name: Option<String>,
45
46 definition_names: HashMap<&'a str, Definition>,
47 selection_map: HashMap<(Definition, &'a str), FieldId>,
48 input_values_map: HashMap<(InputObjectId, &'a str), InputValueDefinitionId>,
49 enum_values_map: HashMap<(EnumDefinitionId, &'a str), EnumValueId>,
50
51 graph_by_enum_str: HashMap<&'a str, SubgraphId>,
53 graph_by_name: HashMap<&'a str, SubgraphId>,
54
55 type_wrappers: Vec<WrappingType>,
56}
57
58impl std::ops::Index<StringId> for State<'_> {
59 type Output = str;
60
61 fn index(&self, index: StringId) -> &Self::Output {
62 &self.strings[usize::from(index)]
63 }
64}
65
66impl<'a> State<'a> {
67 fn field_type(&mut self, field_type: ast::Type<'a>) -> Result<Type, DomainError> {
68 self.field_type_from_name_and_wrapping(field_type.name(), field_type.wrappers())
69 }
70
71 fn field_type_from_str(&mut self, ty: &str) -> Result<Type, DomainError> {
72 let mut wrappers = Vec::new();
73 let mut chars = ty.chars().rev();
74
75 let mut start = 0;
76 let mut end = ty.len();
77 loop {
78 match chars.next() {
79 Some('!') => {
80 wrappers.push(WrappingType::NonNull);
81 }
82 Some(']') => {
83 wrappers.push(WrappingType::List);
84 start += 1;
85 }
86 _ => break,
87 }
88 end -= 1;
89 }
90 self.field_type_from_name_and_wrapping(&ty[start..end], wrappers)
91 }
92
93 fn field_type_from_name_and_wrapping(
94 &mut self,
95 name: &str,
96 wrappers: impl IntoIterator<Item = WrappingType>,
97 ) -> Result<Type, DomainError> {
98 use cynic_parser::common::WrappingType;
99
100 self.type_wrappers.clear();
101 self.type_wrappers.extend(wrappers);
102 self.type_wrappers.reverse();
103
104 let mut wrappers = self.type_wrappers.iter().peekable();
105
106 let mut wrapping = match wrappers.peek() {
107 Some(WrappingType::NonNull) => {
108 wrappers.next();
109 wrapping::Wrapping::new(true)
110 }
111 _ => wrapping::Wrapping::new(false),
112 };
113
114 while let Some(next) = wrappers.next() {
115 debug_assert_eq!(*next, WrappingType::List, "double non-null wrapping type not possible");
116
117 wrapping = match wrappers.peek() {
118 Some(WrappingType::NonNull) => {
119 wrappers.next();
120 wrapping.list_non_null()
121 }
122 None | Some(WrappingType::List) => wrapping.list(),
123 }
124 }
125
126 let definition = *self
127 .definition_names
128 .get(name)
129 .ok_or_else(|| DomainError(format!("Unknown type '{}'", name)))?;
130
131 Ok(Type { definition, wrapping })
132 }
133
134 fn insert_string(&mut self, s: &str) -> StringId {
135 if let Some(idx) = self.strings.get_index_of(s) {
136 return StringId::from(idx);
137 }
138
139 StringId::from(self.strings.insert_full(s.to_owned()).0)
140 }
141
142 fn insert_value(&mut self, node: ParserValue<'_>, expected_enum_type: Option<EnumDefinitionId>) -> Value {
143 match node {
144 ParserValue::Null(_) => Value::Null,
145 ParserValue::Int(n) => Value::Int(n.as_i64()),
146 ParserValue::Float(n) => Value::Float(n.as_f64()),
147 ParserValue::String(s) => Value::String(self.insert_string(s.value())),
148 ParserValue::Boolean(b) => Value::Boolean(b.value()),
149 ParserValue::Enum(enm) => expected_enum_type
150 .and_then(|enum_id| {
151 let enum_value_id = self.enum_values_map.get(&(enum_id, enm.name()))?;
152 Some(Value::EnumValue(*enum_value_id))
153 })
154 .unwrap_or(Value::UnboundEnumValue(self.insert_string(enm.name()))),
155 ParserValue::List(list) => Value::List(
156 list.items()
157 .map(|value| self.insert_value(value, expected_enum_type))
158 .collect(),
159 ),
160 ParserValue::Object(obj) => Value::Object(
161 obj.fields()
162 .map(|field| {
163 (
164 self.insert_string(field.name()),
165 self.insert_value(field.value(), expected_enum_type),
166 )
167 })
168 .collect::<Vec<_>>()
169 .into_boxed_slice(),
170 ),
171 }
172 }
173
174 fn root_operation_types(&self) -> Result<RootOperationTypes, DomainError> {
175 fn get_object_id(state: &State<'_>, name: &str) -> Option<ObjectId> {
176 state
177 .definition_names
178 .get(name)
179 .and_then(|definition| match definition {
180 Definition::Object(object_id) => Some(*object_id),
181 _ => None,
182 })
183 }
184 let query_type_name = self.query_type_name.as_deref().unwrap_or("Query");
185 let mutation_type_name = self.mutation_type_name.as_deref().unwrap_or("Mutation");
186 let subscription_type_name = self.subscription_type_name.as_deref().unwrap_or("Subscription");
187 Ok(RootOperationTypes {
188 query: get_object_id(self, query_type_name),
189 mutation: get_object_id(self, mutation_type_name),
190 subscription: get_object_id(self, subscription_type_name),
191 })
192 }
193
194 fn get_definition_name(&self, definition: Definition) -> &str {
195 let name = match definition {
196 Definition::Object(object_id) => self.graph.at(object_id).name,
197 Definition::Interface(interface_id) => self.graph.at(interface_id).name,
198 Definition::Scalar(scalar_id) => self.graph[scalar_id].name,
199 Definition::Enum(enum_id) => self.graph[enum_id].name,
200 Definition::Union(union_id) => self.graph[union_id].name,
201 Definition::InputObject(input_object_id) => self.graph[input_object_id].name,
202 };
203 &self.strings[usize::from(name)]
204 }
205}
206
207pub(crate) fn from_sdl(sdl: &str) -> Result<FederatedGraph, DomainError> {
208 let parsed = cynic_parser::parse_type_system_document(sdl).map_err(|err| crate::DomainError(err.to_string()))?;
209 let mut state = State::default();
210
211 state.graph.strings.clear();
212 state.graph.objects.clear();
213 state.graph.fields.clear();
214 state.graph.scalar_definitions.clear();
215
216 ingest_definitions(&parsed, &mut state)?;
217 ingest_schema_and_directive_definitions(&parsed, &mut state)?;
218
219 ingest_fields(&parsed, &mut state)?;
220
221 ingest_directives_after_graph(&parsed, &mut state)?;
223
224 let mut graph = FederatedGraph {
225 directive_definitions: std::mem::take(&mut state.graph.directive_definitions),
226 directive_definition_arguments: std::mem::take(&mut state.graph.directive_definition_arguments),
227 root_operation_types: state.root_operation_types()?,
228 strings: state.strings.into_iter().collect(),
229 ..state.graph
230 };
231
232 graph.enum_values.sort_unstable_by_key(|v| v.enum_id);
233
234 Ok(graph)
235}
236
237fn ingest_schema_and_directive_definitions<'a>(
238 parsed: &'a ast::TypeSystemDocument,
239 state: &mut State<'a>,
240) -> Result<(), DomainError> {
241 for definition in parsed.definitions() {
242 match definition {
243 ast::Definition::Schema(schema_definition) => {
244 ingest_schema_definition(schema_definition, state)?;
245 }
246 ast::Definition::Directive(directive_definition) => {
247 ingest_directive_definition(directive_definition, state)?;
248 }
249 _ => (),
250 }
251 }
252
253 Ok(())
254}
255
256fn ingest_fields<'a>(parsed: &'a ast::TypeSystemDocument, state: &mut State<'a>) -> Result<(), DomainError> {
257 for definition in parsed.definitions() {
258 match definition {
259 ast::Definition::Schema(_) | ast::Definition::SchemaExtension(_) | ast::Definition::Directive(_) => (),
260 ast::Definition::Type(typedef) | ast::Definition::TypeExtension(typedef) => match &typedef {
261 ast::TypeDefinition::Scalar(_) => (),
262 ast::TypeDefinition::Object(object) => {
263 let Definition::Object(object_id) = state.definition_names[typedef.name()] else {
264 return Err(DomainError(
265 "Broken invariant: object id behind object name.".to_owned(),
266 ));
267 };
268 ingest_object_interfaces(object_id, object, state)?;
269 ingest_object_fields(object_id, object.fields(), state)?;
270 }
271 ast::TypeDefinition::Interface(interface) => {
272 let Definition::Interface(interface_id) = state.definition_names[typedef.name()] else {
273 return Err(DomainError(
274 "Broken invariant: interface id behind interface name.".to_owned(),
275 ));
276 };
277 ingest_interface_interfaces(interface_id, interface, state)?;
278 ingest_interface_fields(interface_id, interface.fields(), state)?;
279 }
280 ast::TypeDefinition::Union(union) => {
281 let Definition::Union(union_id) = state.definition_names[typedef.name()] else {
282 return Err(DomainError("Broken invariant: UnionId behind union name.".to_owned()));
283 };
284 ingest_union_members(union_id, union, state)?;
285 }
286 ast::TypeDefinition::Enum(_) => {}
287 ast::TypeDefinition::InputObject(input_object) => {
288 let Definition::InputObject(input_object_id) = state.definition_names[typedef.name()] else {
289 return Err(DomainError(
290 "Broken invariant: InputObjectId behind input object name.".to_owned(),
291 ));
292 };
293 ingest_input_object(input_object_id, input_object, state)?;
294 }
295 },
296 }
297 }
298
299 Ok(())
300}
301
302fn ingest_schema_definition(schema: ast::SchemaDefinition<'_>, state: &mut State<'_>) -> Result<(), DomainError> {
303 for directive in schema.directives() {
304 let name = directive.name();
305 if name != "link" {
306 return Err(DomainError(format!("Unsupported directive {name} on schema.")));
307 }
308 }
309
310 if let Some(query) = schema.query_type() {
311 state.query_type_name = Some(query.named_type().to_owned());
312 }
313
314 if let Some(mutation) = schema.mutation_type() {
315 state.mutation_type_name = Some(mutation.named_type().to_owned());
316 }
317
318 if let Some(subscription) = schema.subscription_type() {
319 state.subscription_type_name = Some(subscription.named_type().to_owned());
320 }
321
322 Ok(())
323}
324
325fn ingest_interface_interfaces(
326 interface_id: InterfaceId,
327 interface: &ast::InterfaceDefinition<'_>,
328 state: &mut State<'_>,
329) -> Result<(), DomainError> {
330 state.graph.interfaces[usize::from(interface_id)].implements_interfaces = interface
331 .implements_interfaces()
332 .map(|name| match state.definition_names[name] {
333 Definition::Interface(interface_id) => Ok(interface_id),
334 _ => Err(DomainError(
335 "Broken invariant: object implements non-interface type".to_owned(),
336 )),
337 })
338 .collect::<Result<Vec<_>, _>>()?;
339
340 Ok(())
341}
342
343fn ingest_object_interfaces(
344 object_id: ObjectId,
345 object: &ast::ObjectDefinition<'_>,
346 state: &mut State<'_>,
347) -> Result<(), DomainError> {
348 state.graph.objects[usize::from(object_id)].implements_interfaces = object
349 .implements_interfaces()
350 .map(|name| match state.definition_names[name] {
351 Definition::Interface(interface_id) => Ok(interface_id),
352 _ => Err(DomainError(
353 "Broken invariant: object implements non-interface type".to_owned(),
354 )),
355 })
356 .collect::<Result<Vec<_>, _>>()?;
357
358 Ok(())
359}
360
361fn ingest_directives_after_graph<'a>(
362 parsed: &'a ast::TypeSystemDocument,
363 state: &mut State<'a>,
364) -> Result<(), DomainError> {
365 for definition in parsed.definitions() {
366 let (ast::Definition::Type(typedef) | ast::Definition::TypeExtension(typedef)) = definition else {
367 continue;
368 };
369
370 let Some(definition_id) = state.definition_names.get(typedef.name()).copied() else {
372 continue;
373 };
374 let directives = collect_definition_directives(definition_id, typedef.directives(), state)?;
375
376 match definition_id {
377 Definition::Scalar(id) => state.graph[id].directives = directives,
378 Definition::Object(id) => state.graph[id].directives = directives,
379 Definition::Interface(id) => state.graph[id].directives = directives,
380 Definition::Union(id) => state.graph[id].directives = directives,
381 Definition::Enum(id) => state.graph[id].directives = directives,
382 Definition::InputObject(id) => state.graph[id].directives = directives,
383 }
384
385 let fields = match typedef {
386 ast::TypeDefinition::Object(object) => Some(object.fields()),
387 ast::TypeDefinition::Interface(iface) => Some(iface.fields()),
388 _ => None,
389 };
390 if let Some(fields) = fields {
391 for field in fields {
392 let field_id = state.selection_map[&(definition_id, field.name())];
393 state.graph[field_id].directives =
394 collect_field_directives(definition_id, field_id, field.directives(), state)?;
395 }
396 }
397 }
398
399 Ok(())
400}
401
402fn ingest_definitions<'a>(document: &'a ast::TypeSystemDocument, state: &mut State<'a>) -> Result<(), DomainError> {
403 for definition in document.definitions() {
404 match definition {
405 ast::Definition::SchemaExtension(_) | ast::Definition::Schema(_) | ast::Definition::Directive(_) => (),
406 ast::Definition::TypeExtension(typedef) | ast::Definition::Type(typedef) => {
407 let type_name = typedef.name();
408
409 let (namespace, type_name_id) = split_namespace_name(type_name, state);
410
411 let description = typedef
412 .description()
413 .map(|description| state.insert_string(&description.to_cow()));
414
415 match typedef {
416 ast::TypeDefinition::Enum(enm) if type_name == JOIN_GRAPH_ENUM_NAME => {
417 ingest_join_graph_enum(namespace, type_name_id, description, type_name, enm, state)?;
418 continue;
419 }
420 ast::TypeDefinition::Enum(enm) if type_name == EXTENSION_LINK_ENUM => {
422 ingest_extension_link_enum(namespace, type_name_id, description, type_name, enm, state)?;
423 continue;
424 }
425 _ => (),
426 }
427
428 match typedef {
429 ast::TypeDefinition::Scalar(_) => {
430 let scalar_definition_id = state.graph.push_scalar_definition(ScalarDefinitionRecord {
431 namespace,
432 name: type_name_id,
433 directives: Vec::new(),
434 description,
435 });
436
437 state
438 .definition_names
439 .insert(type_name, Definition::Scalar(scalar_definition_id));
440 }
441 ast::TypeDefinition::Object(_) => {
442 let object_id = ObjectId::from(state.graph.objects.push_return_idx(Object {
443 name: type_name_id,
444 description,
445 directives: Vec::new(),
446 implements_interfaces: Vec::new(),
447 fields: NO_FIELDS,
448 }));
449
450 state.definition_names.insert(type_name, Definition::Object(object_id));
451 }
452 ast::TypeDefinition::Interface(_) => {
453 let interface_id = InterfaceId::from(state.graph.interfaces.push_return_idx(Interface {
454 name: type_name_id,
455 description,
456 directives: Vec::new(),
457 implements_interfaces: Vec::new(),
458 fields: NO_FIELDS,
459 }));
460 state
461 .definition_names
462 .insert(type_name, Definition::Interface(interface_id));
463 }
464 ast::TypeDefinition::Union(_) => {
465 let union_id = UnionId::from(state.graph.unions.push_return_idx(Union {
466 name: type_name_id,
467 members: Vec::new(),
468 description,
469 directives: Vec::new(),
470 }));
471 state.definition_names.insert(type_name, Definition::Union(union_id));
472 }
473 ast::TypeDefinition::Enum(enm) => {
474 if enm.name() == JOIN_GRAPH_ENUM_NAME {
475 continue;
476 }
477
478 ingest_enum_definition(namespace, type_name_id, description, type_name, enm, state)?;
479 }
480 ast::TypeDefinition::InputObject(_) => {
481 let input_object_id =
482 InputObjectId::from(state.graph.input_objects.push_return_idx(InputObject {
483 name: type_name_id,
484 fields: NO_INPUT_VALUE_DEFINITION,
485 directives: Vec::new(),
486 description,
487 }));
488 state
489 .definition_names
490 .insert(type_name, Definition::InputObject(input_object_id));
491 }
492 }
493 }
494 }
495 }
496
497 insert_builtin_scalars(state);
498
499 Ok(())
500}
501
502fn ingest_enum_definition<'a>(
503 namespace: Option<StringId>,
504 type_name_id: StringId,
505 description: Option<StringId>,
506 type_name: &'a str,
507 enm: ast::EnumDefinition<'a>,
508 state: &mut State<'a>,
509) -> Result<EnumDefinitionId, DomainError> {
510 let enum_definition_id = state.graph.push_enum_definition(EnumDefinitionRecord {
511 namespace,
512 name: type_name_id,
513 directives: Vec::new(),
514 description,
515 });
516
517 state
518 .definition_names
519 .insert(type_name, Definition::Enum(enum_definition_id));
520
521 for value in enm.values() {
522 let description = value
523 .description()
524 .map(|description| state.insert_string(&description.to_cow()));
525
526 let directives = collect_enum_value_directives(value.directives(), state)?;
527 let value_string_id = state.insert_string(value.value());
528 let id = state.graph.push_enum_value(EnumValueRecord {
529 enum_id: enum_definition_id,
530 value: value_string_id,
531 directives,
532 description,
533 });
534
535 state.enum_values_map.insert((enum_definition_id, value.value()), id);
536 }
537
538 Ok(enum_definition_id)
539}
540
541fn insert_builtin_scalars(state: &mut State<'_>) {
542 for name_str in ["String", "ID", "Float", "Boolean", "Int"] {
543 let name = state.insert_string(name_str);
544 let id = state.graph.push_scalar_definition(ScalarDefinitionRecord {
545 namespace: None,
546 name,
547 directives: Vec::new(),
548 description: None,
549 });
550 state.definition_names.insert(name_str, Definition::Scalar(id));
551 }
552}
553
554fn ingest_interface_fields<'a>(
555 interface_id: InterfaceId,
556 fields: impl Iterator<Item = ast::FieldDefinition<'a>>,
557 state: &mut State<'a>,
558) -> Result<(), DomainError> {
559 let [mut start, mut end] = [None; 2];
560
561 for field in fields {
562 let field_id = ingest_field(EntityDefinitionId::Interface(interface_id), field, state)?;
563 start = Some(start.unwrap_or(field_id));
564 end = Some(field_id);
565 }
566
567 if let [Some(start), Some(end)] = [start, end] {
568 state.graph.interfaces[usize::from(interface_id)].fields = Range {
569 start,
570 end: FieldId::from(usize::from(end) + 1),
571 };
572 };
573 Ok(())
574}
575
576fn ingest_field<'a>(
577 parent_entity_id: EntityDefinitionId,
578 ast_field: ast::FieldDefinition<'a>,
579 state: &mut State<'a>,
580) -> Result<FieldId, DomainError> {
581 let field_name = ast_field.name();
582 let r#type = state.field_type(ast_field.ty())?;
583 let name = state.insert_string(field_name);
584 let args_start = state.graph.input_value_definitions.len();
585
586 for arg in ast_field.arguments() {
587 let description = arg
588 .description()
589 .map(|description| state.insert_string(&description.to_cow()));
590 let directives = collect_input_value_directives(arg.directives(), state)?;
591 let name = state.insert_string(arg.name());
592 let r#type = state.field_type(arg.ty())?;
593 let default = arg
594 .default_value()
595 .map(|default| state.insert_value(default, r#type.definition.as_enum()));
596
597 state.graph.input_value_definitions.push(InputValueDefinition {
598 name,
599 r#type,
600 directives,
601 description,
602 default,
603 });
604 }
605
606 let args_end = state.graph.input_value_definitions.len();
607
608 let description = ast_field
609 .description()
610 .map(|description| state.insert_string(&description.to_cow()));
611
612 let field_id = FieldId::from(state.graph.fields.push_return_idx(Field {
613 name,
614 r#type,
615 parent_entity_id,
616 arguments: (InputValueDefinitionId::from(args_start), args_end - args_start),
617 description,
618 directives: Vec::new(),
620 }));
621
622 state
623 .selection_map
624 .insert((parent_entity_id.into(), field_name), field_id);
625
626 Ok(field_id)
627}
628
629fn ingest_union_members<'a>(
630 union_id: UnionId,
631 union: &ast::UnionDefinition<'a>,
632 state: &mut State<'a>,
633) -> Result<(), DomainError> {
634 for member in union.members() {
635 let Definition::Object(object_id) = state.definition_names[member.name()] else {
636 return Err(DomainError("Non-object type in union members".to_owned()));
637 };
638 state.graph.unions[usize::from(union_id)].members.push(object_id);
639 }
640
641 Ok(())
642}
643
644fn ingest_input_object<'a>(
645 input_object_id: InputObjectId,
646 input_object: &ast::InputObjectDefinition<'a>,
647 state: &mut State<'a>,
648) -> Result<(), DomainError> {
649 let start = state.graph.input_value_definitions.len();
650 for field in input_object.fields() {
651 state.input_values_map.insert(
652 (input_object_id, field.name()),
653 InputValueDefinitionId::from(state.graph.input_value_definitions.len()),
654 );
655 ingest_input_value_definition(field, state)?;
656 }
657 let end = state.graph.input_value_definitions.len();
658
659 state.graph.input_objects[usize::from(input_object_id)].fields = (InputValueDefinitionId::from(start), end - start);
660 Ok(())
661}
662
663fn ingest_object_fields<'a>(
664 object_id: ObjectId,
665 fields: impl Iterator<Item = ast::FieldDefinition<'a>>,
666 state: &mut State<'a>,
667) -> Result<(), DomainError> {
668 let start = state.graph.fields.len();
669 for field in fields {
670 ingest_field(EntityDefinitionId::Object(object_id), field, state)?;
671 }
672
673 state.graph[object_id].fields = Range {
674 start: FieldId::from(start),
675 end: FieldId::from(state.graph.fields.len()),
676 };
677
678 Ok(())
679}
680
681fn parse_selection_set(fields: &str) -> Result<executable_ast::ExecutableDocument, DomainError> {
682 let fields = format!("{{ {fields} }}");
683
684 cynic_parser::parse_executable_document(&fields)
685 .map_err(|err| format!("Error parsing a selection from a federated directive: {err}"))
686 .map_err(DomainError)
687}
688
689fn attach_selection_set(
692 selection_set: &executable_ast::ExecutableDocument,
693 target: Definition,
694 state: &mut State<'_>,
695) -> Result<SelectionSet, DomainError> {
696 let operation = selection_set
697 .operations()
698 .next()
699 .expect("first operation is there by construction");
700
701 attach_selection_set_rec(operation.selection_set(), target, state)
702}
703
704fn attach_selection_set_rec<'a>(
705 selection_set: impl Iterator<Item = executable_ast::Selection<'a>>,
706 target: Definition,
707 state: &mut State<'_>,
708) -> Result<SelectionSet, DomainError> {
709 selection_set
710 .map(|selection| match selection {
711 executable_ast::Selection::Field(ast_field) => attach_selection_field(ast_field, target, state),
712 executable_ast::Selection::InlineFragment(inline_fragment) => {
713 attach_inline_fragment(inline_fragment, state)
714 }
715 executable_ast::Selection::FragmentSpread(_) => {
716 Err(DomainError("Unsupported fragment spread in selection set".to_owned()))
717 }
718 })
719 .collect()
720}
721
722fn attach_selection_field(
723 ast_field: executable_ast::FieldSelection<'_>,
724 target: Definition,
725 state: &mut State<'_>,
726) -> Result<Selection, DomainError> {
727 let field_id: FieldId = *state.selection_map.get(&(target, ast_field.name())).ok_or_else(|| {
728 DomainError(format!(
729 "Field '{}.{}' does not exist",
730 state.get_definition_name(target),
731 ast_field.name(),
732 ))
733 })?;
734 let field_ty = state.graph[field_id].r#type.definition;
735 let arguments = ast_field
736 .arguments()
737 .map(|argument| {
738 let name = state.insert_string(argument.name());
739 let (start, len) = state.graph[field_id].arguments;
740 let arguments = &state.graph.input_value_definitions[usize::from(start)..usize::from(start) + len];
741 let argument_id = arguments
742 .iter()
743 .position(|arg| arg.name == name)
744 .map(|idx| InputValueDefinitionId::from(usize::from(start) + idx))
745 .expect("unknown argument");
746
747 let argument_type = state.graph.input_value_definitions[usize::from(argument_id)]
748 .r#type
749 .definition
750 .as_enum();
751
752 let const_value = argument
753 .value()
754 .try_into()
755 .map_err(|_| DomainError("FieldSets cant contain variables".into()))?;
756
757 let value = state.insert_value(const_value, argument_type);
758
759 Ok((argument_id, value))
760 })
761 .collect::<Result<_, _>>()?;
762
763 Ok(Selection::Field(FieldSelection {
764 field_id,
765 arguments,
766 subselection: attach_selection_set_rec(ast_field.selection_set(), field_ty, state)?,
767 }))
768}
769
770fn attach_inline_fragment(
771 inline_fragment: executable_ast::InlineFragment<'_>,
772 state: &mut State<'_>,
773) -> Result<Selection, DomainError> {
774 let on: Definition = match inline_fragment.type_condition() {
775 Some(type_name) => *state
776 .definition_names
777 .get(type_name)
778 .ok_or_else(|| DomainError(format!("Type '{}' in type condition does not exist", type_name)))?,
779 None => {
780 return Err(DomainError(
781 "Fragments without type condition are not supported".to_owned(),
782 ));
783 }
784 };
785
786 let subselection = attach_selection_set_rec(inline_fragment.selection_set(), on, state)?;
787
788 Ok(Selection::InlineFragment { on, subselection })
789}
790
791fn attach_input_value_set_to_field_arguments(
792 selection_set: executable_ast::ExecutableDocument,
793 parent: Definition,
794 field_id: FieldId,
795 state: &mut State<'_>,
796) -> Result<InputValueDefinitionSet, DomainError> {
797 let operation = selection_set
798 .operations()
799 .next()
800 .expect("first operation is there by construction");
801
802 attach_input_value_set_to_field_arguments_rec(operation.selection_set(), parent, field_id, state)
803}
804
805fn attach_input_value_set_to_field_arguments_rec<'a>(
806 selection_set: impl Iterator<Item = executable_ast::Selection<'a>>,
807 parent: Definition,
808 field_id: FieldId,
809 state: &mut State<'_>,
810) -> Result<InputValueDefinitionSet, DomainError> {
811 let (start, len) = state.graph[field_id].arguments;
812 selection_set
813 .map(|selection| {
814 let executable_ast::Selection::Field(ast_arg) = selection else {
815 return Err(DomainError("Unsupported fragment spread in selection set".to_owned()));
816 };
817
818 let arguments = &state.graph.input_value_definitions[usize::from(start)..usize::from(start) + len];
819 let Some((i, arg)) = arguments
820 .iter()
821 .enumerate()
822 .find(|(_, arg)| state.strings.get_index(usize::from(arg.name)).unwrap() == ast_arg.name())
823 else {
824 return Err(DomainError(format!(
825 "Argument '{}' does not exist for the field '{}.{}'",
826 ast_arg.name(),
827 state.get_definition_name(parent),
828 state
829 .strings
830 .get_index(usize::from(state.graph[field_id].name))
831 .unwrap(),
832 )));
833 };
834
835 let mut ast_subselection = ast_arg.selection_set().peekable();
836
837 let subselection = if let Definition::InputObject(input_object_id) = arg.r#type.definition {
838 if ast_subselection.peek().is_none() {
839 return Err(DomainError("InputObject must have a subselection".to_owned()));
840 }
841 attach_input_value_set_rec(ast_subselection, input_object_id, state)?
842 } else if ast_subselection.peek().is_some() {
843 return Err(DomainError("Only InputObject can have a subselection".to_owned()));
844 } else {
845 InputValueDefinitionSet::default()
846 };
847
848 Ok(InputValueDefinitionSetItem {
849 input_value_definition: InputValueDefinitionId::from(usize::from(start) + i),
850 subselection,
851 })
852 })
853 .collect()
854}
855
856fn attach_input_value_set_rec<'a>(
857 selection_set: impl Iterator<Item = executable_ast::Selection<'a>>,
858 input_object_id: InputObjectId,
859 state: &mut State<'_>,
860) -> Result<InputValueDefinitionSet, DomainError> {
861 selection_set
862 .map(|selection| {
863 let executable_ast::Selection::Field(ast_field) = selection else {
864 return Err(DomainError("Unsupported fragment spread in selection set".to_owned()));
865 };
866 let id = *state
867 .input_values_map
868 .get(&(input_object_id, ast_field.name()))
869 .ok_or_else(|| {
870 DomainError(format!(
871 "Input field '{}.{}' does not exist",
872 state.get_definition_name(Definition::InputObject(input_object_id)),
873 ast_field.name(),
874 ))
875 })?;
876
877 let mut ast_subselection = ast_field.selection_set().peekable();
878
879 let subselection = if let Definition::InputObject(input_object_id) =
880 state.graph.input_value_definitions[usize::from(id)].r#type.definition
881 {
882 if ast_subselection.peek().is_none() {
883 return Err(DomainError("InputObject must have a subselection".to_owned()));
884 }
885 attach_input_value_set_rec(ast_subselection, input_object_id, state)?
886 } else if ast_subselection.peek().is_some() {
887 return Err(DomainError("Only InputObject can have a subselection".to_owned()));
888 } else {
889 InputValueDefinitionSet::default()
890 };
891
892 Ok(InputValueDefinitionSetItem {
893 input_value_definition: id,
894 subselection,
895 })
896 })
897 .collect()
898}
899
900fn ingest_join_graph_enum<'a>(
901 namespace: Option<StringId>,
902 type_name_id: StringId,
903 description: Option<StringId>,
904 type_name: &'a str,
905 enm: ast::EnumDefinition<'a>,
906 state: &mut State<'a>,
907) -> Result<(), DomainError> {
908 let enum_definition_id = ingest_enum_definition(namespace, type_name_id, description, type_name, enm, state)?;
909
910 for value in enm.values() {
911 let sdl_name = value.value();
912 let directive = value
913 .directives()
914 .find(|directive| directive.name() == JOIN_GRAPH_DIRECTIVE_NAME)
915 .ok_or_else(|| DomainError("Missing @join__graph directive on join__Graph enum value.".to_owned()))?;
916 let name = directive
917 .get_argument("name")
918 .ok_or_else(|| {
919 DomainError(
920 "Missing `name` argument in `@join__graph` directive on `join__Graph` enum value.".to_owned(),
921 )
922 })
923 .and_then(|arg| match arg {
924 ParserValue::String(s) => Ok(s),
925 _ => Err(DomainError(
926 "Unexpected type for `name` argument in `@join__graph` directive on `join__Graph` enum value."
927 .to_owned(),
928 )),
929 })?;
930 let url = directive
931 .get_argument("url")
932 .map(|arg| match arg {
933 ParserValue::String(s) => Ok(s),
934 _ => Err(DomainError(
935 "Unexpected type for `url` argument in `@join__graph` directive on `join__Graph` enum value."
936 .to_owned(),
937 )),
938 })
939 .transpose()?;
940
941 let subgraph_name = state.insert_string(name.value());
942 let url = url.map(|url| state.insert_string(url.value()));
943 let sdl_name_string_id = state.insert_string(sdl_name);
944 let join_graph_enum_value_name = state
945 .graph
946 .iter_enum_values(enum_definition_id)
947 .find(|value| value.value == sdl_name_string_id)
948 .unwrap()
949 .id();
950
951 let id = SubgraphId::from(state.graph.subgraphs.push_return_idx(Subgraph {
952 name: subgraph_name,
953 join_graph_enum_value: join_graph_enum_value_name,
954 url,
955 }));
956 state.graph_by_enum_str.insert(sdl_name, id);
957 state.graph_by_name.insert(name.value(), id);
958 }
959
960 Ok(())
961}
962
963fn ingest_extension_link_enum<'a>(
964 namespace: Option<StringId>,
965 type_name_id: StringId,
966 description: Option<StringId>,
967 type_name: &'a str,
968 enm: ast::EnumDefinition<'a>,
969 state: &mut State<'a>,
970) -> Result<(), DomainError> {
971 use directive::{ExtensionLink, parse_extension_link};
972
973 let enum_definition_id = state.graph.push_enum_definition(EnumDefinitionRecord {
974 namespace,
975 name: type_name_id,
976 directives: Vec::new(),
977 description,
978 });
979
980 state
981 .definition_names
982 .insert(type_name, Definition::Enum(enum_definition_id));
983
984 for value in enm.values() {
985 let description = value
986 .description()
987 .map(|description| state.insert_string(&description.to_cow()));
988
989 let directive = value
990 .directives()
991 .find(|directive| directive.name() == EXTENSION_LINK_DIRECTIVE)
992 .ok_or_else(|| {
993 DomainError(format!(
994 "Missing @{} directive on {} enum value.",
995 EXTENSION_LINK_DIRECTIVE, EXTENSION_LINK_ENUM
996 ))
997 })?;
998
999 let ExtensionLink { url, schema_directives } = parse_extension_link(directive, state)?;
1000 let url = state.insert_string(&url);
1001
1002 let value_string_id = state.insert_string(value.value());
1003 let enum_value_id = state.graph.push_enum_value(EnumValueRecord {
1004 enum_id: enum_definition_id,
1005 value: value_string_id,
1006 directives: Vec::new(),
1007 description,
1008 });
1009
1010 state
1011 .enum_values_map
1012 .insert((enum_definition_id, value.value()), enum_value_id);
1013
1014 let extension_id = state.graph.push_extension(Extension {
1015 url,
1016 enum_value_id,
1017 schema_directives,
1018 });
1019
1020 state.extension_by_enum_value_str.insert(value.value(), extension_id);
1021 }
1022
1023 state.extensions_loaded = true;
1024
1025 Ok(())
1026}
1027
1028trait VecExt<T> {
1029 fn push_return_idx(&mut self, elem: T) -> usize;
1030}
1031
1032impl<T> VecExt<T> for Vec<T> {
1033 fn push_return_idx(&mut self, elem: T) -> usize {
1034 let idx = self.len();
1035 self.push(elem);
1036 idx
1037 }
1038}
1039
1040#[cfg(test)]
1041#[test]
1042fn test_from_sdl() {
1043 FederatedGraph::from_sdl(r#"
1045 schema
1046 @link(url: "https://specs.apollo.dev/link/v1.0")
1047 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1048 {
1049 query: Query
1050 }
1051
1052 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1053
1054 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1055
1056 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1057
1058 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1059
1060 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1061
1062 directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1063
1064 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1065
1066 scalar join__FieldSet
1067
1068 enum join__Graph {
1069 ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql")
1070 INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql")
1071 PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql")
1072 REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql")
1073 }
1074
1075 scalar link__Import
1076
1077 enum link__Purpose {
1078 """
1079 `SECURITY` features provide metadata necessary to securely resolve fields.
1080 """
1081 SECURITY
1082
1083 """
1084 `EXECUTION` features provide metadata necessary for operation execution.
1085 """
1086 EXECUTION
1087 }
1088
1089 type Product
1090 @join__type(graph: INVENTORY, key: "upc")
1091 @join__type(graph: PRODUCTS, key: "upc")
1092 @join__type(graph: REVIEWS, key: "upc")
1093 {
1094 upc: String!
1095 weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS)
1096 price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS)
1097 inStock: Boolean @join__field(graph: INVENTORY)
1098 shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight")
1099 name: String @join__field(graph: PRODUCTS)
1100 reviews: [Review] @join__field(graph: REVIEWS)
1101 }
1102
1103 type Query
1104 @join__type(graph: ACCOUNTS)
1105 @join__type(graph: INVENTORY)
1106 @join__type(graph: PRODUCTS)
1107 @join__type(graph: REVIEWS)
1108 {
1109 me: User @join__field(graph: ACCOUNTS)
1110 user(id: ID!): User @join__field(graph: ACCOUNTS)
1111 users: [User] @join__field(graph: ACCOUNTS)
1112 topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
1113 }
1114
1115 type Review
1116 @join__type(graph: REVIEWS, key: "id")
1117 {
1118 id: ID!
1119 body: String
1120 product: Product
1121 author: User @join__field(graph: REVIEWS, provides: "username")
1122 }
1123
1124 type User
1125 @join__type(graph: ACCOUNTS, key: "id")
1126 @join__type(graph: REVIEWS, key: "id")
1127 {
1128 id: ID!
1129 name: String @join__field(graph: ACCOUNTS)
1130 username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true)
1131 birthday: Int @join__field(graph: ACCOUNTS)
1132 reviews: [Review] @join__field(graph: REVIEWS)
1133 }
1134 "#).unwrap();
1135}
1136
1137#[cfg(test)]
1138#[test]
1139fn test_from_sdl_with_empty_query_root() {
1140 FederatedGraph::from_sdl(
1142 r#"
1143 schema
1144 @link(url: "https://specs.apollo.dev/link/v1.0")
1145 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1146 {
1147 query: Query
1148 }
1149
1150 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1151
1152 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1153
1154 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1155
1156 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1157
1158 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1159
1160 directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1161
1162 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1163
1164 scalar join__FieldSet
1165
1166 enum join__Graph {
1167 ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql")
1168 INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql")
1169 PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql")
1170 REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql")
1171 }
1172
1173 scalar link__Import
1174
1175 enum link__Purpose {
1176 """
1177 `SECURITY` features provide metadata necessary to securely resolve fields.
1178 """
1179 SECURITY
1180
1181 """
1182 `EXECUTION` features provide metadata necessary for operation execution.
1183 """
1184 EXECUTION
1185 }
1186
1187 type Query
1188
1189 type User
1190 @join__type(graph: ACCOUNTS, key: "id")
1191 @join__type(graph: REVIEWS, key: "id")
1192 {
1193 id: ID!
1194 name: String @join__field(graph: ACCOUNTS)
1195 username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true)
1196 birthday: Int @join__field(graph: ACCOUNTS)
1197 reviews: [Review] @join__field(graph: REVIEWS)
1198 }
1199
1200 type Review
1201 @join__type(graph: REVIEWS, key: "id")
1202 {
1203 id: ID!
1204 body: String
1205 author: User @join__field(graph: REVIEWS, provides: "username")
1206 }
1207 "#,
1208 ).unwrap();
1209}
1210
1211#[cfg(test)]
1212#[test]
1213fn test_from_sdl_with_missing_query_root() {
1214 FederatedGraph::from_sdl(
1216 r#"
1217 schema
1218 @link(url: "https://specs.apollo.dev/link/v1.0")
1219 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1220 {
1221 query: Query
1222 }
1223
1224 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1225
1226 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1227
1228 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1229
1230 directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1231
1232 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1233
1234 directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1235
1236 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1237
1238 scalar join__FieldSet
1239
1240 enum join__Graph {
1241 ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql")
1242 INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql")
1243 PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql")
1244 REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql")
1245 }
1246
1247 scalar link__Import
1248
1249 enum link__Purpose {
1250 """
1251 `SECURITY` features provide metadata necessary to securely resolve fields.
1252 """
1253 SECURITY
1254
1255 """
1256 `EXECUTION` features provide metadata necessary for operation execution.
1257 """
1258 EXECUTION
1259 }
1260
1261 type Review
1262 @join__type(graph: REVIEWS, key: "id")
1263 {
1264 id: ID!
1265 body: String
1266 author: User @join__field(graph: REVIEWS, provides: "username")
1267 }
1268
1269 type User
1270 @join__type(graph: ACCOUNTS, key: "id")
1271 @join__type(graph: REVIEWS, key: "id")
1272 {
1273 id: ID!
1274 name: String @join__field(graph: ACCOUNTS)
1275 username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true)
1276 birthday: Int @join__field(graph: ACCOUNTS)
1277 reviews: [Review] @join__field(graph: REVIEWS)
1278 }
1279 "#,
1280 ).unwrap();
1281}
1282
1283pub(crate) fn split_namespace_name(original_name: &str, state: &mut State<'_>) -> (Option<StringId>, StringId) {
1284 match original_name.split_once("__") {
1285 Some((namespace, name)) => {
1286 let namespace = state.insert_string(namespace);
1287 let name = state.insert_string(name);
1288
1289 (Some(namespace), name)
1290 }
1291 None => (None, state.insert_string(original_name)),
1292 }
1293}
1294
1295#[cfg(test)]
1296#[test]
1297fn test_missing_type() {
1298 let sdl = r###"
1299 directive @core(feature: String!) repeatable on SCHEMA
1300
1301 directive @join__owner(graph: join__Graph!) on OBJECT
1302
1303 directive @join__type(
1304 graph: join__Graph!
1305 key: String!
1306 resolvable: Boolean = true
1307 ) repeatable on OBJECT | INTERFACE
1308
1309 directive @join__field(
1310 graph: join__Graph
1311 requires: String
1312 provides: String
1313 ) on FIELD_DEFINITION
1314
1315 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1316
1317 enum join__Graph {
1318 MANGROVE @join__graph(name: "mangrove", url: "http://example.com/mangrove")
1319 STEPPE @join__graph(name: "steppe", url: "http://example.com/steppe")
1320 }
1321
1322 type Query {
1323 getMammoth: Mammoth @join__field(graph: mangrove)
1324 }
1325 "###;
1326 let actual = FederatedGraph::from_sdl(sdl);
1327 assert!(actual.is_err());
1328}
1329
1330#[cfg(test)]
1331#[test]
1332fn test_join_field_type() {
1333 let sdl = r###"
1334 schema
1335 @link(url: "https://specs.apollo.dev/link/v1.0")
1336 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
1337 query: Query
1338 }
1339
1340 directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1341
1342 directive @join__field(
1343 graph: join__Graph
1344 requires: join__FieldSet
1345 provides: join__FieldSet
1346 type: String
1347 external: Boolean
1348 override: String
1349 usedOverridden: Boolean
1350 ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1351
1352 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1353
1354 directive @join__implements(
1355 graph: join__Graph!
1356 interface: String!
1357 ) repeatable on OBJECT | INTERFACE
1358
1359 directive @join__type(
1360 graph: join__Graph!
1361 key: join__FieldSet
1362 extension: Boolean! = false
1363 resolvable: Boolean! = true
1364 isInterfaceObject: Boolean! = false
1365 ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1366
1367 directive @join__unionMember(
1368 graph: join__Graph!
1369 member: String!
1370 ) repeatable on UNION
1371
1372 directive @link(
1373 url: String
1374 as: String
1375 for: link__Purpose
1376 import: [link__Import]
1377 ) repeatable on SCHEMA
1378
1379 union Account
1380 @join__type(graph: B)
1381 @join__unionMember(graph: B, member: "User")
1382 @join__unionMember(graph: B, member: "Admin") =
1383 | User
1384 | Admin
1385
1386 type Admin @join__type(graph: B) {
1387 id: ID
1388 name: String
1389 similarAccounts: [Account!]!
1390 }
1391
1392 scalar join__FieldSet
1393
1394 enum join__Graph {
1395 A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1396 B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1397 }
1398
1399 scalar link__Import
1400
1401 enum link__Purpose {
1402 """
1403 `SECURITY` features provide metadata necessary to securely resolve fields.
1404 """
1405 SECURITY
1406
1407 """
1408 `EXECUTION` features provide metadata necessary for operation execution.
1409 """
1410 EXECUTION
1411 }
1412
1413 type Query @join__type(graph: A) @join__type(graph: B) {
1414 users: [User!]! @join__field(graph: A)
1415 accounts: [Account!]! @join__field(graph: B)
1416 }
1417
1418 type User @join__type(graph: A) @join__type(graph: B, key: "id") {
1419 id: ID @join__field(graph: A, type: "ID") @join__field(graph: B, type: "ID!")
1420 name: String @join__field(graph: B)
1421 similarAccounts: [Account!]! @join__field(graph: B)
1422 }
1423 "###;
1424
1425 let actual = crate::render_sdl::render_federated_sdl(&FederatedGraph::from_sdl(sdl).unwrap()).unwrap();
1426
1427 insta::assert_snapshot!(
1428 &actual,
1429 @r#"
1430 directive @join__enumValue(graph: join__Graph!) on ENUM_VALUE
1431
1432 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1433
1434 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1435
1436 directive @join__implements(graph: join__Graph!, interface: String!) on OBJECT | INTERFACE
1437
1438 directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true, isInterfaceObject: Boolean! = false) on SCALAR | OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT
1439
1440 directive @join__unionMember(graph: join__Graph!, member: String!) on UNION
1441
1442 directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) on SCHEMA
1443
1444 scalar join__FieldSet
1445
1446 scalar link__Import
1447
1448 type Admin
1449 @join__type(graph: B)
1450 {
1451 id: ID
1452 name: String
1453 similarAccounts: [Account!]!
1454 }
1455
1456 type Query
1457 @join__type(graph: A)
1458 @join__type(graph: B)
1459 {
1460 users: [User!]! @join__field(graph: A)
1461 accounts: [Account!]! @join__field(graph: B)
1462 }
1463
1464 type User
1465 @join__type(graph: A)
1466 @join__type(graph: B, key: "id")
1467 {
1468 id: ID @join__field(graph: A, type: "ID") @join__field(graph: B, type: "ID!")
1469 name: String @join__field(graph: B)
1470 similarAccounts: [Account!]! @join__field(graph: B)
1471 }
1472
1473 enum join__Graph
1474 {
1475 A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1476 B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1477 }
1478
1479 enum link__Purpose
1480 {
1481 """
1482 `SECURITY` features provide metadata necessary to securely resolve fields.
1483 """
1484 SECURITY
1485 """
1486 `EXECUTION` features provide metadata necessary for operation execution.
1487 """
1488 EXECUTION
1489 }
1490
1491 union Account
1492 @join__type(graph: B)
1493 @join__unionMember(graph: B, member: "User")
1494 @join__unionMember(graph: B, member: "Admin")
1495 = User | Admin
1496 "#);
1497}
1498
1499#[cfg(test)]
1500#[tokio::test]
1501async fn load_with_extensions() {
1502 let sdl = r###"
1503 directive @join__type(
1504 graph: join__Graph!
1505 key: join__FieldSet
1506 resolvable: Boolean = true
1507 ) repeatable on OBJECT | INTERFACE
1508
1509 directive @join__field(
1510 graph: join__Graph
1511 requires: join__FieldSet
1512 provides: join__FieldSet
1513 ) on FIELD_DEFINITION
1514
1515 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1516
1517 scalar join__FieldSet
1518
1519 enum join__Graph {
1520 A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1521 B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1522 }
1523
1524 enum extension__Link {
1525 REST @extension__link(url: "file:///dummy", schemaDirectives: [{graph: A, name: "test" arguments: {method: "yes"}}])
1526 }
1527
1528 scalar link__Import
1529
1530 type Query @join__type(graph: A) {
1531 users: [User!]! @join__field(graph: A) @extension__directive(graph: A, extension: REST, name: "rest", arguments: { method: GET })
1532 }
1533
1534 type User @join__type(graph: A) {
1535 id: ID!
1536 }
1537 "###;
1538
1539 let rendered_sdl = crate::render_sdl::render_federated_sdl(&FederatedGraph::from_sdl(sdl).unwrap()).unwrap();
1540
1541 insta::assert_snapshot!(rendered_sdl, @r#"
1542 directive @join__type(graph: join__Graph!, key: join__FieldSet, resolvable: Boolean = true) on OBJECT | INTERFACE
1543
1544 directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION
1545
1546 directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1547
1548 scalar join__FieldSet
1549
1550 scalar link__Import
1551
1552 type Query
1553 @join__type(graph: A)
1554 {
1555 users: [User!]! @extension__directive(graph: A, extension: REST, name: "rest", arguments: {method: GET})
1556 }
1557
1558 type User
1559 @join__type(graph: A)
1560 {
1561 id: ID!
1562 }
1563
1564 enum join__Graph
1565 {
1566 A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1567 B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1568 }
1569
1570 enum extension__Link
1571 {
1572 REST @extension__link(url: "file:///dummy", schemaDirectives: [{graph: A, name: "test", arguments: {method: "yes"}}])
1573 }
1574 "#);
1575}