use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use super::Span;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct GraphQLConfig {
pub name: Option<String>,
pub description: Option<String>,
pub skip: bool,
pub is_interface: bool,
pub union_types: Vec<String>,
pub implements: Vec<String>,
pub directives: Vec<GraphQLDirective>,
pub complexity: Option<u32>,
pub guard: Option<String>,
pub generate_input: bool,
pub generate_filter: bool,
pub generate_order: bool,
pub resolver: Option<String>,
}
impl GraphQLConfig {
pub fn new() -> Self {
Self {
generate_input: true,
generate_filter: true,
generate_order: true,
..Default::default()
}
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn skip(mut self) -> Self {
self.skip = true;
self
}
pub fn as_interface(mut self) -> Self {
self.is_interface = true;
self
}
pub fn in_union(mut self, union_name: impl Into<String>) -> Self {
self.union_types.push(union_name.into());
self
}
pub fn implements(mut self, interface: impl Into<String>) -> Self {
self.implements.push(interface.into());
self
}
pub fn with_complexity(mut self, complexity: u32) -> Self {
self.complexity = Some(complexity);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct GraphQLFieldConfig {
pub name: Option<String>,
pub description: Option<String>,
pub skip: bool,
pub deprecation: Option<String>,
pub complexity: Option<ComplexityConfig>,
pub guard: Option<String>,
pub resolver: Option<String>,
pub directives: Vec<GraphQLDirective>,
pub derived: bool,
pub shareable: bool,
pub external: bool,
pub provides: Option<String>,
pub requires: Option<String>,
pub key: bool,
}
impl GraphQLFieldConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn skip(mut self) -> Self {
self.skip = true;
self
}
pub fn deprecated(mut self, reason: impl Into<String>) -> Self {
self.deprecation = Some(reason.into());
self
}
pub fn derived(mut self) -> Self {
self.derived = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ComplexityConfig {
Fixed(u32),
Multiplier {
base: u32,
argument: String,
},
Custom(String),
}
impl Default for ComplexityConfig {
fn default() -> Self {
Self::Fixed(1)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GraphQLDirective {
pub name: SmolStr,
pub arguments: Vec<GraphQLArgument>,
pub span: Span,
}
impl GraphQLDirective {
pub fn new(name: impl Into<SmolStr>, span: Span) -> Self {
Self {
name: name.into(),
arguments: Vec::new(),
span,
}
}
pub fn with_arg(mut self, name: impl Into<SmolStr>, value: GraphQLValue) -> Self {
self.arguments.push(GraphQLArgument {
name: name.into(),
value,
});
self
}
pub fn to_sdl(&self) -> String {
if self.arguments.is_empty() {
format!("@{}", self.name)
} else {
let args: Vec<String> = self
.arguments
.iter()
.map(|a| format!("{}: {}", a.name, a.value.to_sdl()))
.collect();
format!("@{}({})", self.name, args.join(", "))
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct GraphQLArgument {
pub name: SmolStr,
pub value: GraphQLValue,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum GraphQLValue {
String(String),
Int(i64),
Float(f64),
Boolean(bool),
Enum(String),
List(Vec<GraphQLValue>),
Object(Vec<(String, GraphQLValue)>),
Null,
}
impl GraphQLValue {
pub fn to_sdl(&self) -> String {
match self {
Self::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
Self::Int(i) => i.to_string(),
Self::Float(f) => f.to_string(),
Self::Boolean(b) => b.to_string(),
Self::Enum(e) => e.clone(),
Self::List(items) => {
let vals: Vec<String> = items.iter().map(|v| v.to_sdl()).collect();
format!("[{}]", vals.join(", "))
}
Self::Object(fields) => {
let field_strs: Vec<String> = fields
.iter()
.map(|(k, v)| format!("{}: {}", k, v.to_sdl()))
.collect();
format!("{{{}}}", field_strs.join(", "))
}
Self::Null => "null".to_string(),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct SubscriptionConfig {
pub enabled: bool,
pub on_create: bool,
pub on_update: bool,
pub on_delete: bool,
pub filter: Option<String>,
}
impl SubscriptionConfig {
pub fn all() -> Self {
Self {
enabled: true,
on_create: true,
on_update: true,
on_delete: true,
filter: None,
}
}
pub fn only_create() -> Self {
Self {
enabled: true,
on_create: true,
..Default::default()
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct FederationConfig {
pub is_entity: bool,
pub keys: Vec<FederationKey>,
pub shareable: bool,
pub external: bool,
pub extends: bool,
pub interface_object: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FederationKey {
pub fields: String,
pub resolvable: bool,
}
impl FederationKey {
pub fn new(fields: impl Into<String>) -> Self {
Self {
fields: fields.into(),
resolvable: true,
}
}
pub fn non_resolvable(mut self) -> Self {
self.resolvable = false;
self
}
pub fn to_sdl(&self) -> String {
if self.resolvable {
format!("@key(fields: \"{}\")", self.fields)
} else {
format!("@key(fields: \"{}\", resolvable: false)", self.fields)
}
}
}
pub fn parse_graphql_config_from_tags(tags: &[super::validation::DocTag]) -> GraphQLConfig {
let mut config = GraphQLConfig::new();
for tag in tags {
match tag.name.as_str() {
"graphql.skip" | "graphql_skip" => config.skip = true,
"graphql.name" | "graphql_name" => config.name = tag.value.clone(),
"graphql.interface" | "graphql_interface" => config.is_interface = true,
"graphql.union" | "graphql_union" => {
if let Some(v) = &tag.value {
config.union_types.push(v.clone());
}
}
"graphql.implements" | "graphql_implements" => {
if let Some(v) = &tag.value {
config.implements.push(v.clone());
}
}
"graphql.complexity" | "graphql_complexity" => {
if let Some(v) = &tag.value {
config.complexity = v.parse().ok();
}
}
"graphql.guard" | "graphql_guard" => config.guard = tag.value.clone(),
"graphql.resolver" | "graphql_resolver" => config.resolver = tag.value.clone(),
"graphql.no_input" | "graphql_no_input" => config.generate_input = false,
"graphql.no_filter" | "graphql_no_filter" => config.generate_filter = false,
"graphql.no_order" | "graphql_no_order" => config.generate_order = false,
_ => {}
}
}
config
}
pub fn parse_graphql_field_config_from_tags(
tags: &[super::validation::DocTag],
) -> GraphQLFieldConfig {
let mut config = GraphQLFieldConfig::new();
for tag in tags {
match tag.name.as_str() {
"graphql.skip" | "graphql_skip" => config.skip = true,
"graphql.name" | "graphql_name" => config.name = tag.value.clone(),
"graphql.deprecation" | "graphql_deprecation" | "deprecated" => {
config.deprecation = tag.value.clone().or(Some(String::new()));
}
"graphql.guard" | "graphql_guard" => config.guard = tag.value.clone(),
"graphql.resolver" | "graphql_resolver" => config.resolver = tag.value.clone(),
"graphql.derived" | "graphql_derived" => config.derived = true,
"graphql.shareable" | "graphql_shareable" => config.shareable = true,
"graphql.external" | "graphql_external" => config.external = true,
"graphql.provides" | "graphql_provides" => config.provides = tag.value.clone(),
"graphql.requires" | "graphql_requires" => config.requires = tag.value.clone(),
"graphql.key" | "graphql_key" => config.key = true,
_ => {}
}
}
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_graphql_config_builder() {
let config = GraphQLConfig::new()
.with_name("CustomUser")
.as_interface()
.in_union("SearchResult")
.implements("Node")
.with_complexity(10);
assert_eq!(config.name, Some("CustomUser".to_string()));
assert!(config.is_interface);
assert_eq!(config.union_types, vec!["SearchResult"]);
assert_eq!(config.implements, vec!["Node"]);
assert_eq!(config.complexity, Some(10));
}
#[test]
fn test_graphql_field_config() {
let config = GraphQLFieldConfig::new()
.with_name("userId")
.deprecated("Use newId instead")
.derived();
assert_eq!(config.name, Some("userId".to_string()));
assert_eq!(config.deprecation, Some("Use newId instead".to_string()));
assert!(config.derived);
}
#[test]
fn test_graphql_directive_sdl() {
let directive = GraphQLDirective::new("deprecated", Span::new(0, 0))
.with_arg("reason", GraphQLValue::String("Use newField".to_string()));
assert_eq!(directive.to_sdl(), "@deprecated(reason: \"Use newField\")");
let simple_directive = GraphQLDirective::new("shareable", Span::new(0, 0));
assert_eq!(simple_directive.to_sdl(), "@shareable");
}
#[test]
fn test_graphql_value_sdl() {
assert_eq!(
GraphQLValue::String("hello".to_string()).to_sdl(),
"\"hello\""
);
assert_eq!(GraphQLValue::Int(42).to_sdl(), "42");
assert_eq!(GraphQLValue::Float(3.14).to_sdl(), "3.14");
assert_eq!(GraphQLValue::Boolean(true).to_sdl(), "true");
assert_eq!(GraphQLValue::Enum("ADMIN".to_string()).to_sdl(), "ADMIN");
assert_eq!(GraphQLValue::Null.to_sdl(), "null");
let list = GraphQLValue::List(vec![
GraphQLValue::Int(1),
GraphQLValue::Int(2),
GraphQLValue::Int(3),
]);
assert_eq!(list.to_sdl(), "[1, 2, 3]");
let obj = GraphQLValue::Object(vec![
("name".to_string(), GraphQLValue::String("test".to_string())),
("count".to_string(), GraphQLValue::Int(5)),
]);
assert_eq!(obj.to_sdl(), "{name: \"test\", count: 5}");
}
#[test]
fn test_federation_key_sdl() {
let key = FederationKey::new("id");
assert_eq!(key.to_sdl(), "@key(fields: \"id\")");
let composite_key = FederationKey::new("userId organizationId");
assert_eq!(
composite_key.to_sdl(),
"@key(fields: \"userId organizationId\")"
);
let non_resolvable = FederationKey::new("id").non_resolvable();
assert_eq!(
non_resolvable.to_sdl(),
"@key(fields: \"id\", resolvable: false)"
);
}
#[test]
fn test_subscription_config() {
let all = SubscriptionConfig::all();
assert!(all.enabled);
assert!(all.on_create);
assert!(all.on_update);
assert!(all.on_delete);
let only_create = SubscriptionConfig::only_create();
assert!(only_create.enabled);
assert!(only_create.on_create);
assert!(!only_create.on_update);
assert!(!only_create.on_delete);
}
#[test]
fn test_parse_graphql_config_from_tags() {
use super::super::validation::DocTag;
let span = Span::new(0, 0);
let tags = vec![
DocTag::new("graphql.name", Some("CustomModel".to_string()), span),
DocTag::new("graphql.interface", None, span),
DocTag::new("graphql.complexity", Some("5".to_string()), span),
];
let config = parse_graphql_config_from_tags(&tags);
assert_eq!(config.name, Some("CustomModel".to_string()));
assert!(config.is_interface);
assert_eq!(config.complexity, Some(5));
}
#[test]
fn test_parse_graphql_field_config_from_tags() {
use super::super::validation::DocTag;
let span = Span::new(0, 0);
let tags = vec![
DocTag::new("graphql.name", Some("userId".to_string()), span),
DocTag::new("deprecated", Some("Use newId".to_string()), span),
DocTag::new("graphql.shareable", None, span),
];
let config = parse_graphql_field_config_from_tags(&tags);
assert_eq!(config.name, Some("userId".to_string()));
assert_eq!(config.deprecation, Some("Use newId".to_string()));
assert!(config.shareable);
}
}