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    if ast_field.name() == "__typename" {
707        return Ok(Selection::Typename);
708    }
709    let field_id: FieldId = *state.selection_map.get(&(target, ast_field.name())).ok_or_else(|| {
710        DomainError(format!(
711            "Field '{}.{}' does not exist",
712            state.get_definition_name(target),
713            ast_field.name(),
714        ))
715    })?;
716    let field_ty = state.graph[field_id].r#type.definition;
717    let arguments = ast_field
718        .arguments()
719        .map(|argument| {
720            let name = state.insert_string(argument.name());
721            let (start, len) = state.graph[field_id].arguments;
722            let arguments = &state.graph.input_value_definitions[usize::from(start)..usize::from(start) + len];
723            let argument_id = arguments
724                .iter()
725                .position(|arg| arg.name == name)
726                .map(|idx| InputValueDefinitionId::from(usize::from(start) + idx))
727                .expect("unknown argument");
728
729            let argument_type = state.graph.input_value_definitions[usize::from(argument_id)]
730                .r#type
731                .definition
732                .as_enum();
733
734            let const_value = argument
735                .value()
736                .try_into()
737                .map_err(|_| DomainError("FieldSets cant contain variables".into()))?;
738
739            let value = state.insert_value(const_value, argument_type);
740
741            Ok((argument_id, value))
742        })
743        .collect::<Result<_, _>>()?;
744
745    Ok(Selection::Field(FieldSelection {
746        field_id,
747        arguments,
748        subselection: attach_selection_set_rec(ast_field.selection_set(), field_ty, state)?,
749    }))
750}
751
752fn attach_inline_fragment(
753    inline_fragment: executable_ast::InlineFragment<'_>,
754    state: &mut State<'_>,
755) -> Result<Selection, DomainError> {
756    let on: Definition = match inline_fragment.type_condition() {
757        Some(type_name) => *state
758            .definition_names
759            .get(type_name)
760            .ok_or_else(|| DomainError(format!("Type '{type_name}' in type condition does not exist")))?,
761        None => {
762            return Err(DomainError(
763                "Fragments without type condition are not supported".to_owned(),
764            ));
765        }
766    };
767
768    let subselection = attach_selection_set_rec(inline_fragment.selection_set(), on, state)?;
769
770    Ok(Selection::InlineFragment { on, subselection })
771}
772
773fn attach_input_value_set_to_field_arguments(
774    selection_set: executable_ast::ExecutableDocument,
775    parent: Definition,
776    field_id: FieldId,
777    state: &mut State<'_>,
778) -> Result<InputValueDefinitionSet, DomainError> {
779    let operation = selection_set
780        .operations()
781        .next()
782        .expect("first operation is there by construction");
783
784    attach_input_value_set_to_field_arguments_rec(operation.selection_set(), parent, field_id, state)
785}
786
787fn attach_input_value_set_to_field_arguments_rec<'a>(
788    selection_set: impl Iterator<Item = executable_ast::Selection<'a>>,
789    parent: Definition,
790    field_id: FieldId,
791    state: &mut State<'_>,
792) -> Result<InputValueDefinitionSet, DomainError> {
793    let (start, len) = state.graph[field_id].arguments;
794    selection_set
795        .map(|selection| {
796            let executable_ast::Selection::Field(ast_arg) = selection else {
797                return Err(DomainError("Unsupported fragment spread in selection set".to_owned()));
798            };
799
800            let arguments = &state.graph.input_value_definitions[usize::from(start)..usize::from(start) + len];
801            let Some((i, arg)) = arguments
802                .iter()
803                .enumerate()
804                .find(|(_, arg)| state.strings.get_index(usize::from(arg.name)).unwrap() == ast_arg.name())
805            else {
806                return Err(DomainError(format!(
807                    "Argument '{}' does not exist for the field '{}.{}'",
808                    ast_arg.name(),
809                    state.get_definition_name(parent),
810                    state
811                        .strings
812                        .get_index(usize::from(state.graph[field_id].name))
813                        .unwrap(),
814                )));
815            };
816
817            let mut ast_subselection = ast_arg.selection_set().peekable();
818
819            let subselection = if let Definition::InputObject(input_object_id) = arg.r#type.definition {
820                if ast_subselection.peek().is_none() {
821                    return Err(DomainError("InputObject must have a subselection".to_owned()));
822                }
823                attach_input_value_set_rec(ast_subselection, input_object_id, state)?
824            } else if ast_subselection.peek().is_some() {
825                return Err(DomainError("Only InputObject can have a subselection".to_owned()));
826            } else {
827                InputValueDefinitionSet::default()
828            };
829
830            Ok(InputValueDefinitionSetItem {
831                input_value_definition: InputValueDefinitionId::from(usize::from(start) + i),
832                subselection,
833            })
834        })
835        .collect()
836}
837
838fn attach_input_value_set_rec<'a>(
839    selection_set: impl Iterator<Item = executable_ast::Selection<'a>>,
840    input_object_id: InputObjectId,
841    state: &mut State<'_>,
842) -> Result<InputValueDefinitionSet, DomainError> {
843    selection_set
844        .map(|selection| {
845            let executable_ast::Selection::Field(ast_field) = selection else {
846                return Err(DomainError("Unsupported fragment spread in selection set".to_owned()));
847            };
848            let id = *state
849                .input_values_map
850                .get(&(input_object_id, ast_field.name()))
851                .ok_or_else(|| {
852                    DomainError(format!(
853                        "Input field '{}.{}' does not exist",
854                        state.get_definition_name(Definition::InputObject(input_object_id)),
855                        ast_field.name(),
856                    ))
857                })?;
858
859            let mut ast_subselection = ast_field.selection_set().peekable();
860
861            let subselection = if let Definition::InputObject(input_object_id) =
862                state.graph.input_value_definitions[usize::from(id)].r#type.definition
863            {
864                if ast_subselection.peek().is_none() {
865                    return Err(DomainError("InputObject must have a subselection".to_owned()));
866                }
867                attach_input_value_set_rec(ast_subselection, input_object_id, state)?
868            } else if ast_subselection.peek().is_some() {
869                return Err(DomainError("Only InputObject can have a subselection".to_owned()));
870            } else {
871                InputValueDefinitionSet::default()
872            };
873
874            Ok(InputValueDefinitionSetItem {
875                input_value_definition: id,
876                subselection,
877            })
878        })
879        .collect()
880}
881
882fn ingest_join_graph_enum<'a>(
883    namespace: Option<StringId>,
884    type_name_id: StringId,
885    description: Option<StringId>,
886    type_name: &'a str,
887    enm: ast::EnumDefinition<'a>,
888    state: &mut State<'a>,
889) -> Result<(), DomainError> {
890    let enum_definition_id = ingest_enum_definition(namespace, type_name_id, description, type_name, enm, state)?;
891
892    for value in enm.values() {
893        let sdl_name = value.value();
894        let directive = value
895            .directives()
896            .find(|directive| directive.name() == JOIN_GRAPH_DIRECTIVE_NAME)
897            .ok_or_else(|| DomainError("Missing @join__graph directive on join__Graph enum value.".to_owned()))?;
898        let name = directive
899            .get_argument("name")
900            .ok_or_else(|| {
901                DomainError(
902                    "Missing `name` argument in `@join__graph` directive on `join__Graph` enum value.".to_owned(),
903                )
904            })
905            .and_then(|arg| match arg {
906                ParserValue::String(s) => Ok(s),
907                _ => Err(DomainError(
908                    "Unexpected type for `name` argument in `@join__graph` directive on `join__Graph` enum value."
909                        .to_owned(),
910                )),
911            })?;
912        let url = directive
913            .get_argument("url")
914            .map(|arg| match arg {
915                ParserValue::String(s) => Ok(s),
916                _ => Err(DomainError(
917                    "Unexpected type for `url` argument in `@join__graph` directive on `join__Graph` enum value."
918                        .to_owned(),
919                )),
920            })
921            .transpose()?;
922
923        let subgraph_name = state.insert_string(name.value());
924        let url = url.map(|url| state.insert_string(url.value()));
925        let sdl_name_string_id = state.insert_string(sdl_name);
926        let join_graph_enum_value_name = state
927            .graph
928            .iter_enum_values(enum_definition_id)
929            .find(|value| value.value == sdl_name_string_id)
930            .unwrap()
931            .id();
932
933        let id = SubgraphId::from(state.graph.subgraphs.push_return_idx(Subgraph {
934            name: subgraph_name,
935            join_graph_enum_value: join_graph_enum_value_name,
936            url,
937        }));
938        state.graph_by_enum_str.insert(sdl_name, id);
939        state.graph_by_name.insert(name.value(), id);
940    }
941
942    Ok(())
943}
944
945fn ingest_extension_link_enum<'a>(
946    namespace: Option<StringId>,
947    type_name_id: StringId,
948    description: Option<StringId>,
949    type_name: &'a str,
950    enm: ast::EnumDefinition<'a>,
951    state: &mut State<'a>,
952) -> Result<(), DomainError> {
953    use directive::{ExtensionLink, parse_extension_link};
954
955    let enum_definition_id = state.graph.push_enum_definition(EnumDefinitionRecord {
956        namespace,
957        name: type_name_id,
958        directives: Vec::new(),
959        description,
960    });
961
962    state
963        .definition_names
964        .insert(type_name, Definition::Enum(enum_definition_id));
965
966    for value in enm.values() {
967        let description = value
968            .description()
969            .map(|description| state.insert_string(&description.to_cow()));
970
971        let directive = value
972            .directives()
973            .find(|directive| directive.name() == EXTENSION_LINK_DIRECTIVE)
974            .ok_or_else(|| {
975                DomainError(format!(
976                    "Missing @{EXTENSION_LINK_DIRECTIVE} directive on {EXTENSION_LINK_ENUM} enum value."
977                ))
978            })?;
979
980        let ExtensionLink { url, schema_directives } = parse_extension_link(directive, state)?;
981        let url = state.insert_string(&url);
982
983        let value_string_id = state.insert_string(value.value());
984        let enum_value_id = state.graph.push_enum_value(EnumValueRecord {
985            enum_id: enum_definition_id,
986            value: value_string_id,
987            directives: Vec::new(),
988            description,
989        });
990
991        state
992            .enum_values_map
993            .insert((enum_definition_id, value.value()), enum_value_id);
994
995        let extension_id = state.graph.push_extension(Extension {
996            url,
997            enum_value_id,
998            schema_directives,
999        });
1000
1001        state.extension_by_enum_value_str.insert(value.value(), extension_id);
1002    }
1003
1004    state.extensions_loaded = true;
1005
1006    Ok(())
1007}
1008
1009trait VecExt<T> {
1010    fn push_return_idx(&mut self, elem: T) -> usize;
1011}
1012
1013impl<T> VecExt<T> for Vec<T> {
1014    fn push_return_idx(&mut self, elem: T) -> usize {
1015        let idx = self.len();
1016        self.push(elem);
1017        idx
1018    }
1019}
1020
1021#[cfg(test)]
1022#[test]
1023fn test_from_sdl() {
1024    // https://github.com/the-guild-org/gateways-benchmark/blob/main/federation-v1/gateways/apollo-router/supergraph.graphql
1025    FederatedGraph::from_sdl(r#"
1026        schema
1027          @link(url: "https://specs.apollo.dev/link/v1.0")
1028          @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1029        {
1030          query: Query
1031        }
1032
1033        directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1034
1035        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
1036
1037        directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1038
1039        directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1040
1041        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
1042
1043        directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1044
1045        directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1046
1047        scalar join__FieldSet
1048
1049        enum join__Graph {
1050          ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql")
1051          INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql")
1052          PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql")
1053          REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql")
1054        }
1055
1056        scalar link__Import
1057
1058        enum link__Purpose {
1059          """
1060          `SECURITY` features provide metadata necessary to securely resolve fields.
1061          """
1062          SECURITY
1063
1064          """
1065          `EXECUTION` features provide metadata necessary for operation execution.
1066          """
1067          EXECUTION
1068        }
1069
1070        type Product
1071          @join__type(graph: INVENTORY, key: "upc")
1072          @join__type(graph: PRODUCTS, key: "upc")
1073          @join__type(graph: REVIEWS, key: "upc")
1074        {
1075          upc: String!
1076          weight: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS)
1077          price: Int @join__field(graph: INVENTORY, external: true) @join__field(graph: PRODUCTS)
1078          inStock: Boolean @join__field(graph: INVENTORY)
1079          shippingEstimate: Int @join__field(graph: INVENTORY, requires: "price weight")
1080          name: String @join__field(graph: PRODUCTS)
1081          reviews: [Review] @join__field(graph: REVIEWS)
1082        }
1083
1084        type Query
1085          @join__type(graph: ACCOUNTS)
1086          @join__type(graph: INVENTORY)
1087          @join__type(graph: PRODUCTS)
1088          @join__type(graph: REVIEWS)
1089        {
1090          me: User @join__field(graph: ACCOUNTS)
1091          user(id: ID!): User @join__field(graph: ACCOUNTS)
1092          users: [User] @join__field(graph: ACCOUNTS)
1093          topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
1094        }
1095
1096        type Review
1097          @join__type(graph: REVIEWS, key: "id")
1098        {
1099          id: ID!
1100          body: String
1101          product: Product
1102          author: User @join__field(graph: REVIEWS, provides: "username")
1103        }
1104
1105        type User
1106          @join__type(graph: ACCOUNTS, key: "id")
1107          @join__type(graph: REVIEWS, key: "id")
1108        {
1109          id: ID!
1110          name: String @join__field(graph: ACCOUNTS)
1111          username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true)
1112          birthday: Int @join__field(graph: ACCOUNTS)
1113          reviews: [Review] @join__field(graph: REVIEWS)
1114        }
1115    "#).unwrap();
1116}
1117
1118#[cfg(test)]
1119#[test]
1120fn test_from_sdl_with_empty_query_root() {
1121    // https://github.com/the-guild-org/gateways-benchmark/blob/main/federation-v1/gateways/apollo-router/supergraph.graphql
1122    FederatedGraph::from_sdl(
1123        r#"
1124        schema
1125          @link(url: "https://specs.apollo.dev/link/v1.0")
1126          @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1127        {
1128          query: Query
1129        }
1130
1131        directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1132
1133        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
1134
1135        directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1136
1137        directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1138
1139        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
1140
1141        directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1142
1143        directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1144
1145        scalar join__FieldSet
1146
1147        enum join__Graph {
1148          ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql")
1149          INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql")
1150          PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql")
1151          REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql")
1152        }
1153
1154        scalar link__Import
1155
1156        enum link__Purpose {
1157          """
1158          `SECURITY` features provide metadata necessary to securely resolve fields.
1159          """
1160          SECURITY
1161
1162          """
1163          `EXECUTION` features provide metadata necessary for operation execution.
1164          """
1165          EXECUTION
1166        }
1167
1168        type Query
1169
1170        type User
1171          @join__type(graph: ACCOUNTS, key: "id")
1172          @join__type(graph: REVIEWS, key: "id")
1173        {
1174          id: ID!
1175          name: String @join__field(graph: ACCOUNTS)
1176          username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true)
1177          birthday: Int @join__field(graph: ACCOUNTS)
1178          reviews: [Review] @join__field(graph: REVIEWS)
1179        }
1180
1181        type Review
1182          @join__type(graph: REVIEWS, key: "id")
1183        {
1184          id: ID!
1185          body: String
1186          author: User @join__field(graph: REVIEWS, provides: "username")
1187        }
1188    "#,
1189    ).unwrap();
1190}
1191
1192#[cfg(test)]
1193#[test]
1194fn test_from_sdl_with_missing_query_root() {
1195    // https://github.com/the-guild-org/gateways-benchmark/blob/main/federation-v1/gateways/apollo-router/supergraph.graphql
1196    FederatedGraph::from_sdl(
1197        r#"
1198        schema
1199          @link(url: "https://specs.apollo.dev/link/v1.0")
1200          @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
1201        {
1202          query: Query
1203        }
1204
1205        directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1206
1207        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
1208
1209        directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1210
1211        directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE
1212
1213        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
1214
1215        directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
1216
1217        directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
1218
1219        scalar join__FieldSet
1220
1221        enum join__Graph {
1222          ACCOUNTS @join__graph(name: "accounts", url: "http://accounts:4001/graphql")
1223          INVENTORY @join__graph(name: "inventory", url: "http://inventory:4002/graphql")
1224          PRODUCTS @join__graph(name: "products", url: "http://products:4003/graphql")
1225          REVIEWS @join__graph(name: "reviews", url: "http://reviews:4004/graphql")
1226        }
1227
1228        scalar link__Import
1229
1230        enum link__Purpose {
1231          """
1232          `SECURITY` features provide metadata necessary to securely resolve fields.
1233          """
1234          SECURITY
1235
1236          """
1237          `EXECUTION` features provide metadata necessary for operation execution.
1238          """
1239          EXECUTION
1240        }
1241
1242        type Review
1243          @join__type(graph: REVIEWS, key: "id")
1244        {
1245          id: ID!
1246          body: String
1247          author: User @join__field(graph: REVIEWS, provides: "username")
1248        }
1249
1250        type User
1251          @join__type(graph: ACCOUNTS, key: "id")
1252          @join__type(graph: REVIEWS, key: "id")
1253        {
1254          id: ID!
1255          name: String @join__field(graph: ACCOUNTS)
1256          username: String @join__field(graph: ACCOUNTS) @join__field(graph: REVIEWS, external: true)
1257          birthday: Int @join__field(graph: ACCOUNTS)
1258          reviews: [Review] @join__field(graph: REVIEWS)
1259        }
1260    "#,
1261    ).unwrap();
1262}
1263
1264pub(crate) fn split_namespace_name(original_name: &str, state: &mut State<'_>) -> (Option<StringId>, StringId) {
1265    match original_name.split_once("__") {
1266        Some((namespace, name)) => {
1267            let namespace = state.insert_string(namespace);
1268            let name = state.insert_string(name);
1269
1270            (Some(namespace), name)
1271        }
1272        None => (None, state.insert_string(original_name)),
1273    }
1274}
1275
1276#[cfg(test)]
1277#[test]
1278fn test_missing_type() {
1279    let sdl = r###"
1280    directive @core(feature: String!) repeatable on SCHEMA
1281
1282    directive @join__owner(graph: join__Graph!) on OBJECT
1283
1284    directive @join__type(
1285        graph: join__Graph!
1286        key: String!
1287        resolvable: Boolean = true
1288    ) repeatable on OBJECT | INTERFACE
1289
1290    directive @join__field(
1291        graph: join__Graph
1292        requires: String
1293        provides: String
1294    ) on FIELD_DEFINITION
1295
1296    directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1297
1298    enum join__Graph {
1299        MANGROVE @join__graph(name: "mangrove", url: "http://example.com/mangrove")
1300        STEPPE @join__graph(name: "steppe", url: "http://example.com/steppe")
1301    }
1302
1303    type Query {
1304        getMammoth: Mammoth @join__field(graph: mangrove)
1305    }
1306    "###;
1307    let actual = FederatedGraph::from_sdl(sdl);
1308    assert!(actual.is_err());
1309}
1310
1311#[cfg(test)]
1312#[test]
1313fn test_join_field_type() {
1314    let sdl = r###"
1315    schema
1316      @link(url: "https://specs.apollo.dev/link/v1.0")
1317      @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION) {
1318      query: Query
1319    }
1320
1321    directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
1322
1323    directive @join__field(
1324      graph: join__Graph
1325      requires: join__FieldSet
1326      provides: join__FieldSet
1327      type: String
1328      external: Boolean
1329      override: String
1330      usedOverridden: Boolean
1331    ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION
1332
1333    directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1334
1335    directive @join__implements(
1336      graph: join__Graph!
1337      interface: String!
1338    ) repeatable on OBJECT | INTERFACE
1339
1340    directive @join__type(
1341      graph: join__Graph!
1342      key: join__FieldSet
1343      extension: Boolean! = false
1344      resolvable: Boolean! = true
1345      isInterfaceObject: Boolean! = false
1346    ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
1347
1348    directive @join__unionMember(
1349      graph: join__Graph!
1350      member: String!
1351    ) repeatable on UNION
1352
1353    directive @link(
1354      url: String
1355      as: String
1356      for: link__Purpose
1357      import: [link__Import]
1358    ) repeatable on SCHEMA
1359
1360    union Account
1361      @join__type(graph: B)
1362      @join__unionMember(graph: B, member: "User")
1363      @join__unionMember(graph: B, member: "Admin") =
1364      | User
1365      | Admin
1366
1367    type Admin @join__type(graph: B) {
1368      id: ID
1369      name: String
1370      similarAccounts: [Account!]!
1371    }
1372
1373    scalar join__FieldSet
1374
1375    enum join__Graph {
1376      A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1377      B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1378    }
1379
1380    scalar link__Import
1381
1382    enum link__Purpose {
1383      """
1384      `SECURITY` features provide metadata necessary to securely resolve fields.
1385      """
1386      SECURITY
1387
1388      """
1389      `EXECUTION` features provide metadata necessary for operation execution.
1390      """
1391      EXECUTION
1392    }
1393
1394    type Query @join__type(graph: A) @join__type(graph: B) {
1395      users: [User!]! @join__field(graph: A)
1396      accounts: [Account!]! @join__field(graph: B)
1397    }
1398
1399    type User @join__type(graph: A) @join__type(graph: B, key: "id") {
1400      id: ID @join__field(graph: A, type: "ID") @join__field(graph: B, type: "ID!")
1401      name: String @join__field(graph: B)
1402      similarAccounts: [Account!]! @join__field(graph: B)
1403    }
1404    "###;
1405
1406    let actual =
1407        crate::federated_graph::render_sdl::render_federated_sdl(&FederatedGraph::from_sdl(sdl).unwrap()).unwrap();
1408
1409    insta::assert_snapshot!(
1410        &actual,
1411        @r#"
1412        directive @join__enumValue(graph: join__Graph!) on ENUM_VALUE
1413
1414        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
1415
1416        directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1417
1418        directive @join__implements(graph: join__Graph!, interface: String!) on OBJECT | INTERFACE
1419
1420        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
1421
1422        directive @join__unionMember(graph: join__Graph!, member: String!) on UNION
1423
1424        directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) on SCHEMA
1425
1426        scalar join__FieldSet
1427
1428        scalar link__Import
1429
1430        type Admin
1431          @join__type(graph: B)
1432        {
1433          id: ID
1434          name: String
1435          similarAccounts: [Account!]!
1436        }
1437
1438        type Query
1439          @join__type(graph: A)
1440          @join__type(graph: B)
1441        {
1442          users: [User!]! @join__field(graph: A)
1443          accounts: [Account!]! @join__field(graph: B)
1444        }
1445
1446        type User
1447          @join__type(graph: A)
1448          @join__type(graph: B, key: "id")
1449        {
1450          id: ID @join__field(graph: A, type: "ID") @join__field(graph: B, type: "ID!")
1451          name: String @join__field(graph: B)
1452          similarAccounts: [Account!]! @join__field(graph: B)
1453        }
1454
1455        enum join__Graph
1456        {
1457          A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1458          B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1459        }
1460
1461        enum link__Purpose
1462        {
1463          """
1464          `SECURITY` features provide metadata necessary to securely resolve fields.
1465          """
1466          SECURITY
1467          """
1468          `EXECUTION` features provide metadata necessary for operation execution.
1469          """
1470          EXECUTION
1471        }
1472
1473        union Account
1474          @join__type(graph: B)
1475          @join__unionMember(graph: B, member: "User")
1476          @join__unionMember(graph: B, member: "Admin")
1477         = User | Admin
1478    "#);
1479}
1480
1481#[cfg(test)]
1482#[tokio::test]
1483async fn load_with_extensions() {
1484    let sdl = r###"
1485        directive @join__type(
1486            graph: join__Graph!
1487            key: join__FieldSet
1488            resolvable: Boolean = true
1489        ) repeatable on OBJECT | INTERFACE
1490
1491        directive @join__field(
1492            graph: join__Graph
1493            requires: join__FieldSet
1494            provides: join__FieldSet
1495        ) on FIELD_DEFINITION
1496
1497        directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1498
1499        scalar join__FieldSet
1500
1501        enum join__Graph {
1502            A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1503            B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1504        }
1505
1506        enum extension__Link {
1507            REST @extension__link(url: "file:///dummy", schemaDirectives: [{graph: A, name: "test" arguments: {method: "yes"}}])
1508        }
1509
1510        scalar link__Import
1511
1512        type Query @join__type(graph: A) {
1513            users: [User!]! @join__field(graph: A) @extension__directive(graph: A, extension: REST, name: "rest", arguments: { method: GET })
1514        }
1515
1516        type User @join__type(graph: A) {
1517            id: ID!
1518        }
1519        "###;
1520
1521    let rendered_sdl = crate::render_federated_sdl(&FederatedGraph::from_sdl(sdl).unwrap()).unwrap();
1522
1523    insta::assert_snapshot!(rendered_sdl, @r#"
1524        directive @join__type(graph: join__Graph!, key: join__FieldSet, resolvable: Boolean = true) on OBJECT | INTERFACE
1525
1526        directive @join__field(graph: join__Graph, requires: join__FieldSet, provides: join__FieldSet) on FIELD_DEFINITION
1527
1528        directive @join__graph(name: String!, url: String!) on ENUM_VALUE
1529
1530        scalar join__FieldSet
1531
1532        scalar link__Import
1533
1534        type Query
1535          @join__type(graph: A)
1536        {
1537          users: [User!]! @extension__directive(graph: A, extension: REST, name: "rest", arguments: {method: GET})
1538        }
1539
1540        type User
1541          @join__type(graph: A)
1542        {
1543          id: ID!
1544        }
1545
1546        enum join__Graph
1547        {
1548          A @join__graph(name: "a", url: "http://localhost:4200/child-type-mismatch/a")
1549          B @join__graph(name: "b", url: "http://localhost:4200/child-type-mismatch/b")
1550        }
1551
1552        enum extension__Link
1553        {
1554          REST @extension__link(url: "file:///dummy", schemaDirectives: [{graph: A, name: "test", arguments: {method: "yes"}}])
1555        }
1556    "#);
1557}