graphql_federated_graph/render_sdl/
render_api_sdl.rs

1use super::{directive::write_directive, directive_definition::display_directive_definitions, display_utils::*};
2use crate::{FederatedGraph, directives::*, federated_graph::*};
3use std::fmt::{self, Write as _};
4
5/// Render a GraphQL SDL string for a federated graph. It does not include any
6/// federation-specific directives, it only reflects the final API schema as visible
7/// for consumers.
8pub fn render_api_sdl(graph: &FederatedGraph) -> String {
9    Renderer { graph }.to_string()
10}
11
12struct Renderer<'a> {
13    graph: &'a FederatedGraph,
14}
15
16impl fmt::Display for Renderer<'_> {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        let Renderer { graph } = self;
19
20        // For spaces between blocks, to avoid a leading newline at the beginning of the file.
21        let mut write_leading_whitespace = {
22            let mut first_block = true;
23            move |f: &mut fmt::Formatter<'_>| {
24                if first_block {
25                    first_block = false;
26                    Ok(())
27                } else {
28                    f.write_char('\n')
29                }
30            }
31        };
32
33        display_directive_definitions(|def| def.namespace.is_none(), public_directives_filter, graph, f)?;
34
35        for r#enum in graph.iter_enum_definitions() {
36            if has_inaccessible(&r#enum.directives) || r#enum.namespace.is_some() {
37                continue;
38            }
39
40            write_leading_whitespace(f)?;
41
42            write_description(f, r#enum.description, "", graph)?;
43            f.write_str("enum ")?;
44            f.write_str(&graph[r#enum.name])?;
45            write_public_directives(f, &r#enum.directives, graph)?;
46            f.write_char(' ')?;
47
48            write_block(f, |f| {
49                for variant in graph.iter_enum_values(r#enum.id()) {
50                    if has_inaccessible(&variant.directives) {
51                        continue;
52                    }
53
54                    write_enum_variant(f, &variant, graph)?;
55                }
56
57                Ok(())
58            })?;
59
60            f.write_char('\n')?;
61        }
62
63        for object in graph.iter_objects() {
64            if has_inaccessible(&object.directives) {
65                continue;
66            }
67
68            if graph[object.fields.clone()].iter().all(|field| {
69                let field_name = &graph[field.name];
70                field_name.starts_with("__") || has_inaccessible(&field.directives)
71            }) {
72                continue;
73            }
74
75            write_leading_whitespace(f)?;
76
77            write_description(f, object.description, "", graph)?;
78            f.write_str("type ")?;
79            f.write_str(&graph[object.name])?;
80            write_public_directives(f, &object.directives, graph)?;
81            f.write_char(' ')?;
82
83            write_block(f, |f| {
84                for field in &graph[object.fields.clone()] {
85                    let field_name = &graph[field.name];
86
87                    if field_name.starts_with("__") || has_inaccessible(&field.directives) {
88                        continue;
89                    }
90
91                    write_description(f, field.description, INDENT, graph)?;
92                    f.write_str(INDENT)?;
93                    f.write_str(field_name)?;
94                    write_field_arguments(f, &graph[field.arguments], graph)?;
95                    f.write_str(": ")?;
96                    f.write_str(&render_field_type(&field.r#type, graph))?;
97                    write_public_directives(f, &field.directives, graph)?;
98                    f.write_char('\n')?;
99                }
100
101                Ok(())
102            })?;
103
104            f.write_char('\n')?;
105        }
106
107        for interface in &graph.interfaces {
108            if has_inaccessible(&interface.directives) {
109                continue;
110            }
111
112            write_leading_whitespace(f)?;
113
114            write_description(f, interface.description, "", graph)?;
115            f.write_str("interface ")?;
116            f.write_str(&graph[interface.name])?;
117            write_public_directives(f, &interface.directives, graph)?;
118            f.write_char(' ')?;
119
120            write_block(f, |f| {
121                for field in &graph[interface.fields.clone()] {
122                    if has_inaccessible(&field.directives) {
123                        continue;
124                    }
125
126                    let field_name = &graph[field.name];
127                    write_description(f, field.description, INDENT, graph)?;
128                    f.write_str(INDENT)?;
129                    f.write_str(field_name)?;
130                    write_field_arguments(f, &graph[field.arguments], graph)?;
131                    f.write_str(": ")?;
132                    f.write_str(&render_field_type(&field.r#type, graph))?;
133                    write_public_directives(f, &field.directives, graph)?;
134                    f.write_char('\n')?;
135                }
136
137                Ok(())
138            })?;
139
140            f.write_char('\n')?;
141        }
142
143        for input_object in &graph.input_objects {
144            if has_inaccessible(&input_object.directives) {
145                continue;
146            }
147
148            write_leading_whitespace(f)?;
149
150            write_description(f, input_object.description, "", graph)?;
151            f.write_str("input ")?;
152            f.write_str(&graph[input_object.name])?;
153            write_public_directives(f, &input_object.directives, graph)?;
154
155            f.write_char(' ')?;
156
157            write_block(f, |f| {
158                for field in &graph[input_object.fields] {
159                    if has_inaccessible(&field.directives) {
160                        continue;
161                    }
162
163                    write_description(f, field.description, INDENT, graph)?;
164                    let field_name = &graph[field.name];
165                    f.write_str(INDENT)?;
166                    f.write_str(field_name)?;
167                    f.write_str(": ")?;
168                    f.write_str(&render_field_type(&field.r#type, graph))?;
169
170                    if let Some(default) = &field.default {
171                        write!(f, " = {}", ValueDisplay(default, graph))?;
172                    }
173
174                    write_public_directives(f, &field.directives, graph)?;
175                    f.write_char('\n')?;
176                }
177
178                Ok(())
179            })?;
180
181            f.write_char('\n')?;
182        }
183
184        for union in &graph.unions {
185            if has_inaccessible(&union.directives) {
186                continue;
187            }
188
189            write_leading_whitespace(f)?;
190
191            write_description(f, union.description, "", graph)?;
192            f.write_str("union ")?;
193            f.write_str(&graph[union.name])?;
194            write_public_directives(f, &union.directives, graph)?;
195            f.write_str(" =")?;
196
197            let mut members = union.members.iter().peekable();
198
199            while let Some(member) = members.next() {
200                f.write_str(" ")?;
201                f.write_str(graph.at(*member).then(|obj| obj.name).as_str())?;
202
203                if members.peek().is_some() {
204                    f.write_str(" |")?;
205                }
206            }
207
208            f.write_char('\n')?;
209        }
210
211        for scalar in graph.iter_scalar_definitions() {
212            let scalar_name = scalar.then(|scalar| scalar.name).as_str();
213
214            if scalar.namespace.is_some() {
215                continue;
216            }
217
218            if BUILTIN_SCALARS.contains(&scalar_name) || has_inaccessible(&scalar.directives) {
219                continue;
220            }
221
222            write_leading_whitespace(f)?;
223
224            write_description(f, scalar.description, "", graph)?;
225            f.write_str("scalar ")?;
226            f.write_str(scalar_name)?;
227            write_public_directives(f, &scalar.directives, graph)?;
228
229            f.write_char('\n')?;
230        }
231
232        Ok(())
233    }
234}
235
236fn has_inaccessible(directives: &[Directive]) -> bool {
237    directives
238        .iter()
239        .any(|directive| matches!(directive, Directive::Inaccessible))
240}
241
242fn public_directives_filter(directive: &Directive, graph: &FederatedGraph) -> bool {
243    match directive {
244        Directive::Inaccessible
245        | Directive::OneOf
246        | Directive::Policy(_)
247        | Directive::RequiresScopes(_)
248        | Directive::Authenticated
249        | Directive::Cost { .. }
250        | Directive::JoinField(_)
251        | Directive::JoinType(_)
252        | Directive::JoinUnionMember(_)
253        | Directive::JoinImplements(_)
254        | Directive::Authorized(_)
255        | Directive::ListSize(_)
256        | Directive::JoinGraph(_)
257        | Directive::CompositeLookup { .. }
258        | Directive::CompositeDerive { .. }
259        | Directive::CompositeRequire { .. }
260        | Directive::CompositeIs { .. }
261        | Directive::ExtensionDirective { .. } => false,
262
263        Directive::Other { name, .. } if graph[*name] == "tag" => false,
264        Directive::Deprecated { .. } | Directive::Other { .. } => true,
265    }
266}
267
268fn write_public_directives<'a, 'b: 'a>(
269    f: &'a mut fmt::Formatter<'b>,
270    directives: &[Directive],
271    graph: &'a FederatedGraph,
272) -> fmt::Result {
273    for directive in directives
274        .iter()
275        .filter(|directive| public_directives_filter(directive, graph))
276    {
277        f.write_str(" ")?;
278        write_directive(f, directive, graph)?;
279    }
280
281    Ok(())
282}
283
284fn write_enum_variant<'a, 'b: 'a>(
285    f: &'a mut fmt::Formatter<'b>,
286    enum_variant: &EnumValueRecord,
287    graph: &'a FederatedGraph,
288) -> fmt::Result {
289    f.write_str(INDENT)?;
290    write_description(f, enum_variant.description, INDENT, graph)?;
291    f.write_str(&graph[enum_variant.value])?;
292    write_public_directives(f, &enum_variant.directives, graph)?;
293    f.write_char('\n')
294}
295
296fn write_field_arguments<'a, 'b: 'a>(
297    f: &'a mut fmt::Formatter<'b>,
298    args: &[InputValueDefinition],
299    graph: &'a FederatedGraph,
300) -> fmt::Result {
301    if args.is_empty() {
302        return Ok(());
303    }
304
305    let mut inner = args
306        .iter()
307        .map(|arg| {
308            let name = &graph[arg.name];
309            let r#type = render_field_type(&arg.r#type, graph);
310            let directives = &arg.directives;
311            let default = arg.default.as_ref();
312            let description = arg.description;
313            (name, r#type, directives, default, description)
314        })
315        .peekable();
316
317    f.write_str("(")?;
318
319    while let Some((name, ty, directives, default, description)) = inner.next() {
320        if let Some(description) = description {
321            display_graphql_string_literal(&graph[description], f)?;
322            f.write_str(" ")?;
323        }
324
325        f.write_str(name)?;
326        f.write_str(": ")?;
327        f.write_str(&ty)?;
328
329        if let Some(default) = default {
330            write!(f, " = {}", ValueDisplay(default, graph))?;
331        }
332
333        write_public_directives(f, directives, graph)?;
334
335        if inner.peek().is_some() {
336            f.write_str(", ")?;
337        }
338    }
339
340    f.write_str(")")
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn test_empty() {
349        let empty = FederatedGraph::default();
350        let sdl = render_api_sdl(&empty);
351        assert!(sdl.is_empty());
352    }
353}