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