async_graphql/registry/
export_sdl.rs

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