Skip to main content

apollo_federation/subgraph/
mod.rs

1use std::fmt::Display;
2use std::fmt::Formatter;
3use std::ops::Range;
4
5use apollo_compiler::Node;
6use apollo_compiler::Schema;
7use apollo_compiler::collections::IndexMap;
8use apollo_compiler::collections::IndexSet;
9use apollo_compiler::name;
10use apollo_compiler::parser::LineColumn;
11use apollo_compiler::schema::ComponentName;
12use apollo_compiler::schema::ExtendedType;
13use apollo_compiler::schema::ObjectType;
14use apollo_compiler::validation::DiagnosticList;
15use apollo_compiler::validation::Valid;
16use indexmap::map::Entry;
17
18use crate::ValidFederationSubgraph;
19use crate::error::FederationError;
20use crate::error::MultipleFederationErrors;
21use crate::error::SingleFederationError;
22use crate::link::DEFAULT_LINK_NAME;
23use crate::link::Link;
24use crate::link::LinkError;
25use crate::link::spec::Identity;
26use crate::subgraph::spec::ANY_SCALAR_NAME;
27use crate::subgraph::spec::AppliedFederationLink;
28use crate::subgraph::spec::CONTEXTFIELDVALUE_SCALAR_NAME;
29use crate::subgraph::spec::ENTITIES_QUERY;
30use crate::subgraph::spec::ENTITY_UNION_NAME;
31use crate::subgraph::spec::FEDERATION_V2_DIRECTIVE_NAMES;
32use crate::subgraph::spec::FederationSpecDefinitions;
33use crate::subgraph::spec::KEY_DIRECTIVE_NAME;
34use crate::subgraph::spec::LinkSpecDefinitions;
35use crate::subgraph::spec::SERVICE_SDL_QUERY;
36use crate::subgraph::spec::SERVICE_TYPE;
37
38pub mod spec;
39pub mod typestate; // TODO: Move here to overwrite Subgraph after API is reasonable
40
41pub struct Subgraph {
42    pub name: String,
43    pub url: String,
44    pub schema: Schema,
45}
46
47impl Subgraph {
48    pub fn new(name: &str, url: &str, schema_str: &str) -> Result<Self, FederationError> {
49        let schema = Schema::parse(schema_str, name)?;
50        // TODO: federation-specific validation
51        Ok(Self {
52            name: name.to_string(),
53            url: url.to_string(),
54            schema,
55        })
56    }
57
58    pub fn parse_and_expand(
59        name: &str,
60        url: &str,
61        schema_str: &str,
62    ) -> Result<ValidSubgraph, FederationError> {
63        let mut schema = Schema::builder()
64            .adopt_orphan_extensions()
65            .parse(schema_str, name)
66            .build()?;
67
68        let mut imported_federation_definitions: Option<FederationSpecDefinitions> = None;
69        let mut imported_link_definitions: Option<LinkSpecDefinitions> = None;
70        let default_link_name = DEFAULT_LINK_NAME;
71        let link_directives = schema
72            .schema_definition
73            .directives
74            .get_all(&default_link_name);
75
76        for directive in link_directives {
77            let link_directive = Link::from_directive_application(directive, &schema)?;
78            if link_directive.url.identity == Identity::federation_identity() {
79                if imported_federation_definitions.is_some() {
80                    let msg = "invalid graphql schema - multiple @link imports for the federation specification are not supported";
81                    return Err(LinkError::BootstrapError(msg.to_owned()).into());
82                }
83
84                imported_federation_definitions =
85                    Some(FederationSpecDefinitions::from_link(link_directive)?);
86            } else if link_directive.url.identity == Identity::link_identity() {
87                // user manually imported @link specification
88                if imported_link_definitions.is_some() {
89                    let msg = "invalid graphql schema - multiple @link imports for the link specification are not supported";
90                    return Err(LinkError::BootstrapError(msg.to_owned()).into());
91                }
92
93                imported_link_definitions = Some(LinkSpecDefinitions::new(link_directive));
94            }
95        }
96
97        // generate additional schema definitions
98        Self::populate_missing_type_definitions(
99            &mut schema,
100            imported_federation_definitions,
101            imported_link_definitions,
102        )?;
103        let schema = schema.validate()?;
104        Ok(ValidSubgraph {
105            name: name.to_owned(),
106            url: url.to_owned(),
107            schema,
108        })
109    }
110
111    fn populate_missing_type_definitions(
112        schema: &mut Schema,
113        imported_federation_definitions: Option<FederationSpecDefinitions>,
114        imported_link_definitions: Option<LinkSpecDefinitions>,
115    ) -> Result<(), FederationError> {
116        // populate @link spec definitions
117        let link_spec_definitions = match imported_link_definitions {
118            Some(definitions) => definitions,
119            None => {
120                // need to apply default @link directive for link spec on schema
121                let defaults = LinkSpecDefinitions::default();
122                schema
123                    .schema_definition
124                    .make_mut()
125                    .directives
126                    .push(defaults.applied_link_directive());
127                defaults
128            }
129        };
130        Self::populate_missing_link_definitions(schema, link_spec_definitions)?;
131
132        // populate @link federation spec definitions
133        let fed_definitions = match imported_federation_definitions {
134            Some(definitions) => definitions,
135            None => {
136                // federation v1 schema or user does not import federation spec
137                // need to apply default @link directive for federation spec on schema
138                let defaults = FederationSpecDefinitions::default()?;
139                schema
140                    .schema_definition
141                    .make_mut()
142                    .directives
143                    .push(defaults.applied_link_directive());
144                defaults
145            }
146        };
147        Self::populate_missing_federation_directive_definitions(schema, &fed_definitions)?;
148        Self::populate_missing_federation_types(schema, &fed_definitions)
149    }
150
151    fn populate_missing_link_definitions(
152        schema: &mut Schema,
153        link_spec_definitions: LinkSpecDefinitions,
154    ) -> Result<(), FederationError> {
155        let purpose_enum_name = &link_spec_definitions.purpose_enum_name;
156        schema
157            .types
158            .entry(purpose_enum_name.clone())
159            .or_insert_with(|| {
160                link_spec_definitions
161                    .link_purpose_enum_definition(purpose_enum_name.clone())
162                    .into()
163            });
164        let import_scalar_name = &link_spec_definitions.import_scalar_name;
165        schema
166            .types
167            .entry(import_scalar_name.clone())
168            .or_insert_with(|| {
169                link_spec_definitions
170                    .import_scalar_definition(import_scalar_name.clone())
171                    .into()
172            });
173        if let Entry::Vacant(entry) = schema.directive_definitions.entry(DEFAULT_LINK_NAME) {
174            entry.insert(link_spec_definitions.link_directive_definition()?.into());
175        }
176        Ok(())
177    }
178
179    fn populate_missing_federation_directive_definitions(
180        schema: &mut Schema,
181        fed_definitions: &FederationSpecDefinitions,
182    ) -> Result<(), FederationError> {
183        // scalar FieldSet
184        let fieldset_scalar_name = &fed_definitions.fieldset_scalar_name;
185        schema
186            .types
187            .entry(fieldset_scalar_name.clone())
188            .or_insert_with(|| {
189                fed_definitions
190                    .fieldset_scalar_definition(fieldset_scalar_name.clone())
191                    .into()
192            });
193
194        // scalar ContextFieldValue
195        let namespaced_contextfieldvalue_scalar_name =
196            fed_definitions.namespaced_type_name(&CONTEXTFIELDVALUE_SCALAR_NAME, false);
197        if let Entry::Vacant(entry) = schema
198            .types
199            .entry(namespaced_contextfieldvalue_scalar_name.clone())
200        {
201            let type_definition = fed_definitions.contextfieldvalue_scalar_definition(&Some(
202                namespaced_contextfieldvalue_scalar_name,
203            ));
204            entry.insert(type_definition.into());
205        }
206
207        for directive_name in &FEDERATION_V2_DIRECTIVE_NAMES {
208            let namespaced_directive_name =
209                fed_definitions.namespaced_type_name(directive_name, true);
210            if let Entry::Vacant(entry) = schema
211                .directive_definitions
212                .entry(namespaced_directive_name.clone())
213            {
214                let directive_definition = fed_definitions.directive_definition(
215                    directive_name,
216                    &Some(namespaced_directive_name.to_owned()),
217                )?;
218                entry.insert(directive_definition.into());
219            }
220        }
221        Ok(())
222    }
223
224    fn populate_missing_federation_types(
225        schema: &mut Schema,
226        fed_definitions: &FederationSpecDefinitions,
227    ) -> Result<(), FederationError> {
228        schema
229            .types
230            .entry(SERVICE_TYPE)
231            .or_insert_with(|| fed_definitions.service_object_type_definition());
232
233        let entities = Self::locate_entities(schema, fed_definitions);
234        let entities_present = !entities.is_empty();
235        if entities_present {
236            schema
237                .types
238                .entry(ENTITY_UNION_NAME)
239                .or_insert_with(|| fed_definitions.entity_union_definition(entities));
240            schema
241                .types
242                .entry(ANY_SCALAR_NAME)
243                .or_insert_with(|| fed_definitions.any_scalar_definition());
244        }
245
246        let query_type_name = schema
247            .schema_definition
248            .make_mut()
249            .query
250            .get_or_insert(ComponentName::from(name!("Query")));
251        if let ExtendedType::Object(query_type) = schema
252            .types
253            .entry(query_type_name.name.clone())
254            .or_insert(ExtendedType::Object(Node::new(ObjectType {
255                description: None,
256                name: query_type_name.name.clone(),
257                directives: Default::default(),
258                fields: IndexMap::default(),
259                implements_interfaces: IndexSet::default(),
260            })))
261        {
262            let query_type = query_type.make_mut();
263            query_type
264                .fields
265                .entry(SERVICE_SDL_QUERY)
266                .or_insert_with(|| fed_definitions.service_sdl_query_field());
267            if entities_present {
268                // _entities(representations: [_Any!]!): [_Entity]!
269                query_type
270                    .fields
271                    .entry(ENTITIES_QUERY)
272                    .or_insert_with(|| fed_definitions.entities_query_field());
273            }
274        }
275        Ok(())
276    }
277
278    fn locate_entities(
279        schema: &mut Schema,
280        fed_definitions: &FederationSpecDefinitions,
281    ) -> IndexSet<ComponentName> {
282        let mut entities = Vec::new();
283        let immutable_type_map = schema.types.to_owned();
284        for (named_type, extended_type) in immutable_type_map.iter() {
285            let is_entity = extended_type
286                .directives()
287                .iter()
288                .find(|d| {
289                    d.name
290                        == fed_definitions
291                            .namespaced_type_name(&KEY_DIRECTIVE_NAME, true)
292                            .as_str()
293                })
294                .map(|_| true)
295                .unwrap_or(false);
296            if is_entity {
297                entities.push(named_type);
298            }
299        }
300        let entity_set: IndexSet<ComponentName> =
301            entities.iter().map(|e| ComponentName::from(*e)).collect();
302        entity_set
303    }
304}
305
306impl std::fmt::Debug for Subgraph {
307    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
308        write!(f, r#"name: {}, urL: {}"#, self.name, self.url)
309    }
310}
311
312pub struct ValidSubgraph {
313    pub name: String,
314    pub url: String,
315    pub schema: Valid<Schema>,
316}
317
318impl std::fmt::Debug for ValidSubgraph {
319    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
320        write!(f, r#"name: {}, url: {}"#, self.name, self.url)
321    }
322}
323
324impl From<ValidFederationSubgraph> for ValidSubgraph {
325    fn from(value: ValidFederationSubgraph) -> Self {
326        Self {
327            name: value.name,
328            url: value.url,
329            schema: value.schema.schema().clone(),
330        }
331    }
332}
333
334#[derive(Clone, Debug)]
335pub(crate) struct SingleSubgraphError {
336    pub(crate) error: SingleFederationError,
337    pub(crate) locations: Vec<Range<LineColumn>>,
338}
339
340/// Currently, this is making up for the fact that we don't have an equivalent of `addSubgraphToErrors`.
341/// In JS, that manipulates the underlying `GraphQLError` message to prepend the subgraph name. In Rust,
342/// it's idiomatic to have strongly typed errors which defer conversion to strings via `thiserror`, so
343/// for now we wrap the underlying error until we figure out a longer-term replacement that accounts
344/// for missing error codes and the like.
345#[derive(Clone, Debug)]
346pub struct SubgraphError {
347    pub(crate) subgraph: String,
348    pub(crate) errors: Vec<SingleSubgraphError>,
349}
350
351impl SubgraphError {
352    // Legacy constructor without locations info.
353    pub(crate) fn new_without_locations(
354        subgraph: impl Into<String>,
355        error: impl Into<FederationError>,
356    ) -> Self {
357        let subgraph = subgraph.into();
358        let error: FederationError = error.into();
359        SubgraphError {
360            subgraph,
361            errors: error
362                .errors()
363                .into_iter()
364                .map(|e| SingleSubgraphError {
365                    error: e.clone(),
366                    locations: Vec::new(),
367                })
368                .collect(),
369        }
370    }
371
372    /// Construct from a FederationError.
373    ///
374    /// Note: FederationError may hold multiple errors. In that case, all individual errors in the
375    ///       FederationError will share the same locations.
376    #[allow(dead_code)]
377    pub(crate) fn from_federation_error(
378        subgraph: impl Into<String>,
379        error: impl Into<FederationError>,
380        locations: Vec<Range<LineColumn>>,
381    ) -> Self {
382        let error: FederationError = error.into();
383        let errors = error
384            .errors()
385            .into_iter()
386            .map(|e| SingleSubgraphError {
387                error: e.clone(),
388                locations: locations.clone(),
389            })
390            .collect();
391        SubgraphError {
392            subgraph: subgraph.into(),
393            errors,
394        }
395    }
396
397    /// Constructing from GraphQL errors.
398    pub(crate) fn from_diagnostic_list(
399        subgraph: impl Into<String>,
400        errors: DiagnosticList,
401    ) -> Self {
402        let subgraph = subgraph.into();
403        SubgraphError {
404            subgraph,
405            errors: errors
406                .iter()
407                .map(|d| SingleSubgraphError {
408                    error: SingleFederationError::InvalidGraphQL {
409                        message: d.to_string(),
410                    },
411                    locations: d.line_column_range().iter().cloned().collect(),
412                })
413                .collect(),
414        }
415    }
416
417    /// Convert SubgraphError to FederationError.
418    /// * WARNING: This is a lossy conversion, losing location information.
419    pub(crate) fn into_federation_error(self) -> FederationError {
420        MultipleFederationErrors::from_iter(self.errors.into_iter().map(|e| e.error)).into()
421    }
422
423    // Format subgraph errors in the same way as `Rover` does.
424    // And return them as a vector of (error_code, error_message) tuples
425    // - Gather associated errors from the validation error.
426    // - Split each error into its code and message.
427    // - Add the subgraph name prefix to CompositionError message.
428    //
429    // This is mainly for internal testing. Consider using `to_composition_errors` method instead.
430    pub fn format_errors(&self) -> Vec<(String, String)> {
431        self.errors
432            .iter()
433            .map(|e| {
434                let error = &e.error;
435                (
436                    error.code_string(),
437                    format!("[{subgraph}] {error}", subgraph = self.subgraph),
438                )
439            })
440            .collect()
441    }
442}
443
444impl Display for SubgraphError {
445    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
446        for (code, message) in self.format_errors() {
447            writeln!(f, "{code} {message}")?;
448        }
449        Ok(())
450    }
451}
452
453pub mod test_utils {
454
455    use super::SubgraphError;
456    use super::typestate::Expanded;
457    use super::typestate::Subgraph;
458    use super::typestate::Validated;
459
460    pub enum BuildOption {
461        AsIs,
462        AsFed2,
463    }
464
465    pub fn build_inner(
466        schema_str: &str,
467        build_option: BuildOption,
468    ) -> Result<Subgraph<Validated>, SubgraphError> {
469        let name = "S";
470        let subgraph =
471            Subgraph::parse(name, &format!("http://{name}"), schema_str).expect("valid schema");
472        let subgraph = if matches!(build_option, BuildOption::AsFed2) {
473            subgraph.into_fed2_test_subgraph(true)?
474        } else {
475            subgraph
476        };
477        Ok(subgraph
478            .expand_links()?
479            .normalize_root_types()?
480            .assume_validated())
481    }
482
483    pub fn build_inner_expanded(
484        schema_str: &str,
485        build_option: BuildOption,
486    ) -> Result<Subgraph<Expanded>, SubgraphError> {
487        let name = "S";
488        let subgraph =
489            Subgraph::parse(name, &format!("http://{name}"), schema_str).expect("valid schema");
490        let subgraph = if matches!(build_option, BuildOption::AsFed2) {
491            subgraph.into_fed2_test_subgraph(true)?
492        } else {
493            subgraph
494        };
495        subgraph.expand_links_without_validation()
496    }
497
498    pub fn build_and_validate(schema_str: &str) -> Subgraph<Validated> {
499        build_inner(schema_str, BuildOption::AsIs).expect("expanded subgraph to be valid")
500    }
501
502    pub fn build_and_expand(schema_str: &str) -> Subgraph<Expanded> {
503        build_inner_expanded(schema_str, BuildOption::AsIs).expect("expanded subgraph to be valid")
504    }
505
506    pub fn build_for_errors_with_option(
507        schema: &str,
508        build_option: BuildOption,
509    ) -> Vec<(String, String)> {
510        build_inner(schema, build_option)
511            .expect_err("subgraph error was expected")
512            .format_errors()
513    }
514
515    /// Build subgraph expecting errors, assuming fed 2.
516    pub fn build_for_errors(schema: &str) -> Vec<(String, String)> {
517        build_for_errors_with_option(schema, BuildOption::AsFed2)
518    }
519
520    pub fn remove_indentation(s: &str) -> String {
521        // count the last lines that are space-only
522        let first_empty_lines = s.lines().take_while(|line| line.trim().is_empty()).count();
523        let last_empty_lines = s
524            .lines()
525            .rev()
526            .take_while(|line| line.trim().is_empty())
527            .count();
528
529        // lines without the space-only first/last lines
530        let lines = s
531            .lines()
532            .skip(first_empty_lines)
533            .take(s.lines().count() - first_empty_lines - last_empty_lines);
534
535        // compute the indentation
536        let indentation = lines
537            .clone()
538            .map(|line| line.chars().take_while(|c| *c == ' ').count())
539            .min()
540            .unwrap_or(0);
541
542        // remove the indentation
543        lines
544            .map(|line| {
545                line.trim_end()
546                    .chars()
547                    .skip(indentation)
548                    .collect::<String>()
549            })
550            .collect::<Vec<_>>()
551            .join("\n")
552    }
553
554    /// True if a and b contain the same error messages
555    pub fn check_errors(a: &[(String, String)], b: &[(&str, &str)]) -> Result<(), String> {
556        if a.len() != b.len() {
557            return Err(format!(
558                "Mismatched error counts: {} != {}\n\nexpected:\n{}\n\nactual:\n{}",
559                b.len(),
560                a.len(),
561                b.iter()
562                    .map(|(code, msg)| { format!("- {code}: {msg}") })
563                    .collect::<Vec<_>>()
564                    .join("\n"),
565                a.iter()
566                    .map(|(code, msg)| { format!("+ {code}: {msg}") })
567                    .collect::<Vec<_>>()
568                    .join("\n"),
569            ));
570        }
571
572        // remove indentations from messages to ignore indentation differences
573        let b_iter = b
574            .iter()
575            .map(|(code, message)| (*code, remove_indentation(message)));
576        let diff: Vec<_> = a
577            .iter()
578            .map(|(code, message)| (code.as_str(), remove_indentation(message)))
579            .zip(b_iter)
580            .filter(|(a_i, b_i)| a_i.0 != b_i.0 || a_i.1 != b_i.1)
581            .collect();
582        if diff.is_empty() {
583            Ok(())
584        } else {
585            Err(format!(
586                "Mismatched errors:\n{}\n",
587                diff.iter()
588                    .map(|(a_i, b_i)| { format!("- {}: {}\n+ {}: {}", b_i.0, b_i.1, a_i.0, a_i.1) })
589                    .collect::<Vec<_>>()
590                    .join("\n")
591            ))
592        }
593    }
594
595    #[macro_export]
596    macro_rules! assert_errors {
597        ($a:expr, $b:expr) => {
598            match apollo_federation::subgraph::test_utils::check_errors(&$a, &$b) {
599                Ok(()) => {
600                    // Success
601                }
602                Err(e) => {
603                    panic!("{e}")
604                }
605            }
606        };
607    }
608}
609
610// INTERNAL: For use by Language Server Protocol (LSP) team
611// WARNING: Any changes to this function signature will result in breakages in the dependency chain
612// Generates a diff string containing directives and types not included in initial schema string
613pub fn schema_diff_expanded_from_initial(schema_str: String) -> Result<String, FederationError> {
614    // Parse schema string as Schema without validation.
615    let initial_schema = Schema::parse(schema_str, "")?;
616
617    // Initialize and expand subgraph without validation
618    let initial_subgraph =
619        typestate::Subgraph::new("S", "http://S", initial_schema.clone(), Default::default());
620    let expanded_subgraph = initial_subgraph
621        .map_err(|e| e.into_federation_error())?
622        .expand_links_without_validation()
623        .map_err(|e| e.into_federation_error())?;
624
625    // Build string of missing directives and types from initial to expanded
626    let mut diff = String::new();
627
628    // Push newly added directives onto diff
629    for (dir_name, dir_def) in &expanded_subgraph.schema().schema().directive_definitions {
630        if !initial_schema.directive_definitions.contains_key(dir_name) {
631            diff.push_str(&dir_def.to_string());
632            diff.push('\n');
633        }
634    }
635
636    // Push newly added types onto diff
637    for (named_ty, extended_ty) in &expanded_subgraph.schema().schema().types {
638        if !initial_schema.types.contains_key(named_ty) {
639            diff.push_str(&extended_ty.to_string());
640        }
641    }
642
643    Ok(diff)
644}
645
646#[cfg(test)]
647mod tests {
648    use crate::subgraph::schema_diff_expanded_from_initial;
649
650    #[test]
651    fn returns_correct_schema_diff_for_fed_2_0() {
652        let schema_string = r#"
653                extend schema @link(url: "https://specs.apollo.dev/federation/v2.0")
654
655                type Query {
656                    s: String
657                }"#
658        .to_string();
659
660        let diff = schema_diff_expanded_from_initial(schema_string);
661
662        insta::assert_snapshot!(diff.unwrap_or_default(), @r#"directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
663directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
664directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
665directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
666directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
667directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
668directive @federation__extends on OBJECT | INTERFACE
669directive @federation__shareable on OBJECT | FIELD_DEFINITION
670directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
671directive @federation__override(from: String!) on FIELD_DEFINITION
672enum link__Purpose {
673  """
674  `SECURITY` features provide metadata necessary to securely resolve fields.
675  """
676  SECURITY
677  """
678  `EXECUTION` features provide metadata necessary for operation execution.
679  """
680  EXECUTION
681}
682scalar link__Import
683scalar federation__FieldSet
684scalar _Any
685type _Service {
686  sdl: String
687}"#);
688    }
689
690    #[test]
691    fn returns_correct_schema_diff_for_fed_2_4() {
692        let schema_string = r#"
693                extend schema @link(url: "https://specs.apollo.dev/federation/v2.4")
694
695                type Query {
696                    s: String
697                }"#
698        .to_string();
699
700        let diff = schema_diff_expanded_from_initial(schema_string);
701
702        insta::assert_snapshot!(diff.unwrap_or_default(), @r#"directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
703directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
704directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
705directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
706directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
707directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
708directive @federation__extends on OBJECT | INTERFACE
709directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION
710directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
711directive @federation__override(from: String!) on FIELD_DEFINITION
712directive @federation__composeDirective(name: String) repeatable on SCHEMA
713directive @federation__interfaceObject on OBJECT
714enum link__Purpose {
715  """
716  `SECURITY` features provide metadata necessary to securely resolve fields.
717  """
718  SECURITY
719  """
720  `EXECUTION` features provide metadata necessary for operation execution.
721  """
722  EXECUTION
723}
724scalar link__Import
725scalar federation__FieldSet
726scalar _Any
727type _Service {
728  sdl: String
729}"#);
730    }
731
732    #[test]
733    fn returns_correct_schema_diff_for_fed_2_9() {
734        let schema_string = r#"
735                extend schema @link(url: "https://specs.apollo.dev/federation/v2.9")
736
737                type Query {
738                    s: String
739                }"#
740        .to_string();
741
742        let diff = schema_diff_expanded_from_initial(schema_string);
743
744        insta::assert_snapshot!(diff.unwrap_or_default(), @r#"directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
745directive @federation__key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE
746directive @federation__requires(fields: federation__FieldSet!) on FIELD_DEFINITION
747directive @federation__provides(fields: federation__FieldSet!) on FIELD_DEFINITION
748directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION
749directive @federation__tag(name: String!) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | SCHEMA
750directive @federation__extends on OBJECT | INTERFACE
751directive @federation__shareable repeatable on OBJECT | FIELD_DEFINITION
752directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION
753directive @federation__override(from: String!, label: String) on FIELD_DEFINITION
754directive @federation__composeDirective(name: String) repeatable on SCHEMA
755directive @federation__interfaceObject on OBJECT
756directive @federation__authenticated on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
757directive @federation__requiresScopes(scopes: [[federation__Scope!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
758directive @federation__policy(policies: [[federation__Policy!]!]!) on FIELD_DEFINITION | OBJECT | INTERFACE | SCALAR | ENUM
759directive @federation__context(name: String!) repeatable on INTERFACE | OBJECT | UNION
760directive @federation__fromContext(field: federation__ContextFieldValue) on ARGUMENT_DEFINITION
761directive @federation__cost(weight: Int!) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR
762directive @federation__listSize(assumedSize: Int, slicingArguments: [String!], sizedFields: [String!], requireOneSlicingArgument: Boolean = true) on FIELD_DEFINITION
763enum link__Purpose {
764  """
765  `SECURITY` features provide metadata necessary to securely resolve fields.
766  """
767  SECURITY
768  """
769  `EXECUTION` features provide metadata necessary for operation execution.
770  """
771  EXECUTION
772}
773scalar link__Import
774scalar federation__FieldSet
775scalar federation__Scope
776scalar federation__Policy
777scalar federation__ContextFieldValue
778scalar _Any
779type _Service {
780  sdl: String
781}"#);
782    }
783}