1mod schema_definition;
2
3use super::{directive::write_directive, directive_definition::display_directive_definitions, display_utils::*};
4use crate::federated_graph::*;
5use itertools::Itertools;
6use std::fmt::{self, Display, Write};
7
8pub fn render_federated_sdl(graph: &FederatedGraph) -> Result<String, fmt::Error> {
11 let mut sdl = String::new();
12
13 with_formatter(&mut sdl, |f| {
14 schema_definition::display_schema_definition(graph, f)?;
15
16 display_directive_definitions(|_| true, directives_filter, graph, f)?;
17
18 for scalar in graph.iter_scalar_definitions() {
19 let namespace = scalar.namespace.map(|namespace| &graph[namespace]);
20 let name = scalar.then(|scalar| scalar.name).as_str();
21 if let Some(description) = scalar.description {
22 Description(&graph[description], "").fmt(f)?;
23 }
24
25 if BUILTIN_SCALARS.contains(&name) {
26 continue;
27 }
28
29 f.write_str("scalar ")?;
30
31 if let Some(namespace) = namespace {
32 f.write_str(namespace)?;
33 f.write_str("__")?;
34 }
35
36 f.write_str(name)?;
37
38 display_definition_directives(&scalar.directives, graph, f)?;
39
40 f.write_str("\n\n")?;
41 }
42
43 Ok(())
44 })?;
45
46 for object in graph.iter_objects() {
47 let object_name = &graph[object.name];
48
49 let mut fields = graph[object.fields.clone()]
50 .iter()
51 .filter(|field| !graph[field.name].starts_with("__"))
52 .peekable();
53
54 if fields.peek().is_none() {
55 continue;
56 }
57
58 if let Some(description) = object.description {
59 write!(sdl, "{}", Description(&graph[description], ""))?;
60 }
61
62 sdl.push_str("type ");
63 sdl.push_str(object_name);
64
65 if !object.implements_interfaces.is_empty() {
66 sdl.push_str(" implements ");
67
68 for (idx, interface) in object.implements_interfaces.iter().enumerate() {
69 let interface_name = graph.at(*interface).then(|iface| iface.name).as_str();
70
71 sdl.push_str(interface_name);
72
73 if idx < object.implements_interfaces.len() - 1 {
74 sdl.push_str(" & ");
75 }
76 }
77 }
78
79 write_definition_directives(&object.directives, graph, &mut sdl)?;
80
81 if !sdl.ends_with('\n') {
82 sdl.push('\n');
83 }
84 sdl.push_str("{\n");
85
86 for field in fields {
87 write_field(&object.directives, field, graph, &mut sdl)?;
88 }
89
90 writeln!(sdl, "}}\n")?;
91 }
92
93 for interface in graph.iter_interfaces() {
94 let interface_name = &graph[interface.name];
95
96 if let Some(description) = interface.description {
97 write!(sdl, "{}", Description(&graph[description], ""))?;
98 }
99
100 let interface_start = sdl.len();
101 write!(sdl, "interface {interface_name}")?;
102
103 if !interface.implements_interfaces.is_empty() {
104 sdl.push_str(" implements ");
105
106 for (idx, implemented) in interface.implements_interfaces.iter().enumerate() {
107 let implemented_interface = graph.view(*implemented);
108 let implemented_interface_name = &graph[implemented_interface.name];
109 sdl.push_str(implemented_interface_name);
110
111 if idx < interface.implements_interfaces.len() - 1 {
112 sdl.push_str(" & ");
113 }
114 }
115 }
116
117 let directives_start = sdl.len();
118 write_definition_directives(&interface.directives, graph, &mut sdl)?;
119
120 if sdl[interface_start..].len() >= 80 || sdl[directives_start..].len() >= 20 {
121 if !sdl.ends_with('\n') {
122 sdl.push('\n');
123 }
124 } else if !sdl.ends_with('\n') && !sdl.ends_with(' ') {
125 sdl.push(' ');
126 }
127 sdl.push_str("{\n");
128
129 for field in &graph[interface.fields.clone()] {
130 write_field(&interface.directives, field, graph, &mut sdl)?;
131 }
132
133 writeln!(sdl, "}}\n")?;
134 }
135
136 for r#enum in graph.iter_enum_definitions() {
137 let namespace = r#enum.namespace.map(|namespace| graph[namespace].as_str());
138 let enum_name = graph.at(r#enum.name).as_str();
139 let is_extension_link = namespace == Some("extension") && enum_name == "Link";
140
141 if let Some(description) = r#enum.description {
142 write!(sdl, "{}", Description(&graph[description], ""))?;
143 }
144
145 with_formatter(&mut sdl, |f| {
146 f.write_str("enum ")?;
147 if let Some(namespace) = namespace {
148 f.write_str(namespace)?;
149 f.write_str("__")?;
150 }
151 f.write_str(enum_name)?;
152
153 display_definition_directives(&r#enum.directives, graph, f)
154 })?;
155
156 if !sdl.ends_with('\n') {
157 sdl.push('\n');
158 }
159 sdl.push_str("{\n");
160
161 for value in graph.iter_enum_values(r#enum.id()) {
162 let value_name = &graph[value.value];
163
164 if let Some(description) = value.description {
165 write!(sdl, "{}", Description(&graph[description], INDENT))?;
166 }
167
168 write!(sdl, "{INDENT}{value_name}")?;
169 with_formatter(&mut sdl, |f| {
170 for directive in &value.directives {
171 f.write_str(" ")?;
172 write_directive(f, directive, graph)?;
173 }
174
175 if is_extension_link
176 && let Some(extension) = graph
177 .extensions
178 .iter()
179 .find(|extension| extension.enum_value_id == value.id())
180 {
181 super::directive::render_extension_link_directive(
182 f,
183 extension.url,
184 &extension.schema_directives,
185 graph,
186 )?;
187 }
188
189 Ok(())
190 })?;
191
192 sdl.push('\n');
193 }
194
195 writeln!(sdl, "}}\n")?;
196 }
197
198 for union in &graph.unions {
199 let union_name = &graph[r#union.name];
200
201 if let Some(description) = union.description {
202 write!(sdl, "{}", Description(&graph[description], ""))?;
203 }
204
205 write!(sdl, "union {union_name}")?;
206
207 write_definition_directives(&union.directives, graph, &mut sdl)?;
208 if !sdl.ends_with('\n') {
209 sdl.push('\n');
210 }
211 sdl.push_str(" = ");
212
213 let mut members = union.members.iter().peekable();
214
215 while let Some(member) = members.next() {
216 sdl.push_str(graph.at(*member).then(|member| member.name).as_str());
217
218 if members.peek().is_some() {
219 sdl.push_str(" | ");
220 }
221 }
222
223 sdl.push_str("\n\n");
224 }
225
226 for input_object in &graph.input_objects {
227 let name = &graph[input_object.name];
228
229 if let Some(description) = input_object.description {
230 write!(sdl, "{}", Description(&graph[description], ""))?;
231 }
232
233 write!(sdl, "input {name}")?;
234
235 write_definition_directives(&input_object.directives, graph, &mut sdl)?;
236 if !sdl.ends_with('\n') {
237 sdl.push('\n');
238 }
239 sdl.push_str("{\n");
240
241 for field in &graph[input_object.fields] {
242 write_input_field(&input_object.directives, field, graph, &mut sdl)?;
243 }
244
245 writeln!(sdl, "}}\n")?;
246 }
247
248 while let Some('\n') = sdl.chars().next_back() {
250 sdl.pop();
251 }
252 sdl.push('\n');
253
254 Ok(sdl)
255}
256
257fn write_input_field(
258 parent_input_object_directives: &[Directive],
259 field: &InputValueDefinition,
260 graph: &FederatedGraph,
261 sdl: &mut String,
262) -> fmt::Result {
263 let field_name = &graph[field.name];
264 let field_type = render_field_type(&field.r#type, graph);
265
266 if let Some(description) = field.description {
267 write!(sdl, "{}", Description(&graph[description], INDENT))?;
268 }
269
270 write!(sdl, "{INDENT}{field_name}: {field_type}")?;
271
272 if let Some(default) = &field.default {
273 write!(sdl, " = {}", ValueDisplay(default, graph))?;
274 }
275
276 write_field_directives(parent_input_object_directives, &field.directives, graph, sdl)?;
277
278 sdl.push('\n');
279 Ok(())
280}
281
282fn write_field(
283 parent_entity_directives: &[Directive],
284 field: &Field,
285 graph: &FederatedGraph,
286 sdl: &mut String,
287) -> fmt::Result {
288 let field_name = &graph[field.name];
289 let field_type = render_field_type(&field.r#type, graph);
290 let args = render_field_arguments(&graph[field.arguments], graph);
291
292 if let Some(description) = field.description {
293 write!(sdl, "{}", Description(&graph[description], INDENT))?;
294 }
295
296 write!(sdl, "{INDENT}{field_name}{args}: {field_type}")?;
297
298 write_field_directives(parent_entity_directives, &field.directives, graph, sdl)?;
299
300 sdl.push('\n');
301 Ok(())
302}
303
304fn display_definition_directives(
305 directives: &[Directive],
306 graph: &FederatedGraph,
307 f: &mut fmt::Formatter<'_>,
308) -> fmt::Result {
309 for directive in directives {
310 f.write_fmt(format_args!("\n{INDENT}"))?;
311 write_directive(f, directive, graph)?;
312 }
313
314 Ok(())
315}
316
317fn write_definition_directives(directives: &[Directive], graph: &FederatedGraph, sdl: &mut String) -> fmt::Result {
318 with_formatter(sdl, |f| display_definition_directives(directives, graph, f))
319}
320
321fn write_field_directives(
322 parent_type_directives: &[Directive],
323 directives: &[Directive],
324 graph: &FederatedGraph,
325 sdl: &mut String,
326) -> fmt::Result {
327 let mut join_field_must_be_present = false;
330 let mut join_field_subgraph_ids = Vec::new();
331 let mut fully_overridden_subgraph_ids = Vec::new();
334
335 for directive in directives {
336 if let Directive::JoinField(dir) = directive {
337 if let (Some(OverrideSource::Subgraph(id)), None | Some(OverrideLabel::Percent(100))) =
338 (dir.r#override.as_ref(), dir.override_label.as_ref())
339 {
340 fully_overridden_subgraph_ids.push(*id);
341 }
342 join_field_subgraph_ids.extend(dir.subgraph_id);
343 join_field_must_be_present |= dir.r#override.is_some()
344 | dir.requires.is_some()
345 | dir.provides.is_some()
346 | dir.r#type.is_some()
347 | dir.external;
348 }
349 }
350
351 if !join_field_must_be_present {
355 let subgraph_ids = {
356 let mut ids = parent_type_directives
357 .iter()
358 .filter_map(|dir| dir.as_join_type())
359 .map(|dir| dir.subgraph_id)
360 .collect::<Vec<_>>();
361 ids.sort_unstable();
362 ids.into_iter().dedup().collect::<Vec<_>>()
363 };
364 join_field_subgraph_ids.sort_unstable();
365 join_field_must_be_present |= subgraph_ids != join_field_subgraph_ids;
366 }
367
368 with_formatter(sdl, |f| {
369 for directive in directives {
370 if let Directive::JoinField(JoinFieldDirective {
371 subgraph_id: Some(subgraph_id),
372 ..
373 }) = &directive
374 && (!join_field_must_be_present || fully_overridden_subgraph_ids.contains(subgraph_id))
375 {
376 continue;
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) -> 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!(
507 actual,
508 "extend schema\n @link(url: \"https://specs.apollo.dev/link/v1.0\")\n @link(url: \"https://specs.apollo.dev/join/v0.3\", for: EXECUTION)\n @link(url: \"https://specs.apollo.dev/inaccessible/v0.2\", for: SECURITY)\n"
509 );
510 }
511
512 #[test]
513 fn escape_strings() {
514 let empty = FederatedGraph::from_sdl(
515 r###"
516 directive @dummy(test: String!) on FIELD
517
518 type Query {
519 field: String @deprecated(reason: "This is a \"deprecated\" reason") @dummy(test: "a \"test\"")
520 }
521 "###,
522 )
523 .unwrap();
524
525 let actual = render_federated_sdl(&empty).expect("valid");
526 insta::assert_snapshot!(actual, @r#"
527 schema
528 @link(url: "https://specs.apollo.dev/link/v1.0")
529 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
530 @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
531 {
532 query: Query
533 }
534
535 directive @dummy(test: String!) on FIELD
536
537 type Query
538 {
539 field: String @deprecated(reason: "This is a \"deprecated\" reason") @dummy(test: "a \"test\"")
540 }
541 "#);
542 }
543
544 #[test]
545 fn multiline_strings() {
546 let empty = FederatedGraph::from_sdl(
547 r###"
548 directive @dummy(test: String!) on FIELD
549
550 type Query {
551 field: String @deprecated(reason: """This is a "deprecated" reason
552
553 on multiple lines.
554
555 yes, way
556
557 """) @dummy(test: "a \"test\"")
558 }
559 "###,
560 )
561 .unwrap();
562
563 let actual = render_federated_sdl(&empty).expect("valid");
564 insta::assert_snapshot!(actual, @r#"
565 schema
566 @link(url: "https://specs.apollo.dev/link/v1.0")
567 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
568 @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
569 {
570 query: Query
571 }
572
573 directive @dummy(test: String!) on FIELD
574
575 type Query
576 {
577 field: String @deprecated(reason: "This is a \"deprecated\" reason\n\non multiple lines.\n\nyes, way") @dummy(test: "a \"test\"")
578 }
579 "#);
580 }
581
582 #[test]
583 fn regression_empty_keys() {
584 let schema = r##"
586 enum join__Graph {
587 a @join__graph(name: "mocksubgraph", url: "https://mock.example.com/todo/graphql")
588 }
589
590 interface b @join__type(graph: a) {
591 c: String
592 }
593 "##;
594
595 let parsed = FederatedGraph::from_sdl(schema).unwrap();
596 let rendered = render_federated_sdl(&parsed).unwrap();
597
598 insta::assert_snapshot!(
599 &rendered,
600 @r#"
601 extend schema
602 @link(url: "https://specs.apollo.dev/link/v1.0")
603 @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
604 @link(url: "https://specs.apollo.dev/inaccessible/v0.2", for: SECURITY)
605
606 interface b
607 @join__type(graph: a)
608 {
609 c: String
610 }
611
612 enum join__Graph
613 {
614 a @join__graph(name: "mocksubgraph", url: "https://mock.example.com/todo/graphql")
615 }
616 "#
617 );
618
619 {
621 FederatedGraph::from_sdl(&rendered).unwrap();
622 }
623 }
624}