diff --git a/src/registry/export_sdl.rs b/src/registry/export_sdl.rs new file mode 100644 index 00000000..640d0420 --- /dev/null +++ b/src/registry/export_sdl.rs @@ -0,0 +1,204 @@ +use crate::registry::{MetaField, MetaInputValue, MetaType, Registry}; +use itertools::Itertools; +use std::fmt::Write; + +impl Registry { + pub fn export_sdl(&self, federation: bool) -> String { + let mut sdl = String::new(); + + for ty in self.types.values() { + if ty.name().starts_with("__") { + continue; + } + + if federation { + const FEDERATION_TYPES: &[&str] = &["_Any", "_Entity", "_Service"]; + if FEDERATION_TYPES.contains(&ty.name()) { + continue; + } + } + + self.export_type(ty, &mut sdl, federation); + } + + if !federation { + writeln!(sdl, "schema {{").ok(); + writeln!(sdl, "\tquery: {}", self.query_type).ok(); + if let Some(mutation_type) = self.mutation_type.as_deref() { + writeln!(sdl, "\tmutation: {}", mutation_type).ok(); + } + if let Some(subscription_type) = self.subscription_type.as_deref() { + writeln!(sdl, "\tsubscription: {}", subscription_type).ok(); + } + writeln!(sdl, "}}").ok(); + } + + sdl + } + + fn export_fields<'a, I: Iterator>( + sdl: &mut String, + it: I, + federation: bool, + ) { + for field in it { + if field.name.starts_with("__") + || (federation && matches!(&*field.name, "_service" | "_entities")) + { + continue; + } + + if !field.args.is_empty() { + write!( + sdl, + "\t{}({}): {}", + field.name, + field + .args + .values() + .map(|arg| export_input_value(arg)) + .join(", "), + field.ty + ) + .ok(); + } else { + write!(sdl, "\t{}: {}", field.name, field.ty).ok(); + } + + if federation { + if field.external { + write!(sdl, " @external").ok(); + } + if let Some(requires) = field.requires { + write!(sdl, " @requires(fields: \"{}\")", requires).ok(); + } + if let Some(provides) = field.provides { + write!(sdl, " @provides(fields: \"{}\")", provides).ok(); + } + } + + writeln!(sdl).ok(); + } + } + + fn export_type(&self, ty: &MetaType, sdl: &mut String, federation: bool) { + match ty { + MetaType::Scalar { name, .. } => { + 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()) { + export_scalar = false; + } + if export_scalar { + writeln!(sdl, "scalar {}", name).ok(); + } + } + MetaType::Object { + name, + fields, + extends, + keys, + .. + } => { + if name == &self.query_type && self.is_empty_query { + return; + } + + if let Some(subscription_type) = &self.subscription_type { + if name == subscription_type && federation { + return; + } + } + + if federation && *extends { + write!(sdl, "extend ").ok(); + } + write!(sdl, "type {} ", name).ok(); + if let Some(implements) = self.implements.get(name) { + if !implements.is_empty() { + write!(sdl, "implements {} ", implements.iter().join(" & ")).ok(); + } + } + + if federation { + if let Some(keys) = keys { + for key in keys { + write!(sdl, "@key(fields: \"{}\") ", key).ok(); + } + } + } + + writeln!(sdl, "{{").ok(); + Self::export_fields(sdl, fields.values(), federation); + writeln!(sdl, "}}").ok(); + } + MetaType::Interface { + name, + fields, + extends, + keys, + .. + } => { + if federation && *extends { + write!(sdl, "extend ").ok(); + } + write!(sdl, "interface {} ", name).ok(); + if federation { + if let Some(keys) = keys { + for key in keys { + write!(sdl, "@key(fields: \"{}\") ", key).ok(); + } + } + } + writeln!(sdl, "{{").ok(); + Self::export_fields(sdl, fields.values(), federation); + writeln!(sdl, "}}").ok(); + } + MetaType::Enum { + name, enum_values, .. + } => { + write!(sdl, "enum {} ", name).ok(); + writeln!(sdl, "{{").ok(); + for value in enum_values.values() { + writeln!(sdl, "\t{}", value.name).ok(); + } + writeln!(sdl, "}}").ok(); + } + MetaType::InputObject { + name, input_fields, .. + } => { + write!(sdl, "input {} ", name).ok(); + writeln!(sdl, "{{").ok(); + for field in input_fields.values() { + writeln!(sdl, "{}", export_input_value(&field)).ok(); + } + writeln!(sdl, "}}").ok(); + } + MetaType::Union { + name, + possible_types, + .. + } => { + writeln!( + sdl, + "union {} = {}", + name, + possible_types.iter().join(" | ") + ) + .ok(); + } + } + } +} + +fn export_input_value(input_value: &MetaInputValue) -> String { + if let Some(default_value) = &input_value.default_value { + format!( + "{}: {} = {}", + input_value.name, input_value.ty, default_value + ) + } else { + format!("{}: {}", input_value.name, input_value.ty) + } +} diff --git a/src/registry/federation.rs b/src/registry/federation.rs deleted file mode 100644 index 17074cb7..00000000 --- a/src/registry/federation.rs +++ /dev/null @@ -1,248 +0,0 @@ -use crate::registry::{MetaField, MetaInputValue, MetaType, Registry}; -use crate::{Any, Type}; -use indexmap::IndexMap; -use itertools::Itertools; -use std::fmt::Write; - -impl Registry { - pub fn create_federation_sdl(&self) -> String { - let mut sdl = String::new(); - for ty in self.types.values() { - if ty.name().starts_with("__") { - continue; - } - const FEDERATION_TYPES: &[&str] = &["_Any", "_Entity", "_Service"]; - if FEDERATION_TYPES.contains(&ty.name()) { - continue; - } - self.create_federation_type(ty, &mut sdl); - } - sdl - } - - pub fn create_federation_types(&mut self) { - Any::create_type_info(self); - - self.types.insert( - "_Service".to_string(), - MetaType::Object { - name: "_Service".to_string(), - description: None, - fields: { - let mut fields = IndexMap::new(); - fields.insert( - "sdl".to_string(), - MetaField { - name: "sdl".to_string(), - description: None, - args: Default::default(), - ty: "String".to_string(), - deprecation: None, - cache_control: Default::default(), - external: false, - requires: None, - provides: None, - }, - ); - fields - }, - cache_control: Default::default(), - extends: false, - keys: None, - }, - ); - - self.create_entity_type(); - - let query_root = self.types.get_mut(&self.query_type).unwrap(); - if let MetaType::Object { fields, .. } = query_root { - fields.insert( - "_service".to_string(), - MetaField { - name: "_service".to_string(), - description: None, - args: Default::default(), - ty: "_Service!".to_string(), - deprecation: None, - cache_control: Default::default(), - external: false, - requires: None, - provides: None, - }, - ); - - fields.insert( - "_entities".to_string(), - MetaField { - name: "_entities".to_string(), - description: None, - args: { - let mut args = IndexMap::new(); - args.insert( - "representations", - MetaInputValue { - name: "representations", - description: None, - ty: "[_Any!]!".to_string(), - default_value: None, - validator: None, - }, - ); - args - }, - ty: "[_Entity]!".to_string(), - deprecation: None, - cache_control: Default::default(), - external: false, - requires: None, - provides: None, - }, - ); - } - } - - fn create_federation_fields<'a, I: Iterator>(sdl: &mut String, it: I) { - for field in it { - if field.name.starts_with("__") || matches!(&*field.name, "_service" | "_entities") { - continue; - } - - if !field.args.is_empty() { - write!( - sdl, - "\t{}({}): {}", - field.name, - field - .args - .values() - .map(|arg| federation_input_value(arg)) - .join(", "), - field.ty - ) - .ok(); - } else { - write!(sdl, "\t{}: {}", field.name, field.ty).ok(); - } - - if field.external { - write!(sdl, " @external").ok(); - } - if let Some(requires) = field.requires { - write!(sdl, " @requires(fields: \"{}\")", requires).ok(); - } - if let Some(provides) = field.provides { - write!(sdl, " @provides(fields: \"{}\")", provides).ok(); - } - writeln!(sdl).ok(); - } - } - - fn create_federation_type(&self, ty: &MetaType, sdl: &mut String) { - match ty { - MetaType::Scalar { name, .. } => { - const SYSTEM_SCALARS: &[&str] = &["Int", "Float", "String", "Boolean", "ID", "Any"]; - if !SYSTEM_SCALARS.contains(&name.as_str()) { - writeln!(sdl, "scalar {}", name).ok(); - } - } - MetaType::Object { - name, - fields, - extends, - keys, - .. - } => { - if name == &self.query_type && fields.len() == 4 { - // Is empty query root, only __schema, __type, _service, _entities fields - return; - } - if let Some(subscription_type) = &self.subscription_type { - if name == subscription_type { - return; - } - } - if *extends { - write!(sdl, "extend ").ok(); - } - write!(sdl, "type {} ", name).ok(); - if let Some(implements) = self.implements.get(name) { - if !implements.is_empty() { - write!(sdl, "implements {}", implements.iter().join(" & ")).ok(); - } - } - if let Some(keys) = keys { - for key in keys { - write!(sdl, "@key(fields: \"{}\") ", key).ok(); - } - } - writeln!(sdl, "{{").ok(); - Self::create_federation_fields(sdl, fields.values()); - writeln!(sdl, "}}").ok(); - } - MetaType::Interface { - name, - fields, - extends, - keys, - .. - } => { - if *extends { - write!(sdl, "extend ").ok(); - } - write!(sdl, "interface {} ", name).ok(); - if let Some(keys) = keys { - for key in keys { - write!(sdl, "@key(fields: \"{}\") ", key).ok(); - } - } - writeln!(sdl, "{{").ok(); - Self::create_federation_fields(sdl, fields.values()); - writeln!(sdl, "}}").ok(); - } - MetaType::Enum { - name, enum_values, .. - } => { - write!(sdl, "enum {} ", name).ok(); - writeln!(sdl, "{{").ok(); - for value in enum_values.values() { - writeln!(sdl, "{}", value.name).ok(); - } - writeln!(sdl, "}}").ok(); - } - MetaType::InputObject { - name, input_fields, .. - } => { - write!(sdl, "input {} ", name).ok(); - writeln!(sdl, "{{").ok(); - for field in input_fields.values() { - writeln!(sdl, "{}", federation_input_value(&field)).ok(); - } - writeln!(sdl, "}}").ok(); - } - MetaType::Union { - name, - possible_types, - .. - } => { - writeln!( - sdl, - "union {} = {}", - name, - possible_types.iter().join(" | ") - ) - .ok(); - } - } - } -} - -fn federation_input_value(input_value: &MetaInputValue) -> String { - if let Some(default_value) = &input_value.default_value { - format!( - "{}: {} = {}", - input_value.name, input_value.ty, default_value - ) - } else { - format!("{}: {}", input_value.name, input_value.ty) - } -} diff --git a/src/registry/mod.rs b/src/registry/mod.rs index f0748599..9d5a69d4 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -1,9 +1,9 @@ mod cache_control; -mod federation; +mod export_sdl; use crate::parser::types::{BaseType as ParsedBaseType, Type as ParsedType}; use crate::validators::InputValueValidator; -use crate::{model, Value}; +use crate::{model, Any, Type, Value}; use indexmap::map::IndexMap; use indexmap::set::IndexSet; use std::collections::{HashMap, HashSet}; @@ -253,10 +253,11 @@ pub struct MetaDirective { } pub struct Registry { - pub types: HashMap, + pub types: IndexMap, pub directives: HashMap, pub implements: HashMap>, pub query_type: String, + pub is_empty_query: bool, pub mutation_type: Option, pub subscription_type: Option, } @@ -368,4 +369,85 @@ impl Registry { }, ); } + + pub(crate) fn create_federation_types(&mut self) { + Any::create_type_info(self); + + self.types.insert( + "_Service".to_string(), + MetaType::Object { + name: "_Service".to_string(), + description: None, + fields: { + let mut fields = IndexMap::new(); + fields.insert( + "sdl".to_string(), + MetaField { + name: "sdl".to_string(), + description: None, + args: Default::default(), + ty: "String".to_string(), + deprecation: None, + cache_control: Default::default(), + external: false, + requires: None, + provides: None, + }, + ); + fields + }, + cache_control: Default::default(), + extends: false, + keys: None, + }, + ); + + self.create_entity_type(); + + let query_root = self.types.get_mut(&self.query_type).unwrap(); + if let MetaType::Object { fields, .. } = query_root { + fields.insert( + "_service".to_string(), + MetaField { + name: "_service".to_string(), + description: None, + args: Default::default(), + ty: "_Service!".to_string(), + deprecation: None, + cache_control: Default::default(), + external: false, + requires: None, + provides: None, + }, + ); + + fields.insert( + "_entities".to_string(), + MetaField { + name: "_entities".to_string(), + description: None, + args: { + let mut args = IndexMap::new(); + args.insert( + "representations", + MetaInputValue { + name: "representations", + description: None, + ty: "[_Any!]!".to_string(), + default_value: None, + validator: None, + }, + ); + args + }, + ty: "[_Entity]!".to_string(), + deprecation: None, + cache_control: Default::default(), + external: false, + requires: None, + provides: None, + }, + ); + } + } } diff --git a/src/schema.rs b/src/schema.rs index 067ce117..f9ebfc29 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -197,10 +197,29 @@ where mutation: Mutation, subscription: Subscription, ) -> SchemaBuilder { + SchemaBuilder { + validation_mode: ValidationMode::Strict, + query: QueryRoot { + inner: query, + disable_introspection: false, + }, + mutation, + subscription, + registry: Self::create_registry(), + data: Default::default(), + complexity: None, + depth: None, + extensions: Default::default(), + enable_federation: false, + } + } + + fn create_registry() -> Registry { let mut registry = Registry { types: Default::default(), directives: Default::default(), implements: Default::default(), + is_empty_query: Query::is_empty(), query_type: Query::type_name().to_string(), mutation_type: if Mutation::is_empty() { None @@ -278,21 +297,7 @@ where Subscription::create_type_info(&mut registry); } - SchemaBuilder { - validation_mode: ValidationMode::Strict, - query: QueryRoot { - inner: query, - disable_introspection: false, - }, - mutation, - subscription, - registry, - data: Default::default(), - complexity: None, - depth: None, - extensions: Default::default(), - enable_federation: false, - } + registry } /// Create a schema @@ -304,6 +309,11 @@ where Self::build(query, mutation, subscription).finish() } + /// Returns SDL(Schema Definition Language) of this schema. + pub fn sdl() -> String { + Self::create_registry().export_sdl(false) + } + // TODO: Remove the allow #[allow(clippy::type_complexity)] fn prepare_request( diff --git a/src/types/query_root.rs b/src/types/query_root.rs index 45fdd0eb..f3728360 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -130,7 +130,7 @@ impl ObjectType for QueryRoot { let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set); return OutputValueType::resolve( &Service { - sdl: Some(ctx.schema_env.registry.create_federation_sdl()), + sdl: Some(ctx.schema_env.registry.export_sdl(true)), }, &ctx_obj, ctx.item,