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 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 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 let mut join_field_must_be_present = false;
328 let mut join_field_subgraph_ids = Vec::new();
329 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 !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 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 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 {
596 FederatedGraph::from_sdl(&rendered).unwrap();
597 }
598 }
599}