graphql_federated_graph/render_sdl/
render_federated_sdl.rs1use 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
7pub 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 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 let mut join_field_must_be_present = false;
329 let mut join_field_subgraph_ids = Vec::new();
330 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 !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 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 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 {
606 FederatedGraph::from_sdl(&rendered).unwrap();
607 }
608 }
609}