graphql_federated_graph/render_sdl/
render_federated_sdl.rs

1use itertools::Itertools;
2
3use super::{directive::write_directive, directive_definition::display_directive_definitions, display_utils::*};
4use crate::{directives::*, federated_graph::*};
5use std::fmt::{self, Display, Write};
6
7/// Render a GraphQL SDL string for a federated graph. It includes [join spec
8/// directives](https://specs.apollo.dev/join/v0.3/) about subgraphs and entities.
9pub fn render_federated_sdl(graph: &FederatedGraph) -> Result<String, fmt::Error> {
10    let mut sdl = String::new();
11
12    with_formatter(&mut sdl, |f| {
13        display_directive_definitions(|_| true, directives_filter, graph, f)?;
14
15        for scalar in graph.iter_scalar_definitions() {
16            let namespace = scalar.namespace.map(|namespace| &graph[namespace]);
17            let name = scalar.then(|scalar| scalar.name).as_str();
18            if let Some(description) = scalar.description {
19                Description(&graph[description], "").fmt(f)?;
20            }
21
22            if BUILTIN_SCALARS.contains(&name) {
23                continue;
24            }
25
26            f.write_str("scalar ")?;
27
28            if let Some(namespace) = namespace {
29                f.write_str(namespace)?;
30                f.write_str("__")?;
31            }
32
33            f.write_str(name)?;
34
35            display_definition_directives(&scalar.directives, graph, f)?;
36
37            f.write_str("\n\n")?;
38        }
39
40        Ok(())
41    })?;
42
43    for object in graph.iter_objects() {
44        let object_name = &graph[object.name];
45
46        let mut fields = graph[object.fields.clone()]
47            .iter()
48            .filter(|field| !graph[field.name].starts_with("__"))
49            .peekable();
50
51        if fields.peek().is_none() {
52            sdl.push_str("\n\n");
53            continue;
54        }
55
56        if let Some(description) = object.description {
57            write!(sdl, "{}", Description(&graph[description], ""))?;
58        }
59
60        sdl.push_str("type ");
61        sdl.push_str(object_name);
62
63        if !object.implements_interfaces.is_empty() {
64            sdl.push_str(" implements ");
65
66            for (idx, interface) in object.implements_interfaces.iter().enumerate() {
67                let interface_name = graph.at(*interface).then(|iface| iface.name).as_str();
68
69                sdl.push_str(interface_name);
70
71                if idx < object.implements_interfaces.len() - 1 {
72                    sdl.push_str(" & ");
73                }
74            }
75        }
76
77        write_definition_directives(&object.directives, graph, &mut sdl)?;
78
79        if !sdl.ends_with('\n') {
80            sdl.push('\n');
81        }
82        sdl.push_str("{\n");
83
84        for field in fields {
85            write_field(&object.directives, field, graph, &mut sdl)?;
86        }
87
88        writeln!(sdl, "}}\n")?;
89    }
90
91    for interface in graph.iter_interfaces() {
92        let interface_name = &graph[interface.name];
93
94        if let Some(description) = interface.description {
95            write!(sdl, "{}", Description(&graph[description], ""))?;
96        }
97
98        let interface_start = sdl.len();
99        write!(sdl, "interface {interface_name}")?;
100
101        if !interface.implements_interfaces.is_empty() {
102            sdl.push_str(" implements ");
103
104            for (idx, implemented) in interface.implements_interfaces.iter().enumerate() {
105                let implemented_interface = graph.view(*implemented);
106                let implemented_interface_name = &graph[implemented_interface.name];
107                sdl.push_str(implemented_interface_name);
108
109                if idx < interface.implements_interfaces.len() - 1 {
110                    sdl.push_str(" & ");
111                }
112            }
113        }
114
115        let directives_start = sdl.len();
116        write_definition_directives(&interface.directives, graph, &mut sdl)?;
117
118        if sdl[interface_start..].len() >= 80 || sdl[directives_start..].len() >= 20 {
119            if !sdl.ends_with('\n') {
120                sdl.push('\n');
121            }
122        } else if !sdl.ends_with('\n') && !sdl.ends_with(' ') {
123            sdl.push(' ');
124        }
125        sdl.push_str("{\n");
126
127        for field in &graph[interface.fields.clone()] {
128            write_field(&interface.directives, field, graph, &mut sdl)?;
129        }
130
131        writeln!(sdl, "}}\n")?;
132    }
133
134    for r#enum in graph.iter_enum_definitions() {
135        let namespace = r#enum.namespace.map(|namespace| graph[namespace].as_str());
136        let enum_name = graph.at(r#enum.name).as_str();
137        let is_extension_link = namespace == Some("extension") && enum_name == "Link";
138
139        if let Some(description) = r#enum.description {
140            write!(sdl, "{}", Description(&graph[description], ""))?;
141        }
142
143        with_formatter(&mut sdl, |f| {
144            f.write_str("enum ")?;
145            if let Some(namespace) = namespace {
146                f.write_str(namespace)?;
147                f.write_str("__")?;
148            }
149            f.write_str(enum_name)?;
150
151            display_definition_directives(&r#enum.directives, graph, f)
152        })?;
153
154        if !sdl.ends_with('\n') {
155            sdl.push('\n');
156        }
157        sdl.push_str("{\n");
158
159        for value in graph.iter_enum_values(r#enum.id()) {
160            let value_name = &graph[value.value];
161
162            if let Some(description) = value.description {
163                write!(sdl, "{}", Description(&graph[description], INDENT))?;
164            }
165
166            write!(sdl, "{INDENT}{value_name}")?;
167            with_formatter(&mut sdl, |f| {
168                for directive in &value.directives {
169                    f.write_str(" ")?;
170                    write_directive(f, directive, graph)?;
171                }
172
173                if is_extension_link {
174                    if let Some(extension) = graph
175                        .extensions
176                        .iter()
177                        .find(|extension| extension.enum_value_id == value.id())
178                    {
179                        super::directive::render_extension_link_directive(
180                            f,
181                            extension.url,
182                            &extension.schema_directives,
183                            graph,
184                        )?;
185                    }
186                }
187
188                Ok(())
189            })?;
190
191            sdl.push('\n');
192        }
193
194        writeln!(sdl, "}}\n")?;
195    }
196
197    for union in &graph.unions {
198        let union_name = &graph[r#union.name];
199
200        if let Some(description) = union.description {
201            write!(sdl, "{}", Description(&graph[description], ""))?;
202        }
203
204        write!(sdl, "union {union_name}")?;
205
206        write_definition_directives(&union.directives, graph, &mut sdl)?;
207        if !sdl.ends_with('\n') {
208            sdl.push('\n');
209        }
210        sdl.push_str(" = ");
211
212        let mut members = union.members.iter().peekable();
213
214        while let Some(member) = members.next() {
215            sdl.push_str(graph.at(*member).then(|member| member.name).as_str());
216
217            if members.peek().is_some() {
218                sdl.push_str(" | ");
219            }
220        }
221
222        sdl.push_str("\n\n");
223    }
224
225    for input_object in &graph.input_objects {
226        let name = &graph[input_object.name];
227
228        if let Some(description) = input_object.description {
229            write!(sdl, "{}", Description(&graph[description], ""))?;
230        }
231
232        write!(sdl, "input {name}")?;
233
234        write_definition_directives(&input_object.directives, graph, &mut sdl)?;
235        if !sdl.ends_with('\n') {
236            sdl.push('\n');
237        }
238        sdl.push_str("{\n");
239
240        for field in &graph[input_object.fields] {
241            write_input_field(&input_object.directives, field, graph, &mut sdl)?;
242        }
243
244        writeln!(sdl, "}}\n")?;
245    }
246
247    // Normalize to a single final newline.
248    while let Some('\n') = sdl.chars().next_back() {
249        sdl.pop();
250    }
251    sdl.push('\n');
252
253    Ok(sdl)
254}
255
256fn write_input_field(
257    parent_input_object_directives: &[Directive],
258    field: &InputValueDefinition,
259    graph: &FederatedGraph,
260    sdl: &mut String,
261) -> fmt::Result {
262    let field_name = &graph[field.name];
263    let field_type = render_field_type(&field.r#type, graph);
264
265    if let Some(description) = field.description {
266        write!(sdl, "{}", Description(&graph[description], INDENT))?;
267    }
268
269    write!(sdl, "{INDENT}{field_name}: {field_type}")?;
270
271    if let Some(default) = &field.default {
272        write!(sdl, " = {}", ValueDisplay(default, graph))?;
273    }
274
275    write_field_directives(parent_input_object_directives, &field.directives, graph, sdl)?;
276
277    sdl.push('\n');
278    Ok(())
279}
280
281fn write_field(
282    parent_entity_directives: &[Directive],
283    field: &Field,
284    graph: &FederatedGraph,
285    sdl: &mut String,
286) -> fmt::Result {
287    let field_name = &graph[field.name];
288    let field_type = render_field_type(&field.r#type, graph);
289    let args = render_field_arguments(&graph[field.arguments], graph);
290
291    if let Some(description) = field.description {
292        write!(sdl, "{}", Description(&graph[description], INDENT))?;
293    }
294
295    write!(sdl, "{INDENT}{field_name}{args}: {field_type}")?;
296
297    write_field_directives(parent_entity_directives, &field.directives, graph, sdl)?;
298
299    sdl.push('\n');
300    Ok(())
301}
302
303fn display_definition_directives(
304    directives: &[Directive],
305    graph: &FederatedGraph,
306    f: &mut fmt::Formatter<'_>,
307) -> fmt::Result {
308    for directive in directives {
309        f.write_fmt(format_args!("\n{INDENT}"))?;
310        write_directive(f, directive, graph)?;
311    }
312
313    Ok(())
314}
315
316fn write_definition_directives(directives: &[Directive], graph: &FederatedGraph, sdl: &mut String) -> fmt::Result {
317    with_formatter(sdl, |f| display_definition_directives(directives, graph, f))
318}
319
320fn write_field_directives(
321    parent_type_directives: &[Directive],
322    directives: &[Directive],
323    graph: &FederatedGraph,
324    sdl: &mut String,
325) -> fmt::Result {
326    // Whether @join__field directives must be present because one of their optional arguments such
327    // as requires is present on at least one of them.
328    let mut join_field_must_be_present = false;
329    let mut join_field_subgraph_ids = Vec::new();
330    // Subgraphs which are fully overridden by another one. We don't need to generate a
331    // @join__field for those.
332    let mut fully_overridden_subgraph_ids = Vec::new();
333
334    for directive in directives {
335        if let Directive::JoinField(dir) = directive {
336            if let (Some(OverrideSource::Subgraph(id)), None | Some(OverrideLabel::Percent(100))) =
337                (dir.r#override.as_ref(), dir.override_label.as_ref())
338            {
339                fully_overridden_subgraph_ids.push(*id);
340            }
341            join_field_subgraph_ids.extend(dir.subgraph_id);
342            join_field_must_be_present |= dir.r#override.is_some()
343                | dir.requires.is_some()
344                | dir.provides.is_some()
345                | dir.r#type.is_some()
346                | dir.external;
347        }
348    }
349
350    // If there is no use of special arguments of @join_field, we just need to check whether their
351    // count matches the number of subgraphs. If so, they're redundant, which is often the case for
352    // key fields.
353    if !join_field_must_be_present {
354        let subgraph_ids = {
355            let mut ids = parent_type_directives
356                .iter()
357                .filter_map(|dir| dir.as_join_type())
358                .map(|dir| dir.subgraph_id)
359                .collect::<Vec<_>>();
360            ids.sort_unstable();
361            ids.into_iter().dedup().collect::<Vec<_>>()
362        };
363        join_field_subgraph_ids.sort_unstable();
364        join_field_must_be_present |= subgraph_ids != join_field_subgraph_ids;
365    }
366
367    with_formatter(sdl, |f| {
368        for directive in directives {
369            if let Directive::JoinField(JoinFieldDirective {
370                subgraph_id: Some(subgraph_id),
371                ..
372            }) = &directive
373            {
374                if !join_field_must_be_present || fully_overridden_subgraph_ids.contains(subgraph_id) {
375                    continue;
376                }
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, _: &FederatedGraph) -> 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!(actual, "\n");
507    }
508
509    #[test]
510    fn escape_strings() {
511        use expect_test::expect;
512
513        let empty = FederatedGraph::from_sdl(
514            r###"
515            directive @dummy(test: String!) on FIELD
516
517            type Query {
518                field: String @deprecated(reason: "This is a \"deprecated\" reason") @dummy(test: "a \"test\"")
519            }
520            "###,
521        )
522        .unwrap();
523
524        let actual = render_federated_sdl(&empty).expect("valid");
525        let expected = expect![[r#"
526            directive @dummy(test: String!) on FIELD
527
528            type Query
529            {
530                field: String @deprecated(reason: "This is a \"deprecated\" reason") @dummy(test: "a \"test\"")
531            }
532        "#]];
533
534        expected.assert_eq(&actual);
535    }
536
537    #[test]
538    fn multiline_strings() {
539        use expect_test::expect;
540
541        let empty = FederatedGraph::from_sdl(
542            r###"
543            directive @dummy(test: String!) on FIELD
544
545            type Query {
546                field: String @deprecated(reason: """This is a "deprecated" reason
547
548                on multiple lines.
549
550                yes, way
551
552                """) @dummy(test: "a \"test\"")
553            }
554            "###,
555        )
556        .unwrap();
557
558        let actual = render_federated_sdl(&empty).expect("valid");
559        let expected = expect![[r#"
560            directive @dummy(test: String!) on FIELD
561
562            type Query
563            {
564                field: String @deprecated(reason: "This is a \"deprecated\" reason\n\non multiple lines.\n\nyes, way") @dummy(test: "a \"test\"")
565            }
566        "#]];
567
568        expected.assert_eq(&actual);
569    }
570
571    #[test]
572    fn regression_empty_keys() {
573        // Types that have a @join__type without a key argument should _not_ render with an empty string as a key.
574        let schema = r##"
575            enum join__Graph {
576              a @join__graph(name: "mocksubgraph", url: "https://mock.example.com/todo/graphql")
577            }
578
579            interface b @join__type(graph: a) {
580              c: String
581            }
582        "##;
583
584        let parsed = FederatedGraph::from_sdl(schema).unwrap();
585        let rendered = render_federated_sdl(&parsed).unwrap();
586
587        let expected = expect_test::expect![[r#"
588
589
590            interface b
591                @join__type(graph: a)
592            {
593                c: String
594            }
595
596            enum join__Graph
597            {
598                a @join__graph(name: "mocksubgraph", url: "https://mock.example.com/todo/graphql")
599            }
600        "#]];
601
602        expected.assert_eq(&rendered);
603
604        // Check that from_sdl accepts the rendered sdl
605        {
606            FederatedGraph::from_sdl(&rendered).unwrap();
607        }
608    }
609}