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