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