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