graphql_composition/federated_graph/
from_sdl.rs

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