graphql_composition/federated_graph/render_sdl/
render_api_sdl.rs

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