diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ba2834..c4c8cec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added impl of `CursorType` for floats [#897](https://github.com/async-graphql/async-graphql/pull/897) - Implement `OutputType` for `tokio::sync::RwLock` and `tokio::sync::Mutex`. [#896](https://github.com/async-graphql/async-graphql/pull/896) - Bump [`uuid`](https://crates.io/crates/uuid) to `1.0.0`. [#907](https://github.com/async-graphql/async-graphql/pull/907/files) +- Add some options for exporting SDL. [#877](https://github.com/async-graphql/async-graphql/issues/877) # [3.0.38] 2022-4-8 diff --git a/src/registry/export_sdl.rs b/src/registry/export_sdl.rs index 19cacff1..24add5ca 100644 --- a/src/registry/export_sdl.rs +++ b/src/registry/export_sdl.rs @@ -2,8 +2,68 @@ use std::fmt::Write; use crate::registry::{Deprecation, MetaField, MetaInputValue, MetaType, Registry}; +const SYSTEM_SCALARS: &[&str] = &["Int", "Float", "String", "Boolean", "ID"]; +const FEDERATION_SCALARS: &[&str] = &["Any"]; + +/// Options for SDL export +#[derive(Debug, Copy, Clone, Default)] +pub struct SDLExportOptions { + sorted_fields: bool, + sorted_arguments: bool, + sorted_enum_values: bool, + federation: bool, +} + +impl SDLExportOptions { + /// Create a `SDLExportOptions` + #[inline] + pub fn new() -> Self { + Default::default() + } + + /// Export sorted fields + #[inline] + #[must_use] + pub fn sorted_fields(self) -> Self { + Self { + sorted_fields: true, + ..self + } + } + + /// Export sorted field arguments + #[inline] + #[must_use] + pub fn sorted_arguments(self) -> Self { + Self { + sorted_arguments: true, + ..self + } + } + + /// Export sorted enum items + #[inline] + #[must_use] + pub fn sorted_enum_items(self) -> Self { + Self { + sorted_enum_values: true, + ..self + } + } + + /// Export as Federation SDL(Schema Definition Language) + #[inline] + #[must_use] + pub fn federation(self) -> Self { + Self { + federation: true, + ..self + } + } +} + impl Registry { - pub fn export_sdl(&self, federation: bool) -> String { + pub(crate) fn export_sdl(&self, options: SDLExportOptions) -> String { let mut sdl = String::new(); let has_oneof = self.types.values().any(|ty| match ty { @@ -22,17 +82,18 @@ impl Registry { continue; } - if federation { + if options.federation { const FEDERATION_TYPES: &[&str] = &["_Any", "_Entity", "_Service"]; if FEDERATION_TYPES.contains(&ty.name()) { continue; } } - self.export_type(ty, &mut sdl, federation); + self.export_type(ty, &mut sdl, &options); + writeln!(sdl).ok(); } - if !federation { + if !options.federation { writeln!(sdl, "schema {{").ok(); writeln!(sdl, "\tquery: {}", self.query_type).ok(); if let Some(mutation_type) = self.mutation_type.as_deref() { @@ -50,11 +111,17 @@ impl Registry { fn export_fields<'a, I: Iterator>( sdl: &mut String, it: I, - federation: bool, + options: &SDLExportOptions, ) { - for field in it { + let mut fields = it.collect::>(); + + if options.sorted_fields { + fields.sort_by(|a, b| a.name.cmp(&b.name)); + } + + for field in fields { if field.name.starts_with("__") - || (federation && matches!(&*field.name, "_service" | "_entities")) + || (options.federation && matches!(&*field.name, "_service" | "_entities")) { continue; } @@ -67,9 +134,16 @@ impl Registry { ) .ok(); } + if !field.args.is_empty() { write!(sdl, "\t{}(", field.name).ok(); - for (i, arg) in field.args.values().enumerate() { + + let mut args = field.args.values().collect::>(); + if options.sorted_arguments { + args.sort_by(|a, b| a.name.cmp(b.name)); + } + + for (i, arg) in args.into_iter().enumerate() { if i != 0 { sdl.push_str(", "); } @@ -86,7 +160,7 @@ impl Registry { write_deprecated(sdl, &field.deprecation); - if federation { + if options.federation { if field.external { write!(sdl, " @external").ok(); } @@ -102,15 +176,13 @@ impl Registry { } } - fn export_type(&self, ty: &MetaType, sdl: &mut String, federation: bool) { + fn export_type(&self, ty: &MetaType, sdl: &mut String, options: &SDLExportOptions) { match ty { MetaType::Scalar { name, description, .. } => { - const SYSTEM_SCALARS: &[&str] = &["Int", "Float", "String", "Boolean", "ID"]; - const FEDERATION_SCALARS: &[&str] = &["Any"]; let mut export_scalar = !SYSTEM_SCALARS.contains(&name.as_str()); - if federation && FEDERATION_SCALARS.contains(&name.as_str()) { + if options.federation && FEDERATION_SCALARS.contains(&name.as_str()) { export_scalar = false; } if export_scalar { @@ -129,23 +201,25 @@ impl Registry { .. } => { if Some(name.as_str()) == self.subscription_type.as_deref() - && federation + && options.federation && !self.federation_subscription { return; } - if name.as_str() == self.query_type && federation { + if name.as_str() == self.query_type && options.federation { let mut field_count = 0; for field in fields.values() { if field.name.starts_with("__") - || (federation && matches!(&*field.name, "_service" | "_entities")) + || (options.federation + && matches!(&*field.name, "_service" | "_entities")) { continue; } field_count += 1; } if field_count == 0 { + // is empty query root type return; } } @@ -153,13 +227,15 @@ impl Registry { if description.is_some() { writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok(); } - if federation && *extends { + + if options.federation && *extends { write!(sdl, "extend ").ok(); } + write!(sdl, "type {} ", name).ok(); self.write_implements(sdl, name); - if federation { + if options.federation { if let Some(keys) = keys { for key in keys { write!(sdl, "@key(fields: \"{}\") ", key).ok(); @@ -168,7 +244,7 @@ impl Registry { } writeln!(sdl, "{{").ok(); - Self::export_fields(sdl, fields.values(), federation); + Self::export_fields(sdl, fields.values(), options); writeln!(sdl, "}}").ok(); } MetaType::Interface { @@ -182,11 +258,13 @@ impl Registry { if description.is_some() { writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok(); } - if federation && *extends { + + if options.federation && *extends { write!(sdl, "extend ").ok(); } write!(sdl, "interface {} ", name).ok(); - if federation { + + if options.federation { if let Some(keys) = keys { for key in keys { write!(sdl, "@key(fields: \"{}\") ", key).ok(); @@ -196,7 +274,7 @@ impl Registry { self.write_implements(sdl, name); writeln!(sdl, "{{").ok(); - Self::export_fields(sdl, fields.values(), federation); + Self::export_fields(sdl, fields.values(), options); writeln!(sdl, "}}").ok(); } MetaType::Enum { @@ -208,13 +286,21 @@ impl Registry { if description.is_some() { writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok(); } + write!(sdl, "enum {} ", name).ok(); writeln!(sdl, "{{").ok(); - for value in enum_values.values() { + + let mut values = enum_values.values().collect::>(); + if options.sorted_enum_values { + values.sort_by(|a, b| a.name.cmp(&b.name)); + } + + for value in values { write!(sdl, "\t{}", value.name).ok(); write_deprecated(sdl, &value.deprecation); writeln!(sdl).ok(); } + writeln!(sdl, "}}").ok(); } MetaType::InputObject { @@ -227,17 +313,26 @@ impl Registry { if description.is_some() { writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok(); } + write!(sdl, "input {} ", name).ok(); + if *oneof { write!(sdl, "@oneof ").ok(); } writeln!(sdl, "{{").ok(); - for field in input_fields.values() { + + let mut fields = input_fields.values().collect::>(); + if options.sorted_fields { + fields.sort_by(|a, b| a.name.cmp(b.name)); + } + + for field in fields { if let Some(description) = field.description { writeln!(sdl, "\t\"\"\"\n\t{}\n\t\"\"\"", description).ok(); } writeln!(sdl, "\t{}", export_input_value(&field)).ok(); } + writeln!(sdl, "}}").ok(); } MetaType::Union { @@ -249,6 +344,7 @@ impl Registry { if description.is_some() { writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok(); } + write!(sdl, "union {} =", name).ok(); for ty in possible_types { write!(sdl, " | {}", ty).ok(); diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 22bffc12..837ba533 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -8,6 +8,7 @@ use std::{ }; pub use cache_control::CacheControl; +pub use export_sdl::SDLExportOptions; use indexmap::{map::IndexMap, set::IndexSet}; pub use crate::model::__DirectiveLocation; diff --git a/src/schema.rs b/src/schema.rs index 43f86cd9..c5a8b8f2 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -13,7 +13,7 @@ use crate::{ types::{Directive, DocumentOperations, OperationType, Selection, SelectionSet}, Positioned, }, - registry::{MetaDirective, MetaInputValue, Registry}, + registry::{MetaDirective, MetaInputValue, Registry, SDLExportOptions}, resolver_utils::{resolve_container, resolve_container_serial}, subscription::collect_subscription_streams, types::QueryRoot, @@ -421,12 +421,12 @@ where /// Returns SDL(Schema Definition Language) of this schema. pub fn sdl(&self) -> String { - self.0.env.registry.export_sdl(false) + self.0.env.registry.export_sdl(Default::default()) } - /// Returns Federation SDL(Schema Definition Language) of this schema. - pub fn federation_sdl(&self) -> String { - self.0.env.registry.export_sdl(true) + /// Returns SDL(Schema Definition Language) of this schema with options. + pub fn sdl_with_options(&self, options: SDLExportOptions) -> String { + self.0.env.registry.export_sdl(options) } /// Get all names in this schema diff --git a/src/types/query_root.rs b/src/types/query_root.rs index efc83e13..7d817e53 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -5,7 +5,7 @@ use indexmap::map::IndexMap; use crate::{ model::{__Schema, __Type}, parser::types::Field, - registry, + registry::{self, SDLExportOptions}, resolver_utils::{resolve_container, ContainerType}, schema::IntrospectionMode, Any, Context, ContextSelectionSet, ObjectType, OutputType, Positioned, ServerError, @@ -84,7 +84,11 @@ impl ContainerType for QueryRoot { let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set); return OutputType::resolve( &Service { - sdl: Some(ctx.schema_env.registry.export_sdl(true)), + sdl: Some( + ctx.schema_env + .registry + .export_sdl(SDLExportOptions::new().federation()), + ), }, &ctx_obj, ctx.item,