graphql_composition/federated_graph/render_sdl/
render_federated_sdl.rs

1mod schema_definition;
2
3use super::{directive::write_directive, directive_definition::display_directive_definitions, display_utils::*};
4use crate::federated_graph::*;
5use itertools::Itertools;
6use std::fmt::{self, Display, Write};
7
8/// Render a GraphQL SDL string for a federated graph. It includes [join spec
9/// directives](https://specs.apollo.dev/join/v0.3/) about subgraphs and entities.
10pub fn render_federated_sdl(graph: &FederatedGraph) -> Result<String, fmt::Error> {
11    let mut sdl = String::new();
12
13    with_formatter(&mut sdl, |f| {
14        schema_definition::display_schema_definition(graph, f)?;
15
16        display_directive_definitions(|_| true, directives_filter, graph, f)?;
17
18        for scalar in graph.iter_scalar_definitions() {
19            let namespace = scalar.namespace.map(|namespace| &graph[namespace]);
20            let name = scalar.then(|scalar| scalar.name).as_str();
21            if let Some(description) = scalar.description {
22                Description(&graph[description], "").fmt(f)?;
23            }
24
25            if BUILTIN_SCALARS.contains(&name) {
26                continue;
27            }
28
29            f.write_str("scalar ")?;
30
31            if let Some(namespace) = namespace {
32                f.write_str(namespace)?;
33                f.write_str("__")?;
34            }
35
36            f.write_str(name)?;
37
38            display_definition_directives(&scalar.directives, graph, f)?;
39
40            f.write_str("\n\n")?;
41        }
42
43        Ok(())
44    })?;
45
46    for object in graph.iter_objects() {
47        let object_name = &graph[object.name];
48
49        let mut fields = graph[object.fields.clone()]
50            .iter()
51            .filter(|field| !graph[field.name].starts_with("__"))
52            .peekable();
53
54        if fields.peek().is_none() {
55            continue;
56        }
57
58        if let Some(description) = object.description {
59            write!(sdl, "{}", Description(&graph[description], ""))?;
60        }
61
62        sdl.push_str("type ");
63        sdl.push_str(object_name);
64
65        if !object.implements_interfaces.is_empty() {
66            sdl.push_str(" implements ");
67
68            for (idx, interface) in object.implements_interfaces.iter().enumerate() {
69                let interface_name = graph.at(*interface).then(|iface| iface.name).as_str();
70
71                sdl.push_str(interface_name);
72
73                if idx < object.implements_interfaces.len() - 1 {
74                    sdl.push_str(" & ");
75                }
76            }
77        }
78
79        write_definition_directives(&object.directives, graph, &mut sdl)?;
80
81        if !sdl.ends_with('\n') {
82            sdl.push('\n');
83        }
84        sdl.push_str("{\n");
85
86        for field in fields {
87            write_field(&object.directives, field, graph, &mut sdl)?;
88        }
89
90        writeln!(sdl, "}}\n")?;
91    }
92
93    for interface in graph.iter_interfaces() {
94        let interface_name = &graph[interface.name];
95
96        if let Some(description) = interface.description {
97            write!(sdl, "{}", Description(&graph[description], ""))?;
98        }
99
100        let interface_start = sdl.len();
101        write!(sdl, "interface {interface_name}")?;
102
103        if !interface.implements_interfaces.is_empty() {
104            sdl.push_str(" implements ");
105
106            for (idx, implemented) in interface.implements_interfaces.iter().enumerate() {
107                let implemented_interface = graph.view(*implemented);
108                let implemented_interface_name = &graph[implemented_interface.name];
109                sdl.push_str(implemented_interface_name);
110
111                if idx < interface.implements_interfaces.len() - 1 {
112                    sdl.push_str(" & ");
113                }
114            }
115        }
116
117        let directives_start = sdl.len();
118        write_definition_directives(&interface.directives, graph, &mut sdl)?;
119
120        if sdl[interface_start..].len() >= 80 || sdl[directives_start..].len() >= 20 {
121            if !sdl.ends_with('\n') {
122                sdl.push('\n');
123            }
124        } else if !sdl.ends_with('\n') && !sdl.ends_with(' ') {
125            sdl.push(' ');
126        }
127        sdl.push_str("{\n");
128
129        for field in &graph[interface.fields.clone()] {
130            write_field(&interface.directives, field, graph, &mut sdl)?;
131        }
132
133        writeln!(sdl, "}}\n")?;
134    }
135
136    for r#enum in graph.iter_enum_definitions() {
137        let namespace = r#enum.namespace.map(|namespace| graph[namespace].as_str());
138        let enum_name = graph.at(r#enum.name).as_str();
139        let is_extension_link = namespace == Some("extension") && enum_name == "Link";
140
141        if let Some(description) = r#enum.description {
142            write!(sdl, "{}", Description(&graph[description], ""))?;
143        }
144
145        with_formatter(&mut sdl, |f| {
146            f.write_str("enum ")?;
147            if let Some(namespace) = namespace {
148                f.write_str(namespace)?;
149                f.write_str("__")?;
150            }
151            f.write_str(enum_name)?;
152
153            display_definition_directives(&r#enum.directives, graph, f)
154        })?;
155
156        if !sdl.ends_with('\n') {
157            sdl.push('\n');
158        }
159        sdl.push_str("{\n");
160
161        for value in graph.iter_enum_values(r#enum.id()) {
162            let value_name = &graph[value.value];
163
164            if let Some(description) = value.description {
165                write!(sdl, "{}", Description(&graph[description], INDENT))?;
166            }
167
168            write!(sdl, "{INDENT}{value_name}")?;
169            with_formatter(&mut sdl, |f| {
170                for directive in &value.directives {
171                    f.write_str(" ")?;
172                    write_directive(f, directive, graph)?;
173                }
174
175                if is_extension_link
176                    && let Some(extension) = graph
177                        .extensions
178                        .iter()
179                        .find(|extension| extension.enum_value_id == value.id())
180                {
181                    super::directive::render_extension_link_directive(
182                        f,
183                        extension.url,
184                        &extension.schema_directives,
185                        graph,
186                    )?;
187                }
188
189                Ok(())
190            })?;
191
192            sdl.push('\n');
193        }
194
195        writeln!(sdl, "}}\n")?;
196    }
197
198    for union in &graph.unions {
199        let union_name = &graph[r#union.name];
200
201        if let Some(description) = union.description {
202            write!(sdl, "{}", Description(&graph[description], ""))?;
203        }
204
205        write!(sdl, "union {union_name}")?;
206
207        write_definition_directives(&union.directives, graph, &mut sdl)?;
208        if !sdl.ends_with('\n') {
209            sdl.push('\n');
210        }
211        sdl.push_str(" = ");
212
213        let mut members = union.members.iter().peekable();
214
215        while let Some(member) = members.next() {
216            sdl.push_str(graph.at(*member).then(|member| member.name).as_str());
217
218            if members.peek().is_some() {
219                sdl.push_str(" | ");
220            }
221        }
222
223        sdl.push_str("\n\n");
224    }
225
226    for input_object in &graph.input_objects {
227        let name = &graph[input_object.name];
228
229        if let Some(description) = input_object.description {
230            write!(sdl, "{}", Description(&graph[description], ""))?;
231        }
232
233        write!(sdl, "input {name}")?;
234
235        write_definition_directives(&input_object.directives, graph, &mut sdl)?;
236        if !sdl.ends_with('\n') {
237            sdl.push('\n');
238        }
239        sdl.push_str("{\n");
240
241        for field in &graph[input_object.fields] {
242            write_input_field(&input_object.directives, field, graph, &mut sdl)?;
243        }
244
245        writeln!(sdl, "}}\n")?;
246    }
247
248    // Normalize to a single final newline.
249    while let Some('\n') = sdl.chars().next_back() {
250        sdl.pop();
251    }
252    sdl.push('\n');
253
254    Ok(sdl)
255}
256
257fn write_input_field(
258    parent_input_object_directives: &[Directive],
259    field: &InputValueDefinition,
260    graph: &FederatedGraph,
261    sdl: &mut String,
262) -> fmt::Result {
263    let field_name = &graph[field.name];
264    let field_type = render_field_type(&field.r#type, graph);
265
266    if let Some(description) = field.description {
267        write!(sdl, "{}", Description(&graph[description], INDENT))?;
268    }
269
270    write!(sdl, "{INDENT}{field_name}: {field_type}")?;
271
272    if let Some(default) = &field.default {
273        write!(sdl, " = {}", ValueDisplay(default, graph))?;
274    }
275
276    write_field_directives(parent_input_object_directives, &field.directives, graph, sdl)?;
277
278    sdl.push('\n');
279    Ok(())
280}
281
282fn write_field(
283    parent_entity_directives: &[Directive],
284    field: &Field,
285    graph: &FederatedGraph,
286    sdl: &mut String,
287) -> fmt::Result {
288    let field_name = &graph[field.name];
289    let field_type = render_field_type(&field.r#type, graph);
290    let args = render_field_arguments(&graph[field.arguments], graph);
291
292    if let Some(description) = field.description {
293        write!(sdl, "{}", Description(&graph[description], INDENT))?;
294    }
295
296    write!(sdl, "{INDENT}{field_name}{args}: {field_type}")?;
297
298    write_field_directives(parent_entity_directives, &field.directives, graph, sdl)?;
299
300    sdl.push('\n');
301    Ok(())
302}
303
304fn display_definition_directives(
305    directives: &[Directive],
306    graph: &FederatedGraph,
307    f: &mut fmt::Formatter<'_>,
308) -> fmt::Result {
309    for directive in directives {
310        f.write_fmt(format_args!("\n{INDENT}"))?;
311        write_directive(f, directive, graph)?;
312    }
313
314    Ok(())
315}
316
317fn write_definition_directives(directives: &[Directive], graph: &FederatedGraph, sdl: &mut String) -> fmt::Result {
318    with_formatter(sdl, |f| display_definition_directives(directives, graph, f))
319}
320
321fn write_field_directives(
322    parent_type_directives: &[Directive],
323    directives: &[Directive],
324    graph: &FederatedGraph,
325    sdl: &mut String,
326) -> fmt::Result {
327    // Whether @join__field directives must be present because one of their optional arguments such
328    // as requires is present on at least one of them.
329    let mut join_field_must_be_present = false;
330    let mut join_field_subgraph_ids = Vec::new();
331    // Subgraphs which are fully overridden by another one. We don't need to generate a
332    // @join__field for those.
333    let mut fully_overridden_subgraph_ids = Vec::new();
334
335    for directive in directives {
336        if let Directive::JoinField(dir) = directive {
337            if let (Some(OverrideSource::Subgraph(id)), None | Some(OverrideLabel::Percent(100))) =
338                (dir.r#override.as_ref(), dir.override_label.as_ref())
339            {
340                fully_overridden_subgraph_ids.push(*id);
341            }
342            join_field_subgraph_ids.extend(dir.subgraph_id);
343            join_field_must_be_present |= dir.r#override.is_some()
344                | dir.requires.is_some()
345                | dir.provides.is_some()
346                | dir.r#type.is_some()
347                | dir.external;
348        }
349    }
350
351    // If there is no use of special arguments of @join_field, we just need to check whether their
352    // count matches the number of subgraphs. If so, they're redundant, which is often the case for
353    // key fields.
354    if !join_field_must_be_present {
355        let subgraph_ids = {
356            let mut ids = parent_type_directives
357                .iter()
358                .filter_map(|dir| dir.as_join_type())
359                .map(|dir| dir.subgraph_id)
360                .collect::<Vec<_>>();
361            ids.sort_unstable();
362            ids.into_iter().dedup().collect::<Vec<_>>()
363        };
364        join_field_subgraph_ids.sort_unstable();
365        join_field_must_be_present |= subgraph_ids != join_field_subgraph_ids;
366    }
367
368    with_formatter(sdl, |f| {
369        for directive in directives {
370            if let Directive::JoinField(JoinFieldDirective {
371                subgraph_id: Some(subgraph_id),
372                ..
373            }) = &directive
374                && (!join_field_must_be_present || fully_overridden_subgraph_ids.contains(subgraph_id))
375            {
376                continue;
377            }
378            f.write_str(" ")?;
379            write_directive(f, directive, graph)?;
380        }
381        Ok(())
382    })
383}
384
385fn render_field_arguments(args: &[InputValueDefinition], graph: &FederatedGraph) -> String {
386    if args.is_empty() {
387        String::new()
388    } else {
389        let mut inner = args
390            .iter()
391            .map(|arg| {
392                let name = &graph[arg.name];
393                let r#type = render_field_type(&arg.r#type, graph);
394                let directives = &arg.directives;
395                let default = arg.default.as_ref();
396                let description = arg.description;
397                (name, r#type, directives, default, description)
398            })
399            .peekable();
400        let mut out = String::from('(');
401
402        while let Some((name, ty, directives, default, description)) = inner.next() {
403            if let Some(description) = description {
404                with_formatter(&mut out, |f| {
405                    display_graphql_string_literal(&graph[description], f)?;
406                    f.write_str(" ")
407                })
408                .unwrap();
409            }
410
411            out.push_str(name);
412            out.push_str(": ");
413            out.push_str(&ty);
414
415            if let Some(default) = default {
416                out.push_str(" = ");
417                write!(out, "{}", ValueDisplay(default, graph)).unwrap();
418            }
419
420            with_formatter(&mut out, |f| {
421                for directive in directives {
422                    f.write_str(" ")?;
423                    write_directive(f, directive, graph)?;
424                }
425                Ok(())
426            })
427            .unwrap();
428
429            if inner.peek().is_some() {
430                out.push_str(", ");
431            }
432        }
433        out.push(')');
434        out
435    }
436}
437
438pub(super) struct ListSizeRender<'a> {
439    pub list_size: &'a ListSize,
440    pub graph: &'a FederatedGraph,
441}
442
443impl std::fmt::Display for ListSizeRender<'_> {
444    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445        f.write_str(" ")?;
446
447        let ListSizeRender {
448            graph,
449            list_size:
450                ListSize {
451                    assumed_size,
452                    slicing_arguments,
453                    sized_fields,
454                    require_one_slicing_argument,
455                },
456        } = self;
457
458        let mut writer = DirectiveWriter::new("listSize", f, graph)?;
459        if let Some(size) = assumed_size {
460            writer = writer.arg("assumedSize", Value::Int(*size as i64))?;
461        }
462
463        if !slicing_arguments.is_empty() {
464            let slicing_arguments = slicing_arguments
465                .iter()
466                .map(|arg| Value::String(graph[*arg].name))
467                .collect::<Vec<_>>();
468
469            writer = writer.arg("slicingArguments", Value::List(slicing_arguments.into_boxed_slice()))?;
470        }
471
472        if !sized_fields.is_empty() {
473            let sized_fields = sized_fields
474                .iter()
475                .map(|field| Value::String(graph[*field].name))
476                .collect::<Vec<_>>();
477
478            writer = writer.arg("sizedFields", Value::List(sized_fields.into_boxed_slice()))?;
479        }
480
481        if !require_one_slicing_argument {
482            // require_one_slicing_argument defaults to true so we omit it unless its false
483            writer.arg(
484                "requireOneSlicingArgument",
485                Value::Boolean(*require_one_slicing_argument),
486            )?;
487        }
488
489        Ok(())
490    }
491}
492
493fn directives_filter(_: &Directive) -> bool {
494    true
495}
496
497#[cfg(test)]
498mod tests {
499    use super::*;
500
501    #[test]
502    fn test_render_empty() {
503        let empty = FederatedGraph::default();
504
505        let actual = render_federated_sdl(&empty).expect("valid");
506        assert_eq!(
507            actual,
508            "extend schema\n  @link(url: \"https://specs.apollo.dev/link/v1.0\")\n  @link(url: \"https://specs.apollo.dev/join/v0.3\", for: EXECUTION)\n  @link(url: \"https://specs.apollo.dev/inaccessible/v0.2\", for: SECURITY)\n"
509        );
510    }
511
512    #[test]
513    fn escape_strings() {
514        let empty = FederatedGraph::from_sdl(
515            r###"
516            directive @dummy(test: String!) on FIELD
517
518            type Query {
519                field: String @deprecated(reason: "This is a \"deprecated\" reason") @dummy(test: "a \"test\"")
520            }
521            "###,
522        )
523        .unwrap();
524
525        let actual = render_federated_sdl(&empty).expect("valid");
526        insta::assert_snapshot!(actual, @r#"
527        schema
528          @link(url: "https://specs.apollo.dev/link/v1.0")
529          @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
530          @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
531        {
532          query: Query
533        }
534
535        directive @dummy(test: String!) on FIELD
536
537        type Query
538        {
539          field: String @deprecated(reason: "This is a \"deprecated\" reason") @dummy(test: "a \"test\"")
540        }
541        "#);
542    }
543
544    #[test]
545    fn multiline_strings() {
546        let empty = FederatedGraph::from_sdl(
547            r###"
548            directive @dummy(test: String!) on FIELD
549
550            type Query {
551                field: String @deprecated(reason: """This is a "deprecated" reason
552
553                on multiple lines.
554
555                yes, way
556
557                """) @dummy(test: "a \"test\"")
558            }
559            "###,
560        )
561        .unwrap();
562
563        let actual = render_federated_sdl(&empty).expect("valid");
564        insta::assert_snapshot!(actual, @r#"
565        schema
566          @link(url: "https://specs.apollo.dev/link/v1.0")
567          @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
568          @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
569        {
570          query: Query
571        }
572
573        directive @dummy(test: String!) on FIELD
574
575        type Query
576        {
577          field: String @deprecated(reason: "This is a \"deprecated\" reason\n\non multiple lines.\n\nyes, way") @dummy(test: "a \"test\"")
578        }
579        "#);
580    }
581
582    #[test]
583    fn regression_empty_keys() {
584        // Types that have a @join__type without a key argument should _not_ render with an empty string as a key.
585        let schema = r##"
586            enum join__Graph {
587              a @join__graph(name: "mocksubgraph", url: "https://mock.example.com/todo/graphql")
588            }
589
590            interface b @join__type(graph: a) {
591              c: String
592            }
593        "##;
594
595        let parsed = FederatedGraph::from_sdl(schema).unwrap();
596        let rendered = render_federated_sdl(&parsed).unwrap();
597
598        insta::assert_snapshot!(
599            &rendered,
600            @r#"
601        extend schema
602          @link(url: "https://specs.apollo.dev/link/v1.0")
603          @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
604          @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
605
606        interface b
607          @join__type(graph: a)
608        {
609          c: String
610        }
611
612        enum join__Graph
613        {
614          a @join__graph(name: "mocksubgraph", url: "https://mock.example.com/todo/graphql")
615        }
616        "#
617        );
618
619        // Check that from_sdl accepts the rendered sdl
620        {
621            FederatedGraph::from_sdl(&rendered).unwrap();
622        }
623    }
624}