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