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