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