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