1use std::{collections::HashMap, fmt::Write};
2
3use crate::registry::{Deprecation, MetaField, MetaInputValue, MetaType, Registry};
4
5const SYSTEM_SCALARS: &[&str] = &["Int", "Float", "String", "Boolean", "ID"];
6const FEDERATION_SCALARS: &[&str] = &["Any"];
7
8#[derive(Debug, Copy, Clone, Default)]
10pub struct SDLExportOptions {
11 sorted_fields: bool,
12 sorted_arguments: bool,
13 sorted_enum_values: bool,
14 federation: bool,
15 prefer_single_line_descriptions: bool,
16 include_specified_by: bool,
17 compose_directive: bool,
18}
19
20impl SDLExportOptions {
21 #[inline]
23 pub fn new() -> Self {
24 Default::default()
25 }
26
27 #[inline]
29 #[must_use]
30 pub fn sorted_fields(self) -> Self {
31 Self {
32 sorted_fields: true,
33 ..self
34 }
35 }
36
37 #[inline]
39 #[must_use]
40 pub fn sorted_arguments(self) -> Self {
41 Self {
42 sorted_arguments: true,
43 ..self
44 }
45 }
46
47 #[inline]
49 #[must_use]
50 pub fn sorted_enum_items(self) -> Self {
51 Self {
52 sorted_enum_values: true,
53 ..self
54 }
55 }
56
57 #[inline]
59 #[must_use]
60 pub fn federation(self) -> Self {
61 Self {
62 federation: true,
63 ..self
64 }
65 }
66
67 #[inline]
69 #[must_use]
70 pub fn prefer_single_line_descriptions(self) -> Self {
71 Self {
72 prefer_single_line_descriptions: true,
73 ..self
74 }
75 }
76
77 pub fn include_specified_by(self) -> Self {
79 Self {
80 include_specified_by: true,
81 ..self
82 }
83 }
84
85 pub fn compose_directive(self) -> Self {
87 Self {
88 compose_directive: true,
89 ..self
90 }
91 }
92}
93
94impl Registry {
95 pub(crate) fn export_sdl(&self, options: SDLExportOptions) -> String {
96 let mut sdl = String::new();
97
98 for ty in self.types.values() {
99 if ty.name().starts_with("__") {
100 continue;
101 }
102
103 if options.federation {
104 const FEDERATION_TYPES: &[&str] = &["_Any", "_Entity", "_Service"];
105 if FEDERATION_TYPES.contains(&ty.name()) {
106 continue;
107 }
108 }
109
110 self.export_type(ty, &mut sdl, &options);
111 writeln!(sdl).ok();
112 }
113
114 self.directives.values().for_each(|directive| {
115 if directive.name == "deprecated"
117 && !self.types.values().any(|ty| match ty {
118 MetaType::Object { fields, .. } => fields
119 .values()
120 .any(|field| field.deprecation.is_deprecated()),
121 MetaType::Enum { enum_values, .. } => enum_values
122 .values()
123 .any(|value| value.deprecation.is_deprecated()),
124 _ => false,
125 })
126 {
127 return;
128 }
129
130 if directive.name == "specifiedBy"
132 && !self.types.values().any(|ty| {
133 matches!(
134 ty,
135 MetaType::Scalar {
136 specified_by_url: Some(_),
137 ..
138 }
139 )
140 })
141 {
142 return;
143 }
144
145 if directive.name == "oneOf"
147 && !self
148 .types
149 .values()
150 .any(|ty| matches!(ty, MetaType::InputObject { oneof: true, .. }))
151 {
152 return;
153 }
154
155 writeln!(sdl, "{}", directive.sdl()).ok();
156 });
157
158 if options.federation {
159 writeln!(sdl, "extend schema @link(").ok();
160 writeln!(sdl, "\turl: \"https://specs.apollo.dev/federation/v2.3\",").ok();
161 writeln!(sdl, "\timport: [\"@key\", \"@tag\", \"@shareable\", \"@inaccessible\", \"@override\", \"@external\", \"@provides\", \"@requires\", \"@composeDirective\", \"@interfaceObject\"]").ok();
162 writeln!(sdl, ")").ok();
163
164 if options.compose_directive {
165 writeln!(sdl).ok();
166 let mut compose_directives = HashMap::<&str, Vec<String>>::new();
167 self.directives
168 .values()
169 .filter_map(|d| {
170 d.composable
171 .as_ref()
172 .map(|ext_url| (ext_url, format!("\"@{}\"", d.name)))
173 })
174 .for_each(|(ext_url, name)| {
175 compose_directives.entry(ext_url).or_default().push(name)
176 });
177 for (url, directives) in compose_directives {
178 writeln!(sdl, "extend schema @link(").ok();
179 writeln!(sdl, "\turl: \"{}\"", url).ok();
180 writeln!(sdl, "\timport: [{}]", directives.join(",")).ok();
181 writeln!(sdl, ")").ok();
182 for name in directives {
183 writeln!(sdl, "\t@composeDirective(name: {})", name).ok();
184 }
185 writeln!(sdl).ok();
186 }
187 }
188 } else {
189 writeln!(sdl, "schema {{").ok();
190 writeln!(sdl, "\tquery: {}", self.query_type).ok();
191 if let Some(mutation_type) = self.mutation_type.as_deref() {
192 writeln!(sdl, "\tmutation: {}", mutation_type).ok();
193 }
194 if let Some(subscription_type) = self.subscription_type.as_deref() {
195 writeln!(sdl, "\tsubscription: {}", subscription_type).ok();
196 }
197 writeln!(sdl, "}}").ok();
198 }
199
200 sdl
201 }
202
203 fn export_fields<'a, I: Iterator<Item = &'a MetaField>>(
204 sdl: &mut String,
205 it: I,
206 options: &SDLExportOptions,
207 ) {
208 let mut fields = it.collect::<Vec<_>>();
209
210 if options.sorted_fields {
211 fields.sort_by_key(|field| &field.name);
212 }
213
214 for field in fields {
215 if field.name.starts_with("__")
216 || (options.federation && matches!(&*field.name, "_service" | "_entities"))
217 {
218 continue;
219 }
220
221 if let Some(description) = &field.description {
222 write_description(sdl, options, 1, description);
223 }
224
225 if !field.args.is_empty() {
226 write!(sdl, "\t{}(", field.name).ok();
227
228 let mut args = field.args.values().collect::<Vec<_>>();
229 if options.sorted_arguments {
230 args.sort_by_key(|value| &value.name);
231 }
232
233 let need_multiline = args.iter().any(|x| x.description.is_some());
234
235 for (i, arg) in args.into_iter().enumerate() {
236 if i != 0 {
237 sdl.push(',');
238 }
239
240 if let Some(description) = &arg.description {
241 writeln!(sdl).ok();
242 write_description(sdl, options, 2, description);
243 }
244
245 if need_multiline {
246 write!(sdl, "\t\t").ok();
247 } else if i != 0 {
248 sdl.push(' ');
249 }
250
251 write_input_value(sdl, arg);
252
253 if options.federation {
254 if arg.inaccessible {
255 write!(sdl, " @inaccessible").ok();
256 }
257
258 for tag in &arg.tags {
259 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
260 }
261 }
262
263 for directive in &arg.directive_invocations {
264 write!(sdl, " {}", directive.sdl()).ok();
265 }
266 }
267
268 if need_multiline {
269 sdl.push_str("\n\t");
270 }
271 write!(sdl, "): {}", field.ty).ok();
272 } else {
273 write!(sdl, "\t{}: {}", field.name, field.ty).ok();
274 }
275
276 write_deprecated(sdl, &field.deprecation);
277
278 for directive in &field.directive_invocations {
279 write!(sdl, " {}", directive.sdl()).ok();
280 }
281
282 if options.federation {
283 if field.external {
284 write!(sdl, " @external").ok();
285 }
286 if let Some(requires) = &field.requires {
287 write!(sdl, " @requires(fields: \"{}\")", requires).ok();
288 }
289 if let Some(provides) = &field.provides {
290 write!(sdl, " @provides(fields: \"{}\")", provides).ok();
291 }
292 if field.shareable {
293 write!(sdl, " @shareable").ok();
294 }
295 if field.inaccessible {
296 write!(sdl, " @inaccessible").ok();
297 }
298 for tag in &field.tags {
299 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
300 }
301 if let Some(from) = &field.override_from {
302 write!(sdl, " @override(from: \"{}\")", from).ok();
303 }
304 }
305
306 writeln!(sdl).ok();
307 }
308 }
309
310 fn export_type(&self, ty: &MetaType, sdl: &mut String, options: &SDLExportOptions) {
311 match ty {
312 MetaType::Scalar {
313 name,
314 description,
315 inaccessible,
316 tags,
317 specified_by_url,
318 directive_invocations,
319 ..
320 } => {
321 let mut export_scalar = !SYSTEM_SCALARS.contains(&name.as_str());
322 if options.federation && FEDERATION_SCALARS.contains(&name.as_str()) {
323 export_scalar = false;
324 }
325 if export_scalar {
326 if let Some(description) = description {
327 write_description(sdl, options, 0, description);
328 }
329 write!(sdl, "scalar {}", name).ok();
330
331 if options.include_specified_by {
332 if let Some(specified_by_url) = specified_by_url {
333 write!(
334 sdl,
335 " @specifiedBy(url: \"{}\")",
336 specified_by_url.replace('"', "\\\"")
337 )
338 .ok();
339 }
340 }
341
342 if options.federation {
343 if *inaccessible {
344 write!(sdl, " @inaccessible").ok();
345 }
346 for tag in tags {
347 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
348 }
349 }
350
351 for directive in directive_invocations {
352 write!(sdl, " {}", directive.sdl()).ok();
353 }
354
355 writeln!(sdl).ok();
356 }
357 }
358 MetaType::Object {
359 name,
360 fields,
361 extends,
362 keys,
363 description,
364 shareable,
365 resolvable,
366 inaccessible,
367 interface_object,
368 tags,
369 directive_invocations: raw_directives,
370 ..
371 } => {
372 if Some(name.as_str()) == self.subscription_type.as_deref()
373 && options.federation
374 && !self.federation_subscription
375 {
376 return;
377 }
378
379 if name.as_str() == self.query_type && options.federation {
380 let mut field_count = 0;
381 for field in fields.values() {
382 if field.name.starts_with("__")
383 || (options.federation
384 && matches!(&*field.name, "_service" | "_entities"))
385 {
386 continue;
387 }
388 field_count += 1;
389 }
390 if field_count == 0 {
391 return;
393 }
394 }
395
396 if let Some(description) = description {
397 write_description(sdl, options, 0, description);
398 }
399
400 if options.federation && *extends {
401 write!(sdl, "extend ").ok();
402 }
403
404 write!(sdl, "type {}", name).ok();
405 self.write_implements(sdl, name);
406
407 for directive_invocation in raw_directives {
408 write!(sdl, " {}", directive_invocation.sdl()).ok();
409 }
410
411 if options.federation {
412 if let Some(keys) = keys {
413 for key in keys {
414 write!(sdl, " @key(fields: \"{}\"", key).ok();
415 if !resolvable {
416 write!(sdl, ", resolvable: false").ok();
417 }
418 write!(sdl, ")").ok();
419 }
420 }
421 if *shareable {
422 write!(sdl, " @shareable").ok();
423 }
424
425 if *inaccessible {
426 write!(sdl, " @inaccessible").ok();
427 }
428
429 if *interface_object {
430 write!(sdl, " @interfaceObject").ok();
431 }
432
433 for tag in tags {
434 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
435 }
436 }
437
438 writeln!(sdl, " {{").ok();
439 Self::export_fields(sdl, fields.values(), options);
440 writeln!(sdl, "}}").ok();
441 }
442 MetaType::Interface {
443 name,
444 fields,
445 extends,
446 keys,
447 description,
448 inaccessible,
449 tags,
450 directive_invocations,
451 ..
452 } => {
453 if let Some(description) = description {
454 write_description(sdl, options, 0, description);
455 }
456
457 if options.federation && *extends {
458 write!(sdl, "extend ").ok();
459 }
460 write!(sdl, "interface {}", name).ok();
461
462 if options.federation {
463 if let Some(keys) = keys {
464 for key in keys {
465 write!(sdl, " @key(fields: \"{}\")", key).ok();
466 }
467 }
468 if *inaccessible {
469 write!(sdl, " @inaccessible").ok();
470 }
471
472 for tag in tags {
473 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
474 }
475 }
476
477 for directive in directive_invocations {
478 write!(sdl, " {}", directive.sdl()).ok();
479 }
480
481 self.write_implements(sdl, name);
482
483 writeln!(sdl, " {{").ok();
484 Self::export_fields(sdl, fields.values(), options);
485 writeln!(sdl, "}}").ok();
486 }
487 MetaType::Enum {
488 name,
489 enum_values,
490 description,
491 inaccessible,
492 tags,
493 directive_invocations,
494 ..
495 } => {
496 if let Some(description) = description {
497 write_description(sdl, options, 0, description);
498 }
499
500 write!(sdl, "enum {}", name).ok();
501 if options.federation {
502 if *inaccessible {
503 write!(sdl, " @inaccessible").ok();
504 }
505 for tag in tags {
506 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
507 }
508 }
509
510 for directive in directive_invocations {
511 write!(sdl, " {}", directive.sdl()).ok();
512 }
513
514 writeln!(sdl, " {{").ok();
515
516 let mut values = enum_values.values().collect::<Vec<_>>();
517 if options.sorted_enum_values {
518 values.sort_by_key(|value| &value.name);
519 }
520
521 for value in values {
522 if let Some(description) = &value.description {
523 write_description(sdl, options, 1, description);
524 }
525 write!(sdl, "\t{}", value.name).ok();
526 write_deprecated(sdl, &value.deprecation);
527
528 if options.federation {
529 if value.inaccessible {
530 write!(sdl, " @inaccessible").ok();
531 }
532
533 for tag in &value.tags {
534 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
535 }
536 }
537
538 for directive in &value.directive_invocations {
539 write!(sdl, " {}", directive.sdl()).ok();
540 }
541
542 writeln!(sdl).ok();
543 }
544
545 writeln!(sdl, "}}").ok();
546 }
547 MetaType::InputObject {
548 name,
549 input_fields,
550 description,
551 inaccessible,
552 tags,
553 oneof,
554 directive_invocations: raw_directives,
555 ..
556 } => {
557 if let Some(description) = description {
558 write_description(sdl, options, 0, description);
559 }
560
561 write!(sdl, "input {}", name).ok();
562
563 if *oneof {
564 write!(sdl, " @oneOf").ok();
565 }
566 if options.federation {
567 if *inaccessible {
568 write!(sdl, " @inaccessible").ok();
569 }
570 for tag in tags {
571 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
572 }
573 }
574
575 for directive in raw_directives {
576 write!(sdl, " {}", directive.sdl()).ok();
577 }
578
579 writeln!(sdl, " {{").ok();
580
581 let mut fields = input_fields.values().collect::<Vec<_>>();
582 if options.sorted_fields {
583 fields.sort_by_key(|value| &value.name);
584 }
585
586 for field in fields {
587 if let Some(ref description) = &field.description {
588 write_description(sdl, options, 1, description);
589 }
590 sdl.push('\t');
591 write_input_value(sdl, field);
592 if options.federation {
593 if field.inaccessible {
594 write!(sdl, " @inaccessible").ok();
595 }
596 for tag in &field.tags {
597 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
598 }
599 }
600 for directive in &field.directive_invocations {
601 write!(sdl, " {}", directive.sdl()).ok();
602 }
603 writeln!(sdl).ok();
604 }
605
606 writeln!(sdl, "}}").ok();
607 }
608 MetaType::Union {
609 name,
610 possible_types,
611 description,
612 inaccessible,
613 tags,
614 directive_invocations,
615 ..
616 } => {
617 if let Some(description) = description {
618 write_description(sdl, options, 0, description);
619 }
620
621 write!(sdl, "union {}", name).ok();
622 if options.federation {
623 if *inaccessible {
624 write!(sdl, " @inaccessible").ok();
625 }
626 for tag in tags {
627 write!(sdl, " @tag(name: \"{}\")", tag.replace('"', "\\\"")).ok();
628 }
629 }
630
631 for directive in directive_invocations {
632 write!(sdl, " {}", directive.sdl()).ok();
633 }
634
635 write!(sdl, " =").ok();
636
637 for (idx, ty) in possible_types.iter().enumerate() {
638 if idx == 0 {
639 write!(sdl, " {}", ty).ok();
640 } else {
641 write!(sdl, " | {}", ty).ok();
642 }
643 }
644 writeln!(sdl).ok();
645 }
646 }
647 }
648
649 fn write_implements(&self, sdl: &mut String, name: &str) {
650 if let Some(implements) = self.implements.get(name) {
651 if !implements.is_empty() {
652 write!(
653 sdl,
654 " implements {}",
655 implements
656 .iter()
657 .map(AsRef::as_ref)
658 .collect::<Vec<&str>>()
659 .join(" & ")
660 )
661 .ok();
662 }
663 }
664 }
665}
666
667fn write_description(
668 sdl: &mut String,
669 options: &SDLExportOptions,
670 level: usize,
671 description: &str,
672) {
673 let tabs = "\t".repeat(level);
674
675 if options.prefer_single_line_descriptions && !description.contains('\n') {
676 let description = description.replace('"', r#"\""#);
677 writeln!(sdl, "{tabs}\"{description}\"").ok();
678 } else {
679 let description = description.replace('\n', &format!("\n{tabs}"));
680 writeln!(sdl, "{tabs}\"\"\"\n{tabs}{description}\n{tabs}\"\"\"").ok();
681 }
682}
683
684fn write_input_value(sdl: &mut String, input_value: &MetaInputValue) {
685 if let Some(default_value) = &input_value.default_value {
686 _ = write!(
687 sdl,
688 "{}: {} = {}",
689 input_value.name, input_value.ty, default_value
690 );
691 } else {
692 _ = write!(sdl, "{}: {}", input_value.name, input_value.ty);
693 }
694
695 write_deprecated(sdl, &input_value.deprecation);
696}
697
698fn write_deprecated(sdl: &mut String, deprecation: &Deprecation) {
699 if let Deprecation::Deprecated { reason } = deprecation {
700 let _ = match reason {
701 Some(reason) => write!(sdl, " @deprecated(reason: \"{}\")", escape_string(reason)).ok(),
702 None => write!(sdl, " @deprecated").ok(),
703 };
704 }
705}
706
707fn escape_string(s: &str) -> String {
708 let mut res = String::new();
709
710 for c in s.chars() {
711 let ec = match c {
712 '\\' => Some("\\\\"),
713 '\x08' => Some("\\b"),
714 '\x0c' => Some("\\f"),
715 '\n' => Some("\\n"),
716 '\r' => Some("\\r"),
717 '\t' => Some("\\t"),
718 _ => None,
719 };
720 match ec {
721 Some(ec) => {
722 res.write_str(ec).ok();
723 }
724 None => {
725 res.write_char(c).ok();
726 }
727 }
728 }
729
730 res
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use crate::{model::__DirectiveLocation, registry::MetaDirective};
737
738 #[test]
739 fn test_escape_string() {
740 assert_eq!(
741 escape_string("1\\\x08d\x0c3\n4\r5\t6"),
742 "1\\\\\\bd\\f3\\n4\\r5\\t6"
743 );
744 }
745
746 #[test]
747 fn test_compose_directive_dsl() {
748 let expected = r#"directive @custom_type_directive on FIELD_DEFINITION
749extend schema @link(
750 url: "https://specs.apollo.dev/federation/v2.3",
751 import: ["@key", "@tag", "@shareable", "@inaccessible", "@override", "@external", "@provides", "@requires", "@composeDirective", "@interfaceObject"]
752)
753
754extend schema @link(
755 url: "https://custom.spec.dev/extension/v1.0"
756 import: ["@custom_type_directive"]
757)
758 @composeDirective(name: "@custom_type_directive")
759
760"#;
761 let mut registry = Registry::default();
762 registry.add_directive(MetaDirective {
763 name: "custom_type_directive".to_string(),
764 description: None,
765 locations: vec![__DirectiveLocation::FIELD_DEFINITION],
766 args: Default::default(),
767 is_repeatable: false,
768 visible: None,
769 composable: Some("https://custom.spec.dev/extension/v1.0".to_string()),
770 });
771 let dsl = registry.export_sdl(SDLExportOptions::new().federation().compose_directive());
772 assert_eq!(dsl, expected)
773 }
774
775 #[test]
776 fn test_type_directive_sdl_without_federation() {
777 let expected = r#"directive @custom_type_directive(optionalWithoutDefault: String, optionalWithDefault: String = "DEFAULT") on FIELD_DEFINITION | OBJECT
778schema {
779 query: Query
780}
781"#;
782 let mut registry = Registry::default();
783 registry.add_directive(MetaDirective {
784 name: "custom_type_directive".to_string(),
785 description: None,
786 locations: vec![
787 __DirectiveLocation::FIELD_DEFINITION,
788 __DirectiveLocation::OBJECT,
789 ],
790 args: [
791 (
792 "optionalWithoutDefault".to_string(),
793 MetaInputValue {
794 name: "optionalWithoutDefault".to_string(),
795 description: None,
796 ty: "String".to_string(),
797 deprecation: Deprecation::NoDeprecated,
798 default_value: None,
799 visible: None,
800 inaccessible: false,
801 tags: vec![],
802 is_secret: false,
803 directive_invocations: vec![],
804 },
805 ),
806 (
807 "optionalWithDefault".to_string(),
808 MetaInputValue {
809 name: "optionalWithDefault".to_string(),
810 description: None,
811 ty: "String".to_string(),
812 deprecation: Deprecation::NoDeprecated,
813 default_value: Some("\"DEFAULT\"".to_string()),
814 visible: None,
815 inaccessible: false,
816 tags: vec![],
817 is_secret: false,
818 directive_invocations: vec![],
819 },
820 ),
821 ]
822 .into(),
823 is_repeatable: false,
824 visible: None,
825 composable: None,
826 });
827 registry.query_type = "Query".to_string();
828 let sdl = registry.export_sdl(SDLExportOptions::new());
829 assert_eq!(sdl, expected)
830 }
831}