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