use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SchemaError {
#[error("Schema building failed: {0}")]
BuildingFailed(String),
#[error("Configuration validation failed: {0}")]
ValidationError(String),
#[error("Query complexity limit exceeded: limit={limit}, actual={actual}")]
ComplexityLimitExceeded {
limit: usize,
actual: usize,
},
#[error("Query depth limit exceeded: limit={limit}, actual={actual}")]
DepthLimitExceeded {
limit: usize,
actual: usize,
},
}
pub type SchemaResult<T> = Result<T, SchemaError>;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaConfig {
pub introspection_enabled: bool,
pub complexity_limit: Option<usize>,
pub depth_limit: Option<usize>,
}
impl Default for SchemaConfig {
fn default() -> Self {
Self {
introspection_enabled: true,
complexity_limit: None,
depth_limit: None,
}
}
}
impl SchemaConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub const fn set_introspection_enabled(&mut self, enabled: bool) -> &mut Self {
self.introspection_enabled = enabled;
self
}
pub const fn set_complexity_limit(&mut self, limit: usize) -> &mut Self {
self.complexity_limit = if limit > 0 { Some(limit) } else { None };
self
}
pub const fn set_depth_limit(&mut self, limit: usize) -> &mut Self {
self.depth_limit = if limit > 0 { Some(limit) } else { None };
self
}
pub const fn validate(&self) -> SchemaResult<()> {
Ok(())
}
}
pub struct SchemaBuilder<Query, Mutation, Subscription> {
config: SchemaConfig,
_query: std::marker::PhantomData<Query>,
_mutation: std::marker::PhantomData<Mutation>,
_subscription: std::marker::PhantomData<Subscription>,
}
impl<Query, Mutation, Subscription> Clone for SchemaBuilder<Query, Mutation, Subscription> {
fn clone(&self) -> Self {
Self {
config: self.config.clone(),
_query: std::marker::PhantomData,
_mutation: std::marker::PhantomData,
_subscription: std::marker::PhantomData,
}
}
}
impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription> {
#[must_use]
pub fn new(_query: Query, _mutation: Mutation, _subscription: Subscription) -> Self {
Self {
config: SchemaConfig::default(),
_query: std::marker::PhantomData,
_mutation: std::marker::PhantomData,
_subscription: std::marker::PhantomData,
}
}
#[must_use]
pub const fn enable_introspection(mut self, enable: bool) -> Self {
self.config.introspection_enabled = enable;
self
}
#[must_use]
pub const fn complexity_limit(mut self, limit: usize) -> Self {
self.config.set_complexity_limit(limit);
self
}
#[must_use]
pub const fn depth_limit(mut self, limit: usize) -> Self {
self.config.set_depth_limit(limit);
self
}
#[must_use]
pub const fn is_introspection_enabled(&self) -> bool {
self.config.introspection_enabled
}
#[must_use]
pub const fn get_complexity_limit(&self) -> Option<usize> {
self.config.complexity_limit
}
#[must_use]
pub const fn get_depth_limit(&self) -> Option<usize> {
self.config.depth_limit
}
#[must_use]
pub const fn config(&self) -> &SchemaConfig {
&self.config
}
#[must_use]
pub const fn finish(self) -> SchemaConfig {
self.config
}
}
impl<Query, Mutation, Subscription> fmt::Debug for SchemaBuilder<Query, Mutation, Subscription> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SchemaBuilder").field("config", &self.config).finish()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryOnlyConfig {
pub introspection_enabled: bool,
pub complexity_limit: Option<usize>,
pub depth_limit: Option<usize>,
}
impl Default for QueryOnlyConfig {
fn default() -> Self {
Self {
introspection_enabled: true,
complexity_limit: None,
depth_limit: None,
}
}
}
#[must_use]
pub fn schema_query_only() -> QueryOnlyConfig {
QueryOnlyConfig::default()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryMutationConfig {
pub introspection_enabled: bool,
pub complexity_limit: Option<usize>,
pub depth_limit: Option<usize>,
}
impl Default for QueryMutationConfig {
fn default() -> Self {
Self {
introspection_enabled: true,
complexity_limit: None,
depth_limit: None,
}
}
}
#[must_use]
pub fn schema_query_mutation() -> QueryMutationConfig {
QueryMutationConfig::default()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FullSchemaConfig {
pub introspection_enabled: bool,
pub complexity_limit: Option<usize>,
pub depth_limit: Option<usize>,
}
impl Default for FullSchemaConfig {
fn default() -> Self {
Self {
introspection_enabled: true,
complexity_limit: None,
depth_limit: None,
}
}
}
#[must_use]
pub fn schema_full() -> FullSchemaConfig {
FullSchemaConfig::default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_config_creation() {
let config = SchemaConfig::new();
assert!(config.introspection_enabled);
assert_eq!(config.complexity_limit, None);
assert_eq!(config.depth_limit, None);
}
#[test]
fn test_schema_config_validation() {
let config = SchemaConfig::new();
assert!(config.validate().is_ok());
}
#[test]
fn test_schema_builder_creation() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription);
assert!(builder.is_introspection_enabled());
assert_eq!(builder.get_complexity_limit(), None);
assert_eq!(builder.get_depth_limit(), None);
}
#[test]
fn test_schema_builder_disable_introspection() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription).enable_introspection(false);
assert!(!builder.is_introspection_enabled());
}
#[test]
fn test_schema_builder_complexity_limit() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription).complexity_limit(5000);
assert_eq!(builder.get_complexity_limit(), Some(5000));
}
#[test]
fn test_schema_builder_depth_limit() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription).depth_limit(50);
assert_eq!(builder.get_depth_limit(), Some(50));
}
#[test]
fn test_schema_builder_chaining() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
.enable_introspection(false)
.complexity_limit(3000)
.depth_limit(100);
assert!(!builder.is_introspection_enabled());
assert_eq!(builder.get_complexity_limit(), Some(3000));
assert_eq!(builder.get_depth_limit(), Some(100));
}
#[test]
fn test_schema_builder_zero_limits_are_ignored() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
.complexity_limit(0)
.depth_limit(0);
assert_eq!(builder.get_complexity_limit(), None);
assert_eq!(builder.get_depth_limit(), None);
}
#[test]
fn test_schema_builder_debug() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
.complexity_limit(5000)
.depth_limit(50);
let debug_str = format!("{builder:?}");
assert!(debug_str.contains("SchemaBuilder"));
assert!(debug_str.contains("config"));
}
#[test]
fn test_schema_builder_finish() {
struct DummyQuery;
struct DummyMutation;
struct DummySubscription;
let config = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
.complexity_limit(5000)
.depth_limit(50)
.finish();
assert!(config.introspection_enabled);
assert_eq!(config.complexity_limit, Some(5000));
assert_eq!(config.depth_limit, Some(50));
}
#[test]
fn test_query_only_config() {
let config = schema_query_only();
assert!(config.introspection_enabled);
assert_eq!(config.complexity_limit, None);
}
#[test]
fn test_query_mutation_config() {
let config = schema_query_mutation();
assert!(config.introspection_enabled);
assert_eq!(config.complexity_limit, None);
}
#[test]
fn test_full_schema_config() {
let config = schema_full();
assert!(config.introspection_enabled);
assert_eq!(config.complexity_limit, None);
}
#[test]
fn test_schema_error_display() {
let err = SchemaError::ComplexityLimitExceeded {
limit: 5000,
actual: 6000,
};
let msg = err.to_string();
assert!(msg.contains("5000"));
assert!(msg.contains("6000"));
}
}