Skip to main content

spikard_graphql/
schema.rs

1//! GraphQL schema builder with support for Query, Mutation, and Subscription types.
2//!
3//! Provides a builder pattern for constructing async-graphql schemas with optional
4//! features like introspection, complexity limits, depth limits, and federation support.
5//!
6//! # Examples
7//!
8//! ```ignore
9//! use spikard_graphql::SchemaBuilder;
10//!
11//! let schema = SchemaBuilder::new(query, mutation, subscription)
12//!     .enable_introspection(true)
13//!     .complexity_limit(5000)
14//!     .depth_limit(50)
15//!     .finish();
16//! ```
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use thiserror::Error;
21
22/// Error type for schema building operations
23#[derive(Debug, Error)]
24pub enum SchemaError {
25    /// Generic schema building error
26    #[error("Schema building failed: {0}")]
27    BuildingFailed(String),
28
29    /// Configuration validation error
30    #[error("Configuration validation failed: {0}")]
31    ValidationError(String),
32
33    /// Complexity limit exceeded
34    #[error("Query complexity limit exceeded: limit={limit}, actual={actual}")]
35    ComplexityLimitExceeded {
36        /// The maximum allowed complexity
37        limit: usize,
38        /// The actual query complexity
39        actual: usize,
40    },
41
42    /// Depth limit exceeded
43    #[error("Query depth limit exceeded: limit={limit}, actual={actual}")]
44    DepthLimitExceeded {
45        /// The maximum allowed depth
46        limit: usize,
47        /// The actual query depth
48        actual: usize,
49    },
50}
51
52/// Result type for schema operations
53pub type SchemaResult<T> = Result<T, SchemaError>;
54
55/// Configuration for GraphQL schema building.
56///
57/// Encapsulates all schema-level configuration options including
58/// introspection control, complexity limits, and depth limits.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct SchemaConfig {
61    /// Enable introspection queries
62    pub introspection_enabled: bool,
63    /// Maximum query complexity (None = unlimited)
64    pub complexity_limit: Option<usize>,
65    /// Maximum query depth (None = unlimited)
66    pub depth_limit: Option<usize>,
67}
68
69impl Default for SchemaConfig {
70    fn default() -> Self {
71        Self {
72            introspection_enabled: true,
73            complexity_limit: None,
74            depth_limit: None,
75        }
76    }
77}
78
79impl SchemaConfig {
80    /// Create a new default configuration
81    #[must_use]
82    pub fn new() -> Self {
83        Self::default()
84    }
85
86    /// Enable or disable introspection
87    pub const fn set_introspection_enabled(&mut self, enabled: bool) -> &mut Self {
88        self.introspection_enabled = enabled;
89        self
90    }
91
92    /// Set the complexity limit (0 means unlimited)
93    pub const fn set_complexity_limit(&mut self, limit: usize) -> &mut Self {
94        self.complexity_limit = if limit > 0 { Some(limit) } else { None };
95        self
96    }
97
98    /// Set the depth limit (0 means unlimited)
99    pub const fn set_depth_limit(&mut self, limit: usize) -> &mut Self {
100        self.depth_limit = if limit > 0 { Some(limit) } else { None };
101        self
102    }
103
104    /// Validate the configuration
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the configuration is invalid (currently all configurations are valid)
109    pub const fn validate(&self) -> SchemaResult<()> {
110        // Configuration is valid if introspection and limits are set
111        // Add specific validation rules as needed
112        Ok(())
113    }
114}
115
116/// Builder for constructing GraphQL schemas with async-graphql.
117///
118/// Provides a fluent interface for building schemas with Query, Mutation, and Subscription types.
119/// Supports optional features like introspection, complexity limits, and depth limits.
120///
121/// This is a generic schema builder that can be used with any async-graphql compatible types.
122///
123/// # Type Parameters
124///
125/// * `Query` - The GraphQL Query root type
126/// * `Mutation` - The GraphQL Mutation root type
127/// * `Subscription` - The GraphQL Subscription root type
128///
129/// # Examples
130///
131/// ```ignore
132/// let schema = SchemaBuilder::new(query, mutation, subscription)
133///     .enable_introspection(true)
134///     .complexity_limit(5000)
135///     .depth_limit(50)
136///     .finish();
137/// ```
138pub struct SchemaBuilder<Query, Mutation, Subscription> {
139    config: SchemaConfig,
140    _query: std::marker::PhantomData<Query>,
141    _mutation: std::marker::PhantomData<Mutation>,
142    _subscription: std::marker::PhantomData<Subscription>,
143}
144
145impl<Query, Mutation, Subscription> Clone for SchemaBuilder<Query, Mutation, Subscription> {
146    fn clone(&self) -> Self {
147        Self {
148            config: self.config.clone(),
149            _query: std::marker::PhantomData,
150            _mutation: std::marker::PhantomData,
151            _subscription: std::marker::PhantomData,
152        }
153    }
154}
155
156impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription> {
157    /// Create a new schema builder with the specified root types.
158    ///
159    /// # Arguments
160    ///
161    /// * `_query` - The Query root type (for type tracking)
162    /// * `_mutation` - The Mutation root type (for type tracking)
163    /// * `_subscription` - The Subscription root type (for type tracking)
164    ///
165    /// # Returns
166    ///
167    /// A new `SchemaBuilder` instance
168    ///
169    /// # Examples
170    ///
171    /// ```ignore
172    /// let builder = SchemaBuilder::new(query, mutation, subscription);
173    /// ```
174    #[must_use]
175    pub fn new(_query: Query, _mutation: Mutation, _subscription: Subscription) -> Self {
176        Self {
177            config: SchemaConfig::default(),
178            _query: std::marker::PhantomData,
179            _mutation: std::marker::PhantomData,
180            _subscription: std::marker::PhantomData,
181        }
182    }
183
184    /// Enable or disable introspection.
185    ///
186    /// Introspection is enabled by default. Disabling it prevents clients from
187    /// querying the schema structure via introspection queries.
188    ///
189    /// # Arguments
190    ///
191    /// * `enable` - Whether to enable introspection
192    ///
193    /// # Returns
194    ///
195    /// Self for method chaining
196    #[must_use]
197    pub const fn enable_introspection(mut self, enable: bool) -> Self {
198        self.config.introspection_enabled = enable;
199        self
200    }
201
202    /// Set the maximum complexity allowed for queries.
203    ///
204    /// The complexity is calculated based on the query structure and field costs.
205    /// Queries exceeding this limit will be rejected.
206    ///
207    /// # Arguments
208    ///
209    /// * `limit` - The maximum complexity allowed (0 means unlimited)
210    ///
211    /// # Returns
212    ///
213    /// Self for method chaining
214    #[must_use]
215    pub const fn complexity_limit(mut self, limit: usize) -> Self {
216        self.config.set_complexity_limit(limit);
217        self
218    }
219
220    /// Set the maximum depth allowed for queries.
221    ///
222    /// The depth is the maximum nesting level of selections.
223    /// Queries exceeding this limit will be rejected.
224    ///
225    /// # Arguments
226    ///
227    /// * `limit` - The maximum depth allowed (0 means unlimited)
228    ///
229    /// # Returns
230    ///
231    /// Self for method chaining
232    #[must_use]
233    pub const fn depth_limit(mut self, limit: usize) -> Self {
234        self.config.set_depth_limit(limit);
235        self
236    }
237
238    /// Get the current introspection setting.
239    ///
240    /// # Returns
241    ///
242    /// Whether introspection is enabled
243    #[must_use]
244    pub const fn is_introspection_enabled(&self) -> bool {
245        self.config.introspection_enabled
246    }
247
248    /// Get the current complexity limit if set.
249    ///
250    /// # Returns
251    ///
252    /// The complexity limit, or None if unlimited
253    #[must_use]
254    pub const fn get_complexity_limit(&self) -> Option<usize> {
255        self.config.complexity_limit
256    }
257
258    /// Get the current depth limit if set.
259    ///
260    /// # Returns
261    ///
262    /// The depth limit, or None if unlimited
263    #[must_use]
264    pub const fn get_depth_limit(&self) -> Option<usize> {
265        self.config.depth_limit
266    }
267
268    /// Get the underlying configuration
269    ///
270    /// # Returns
271    ///
272    /// A reference to the `SchemaConfig`
273    #[must_use]
274    pub const fn config(&self) -> &SchemaConfig {
275        &self.config
276    }
277
278    /// Build the schema configuration (does not construct async-graphql Schema directly).
279    ///
280    /// Returns the configuration that should be applied to an async-graphql `SchemaBuilder`.
281    /// The actual Schema construction must be done by the caller using the async-graphql API.
282    ///
283    /// # Returns
284    ///
285    /// The `SchemaConfig` instance
286    #[must_use]
287    pub const fn finish(self) -> SchemaConfig {
288        self.config
289    }
290}
291
292impl<Query, Mutation, Subscription> fmt::Debug for SchemaBuilder<Query, Mutation, Subscription> {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        f.debug_struct("SchemaBuilder").field("config", &self.config).finish()
295    }
296}
297
298/// Configuration for schemas with only Query type
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct QueryOnlyConfig {
301    /// Enable introspection queries
302    pub introspection_enabled: bool,
303    /// Maximum query complexity (None = unlimited)
304    pub complexity_limit: Option<usize>,
305    /// Maximum query depth (None = unlimited)
306    pub depth_limit: Option<usize>,
307}
308
309impl Default for QueryOnlyConfig {
310    fn default() -> Self {
311        Self {
312            introspection_enabled: true,
313            complexity_limit: None,
314            depth_limit: None,
315        }
316    }
317}
318
319/// Create a simple schema configuration with only Query type.
320///
321/// This is a convenience function for schemas that only have queries.
322///
323/// # Returns
324///
325/// A `QueryOnlyConfig` with default settings
326#[must_use]
327pub fn schema_query_only() -> QueryOnlyConfig {
328    QueryOnlyConfig::default()
329}
330
331/// Configuration for schemas with Query and Mutation types
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct QueryMutationConfig {
334    /// Enable introspection queries
335    pub introspection_enabled: bool,
336    /// Maximum query complexity (None = unlimited)
337    pub complexity_limit: Option<usize>,
338    /// Maximum query depth (None = unlimited)
339    pub depth_limit: Option<usize>,
340}
341
342impl Default for QueryMutationConfig {
343    fn default() -> Self {
344        Self {
345            introspection_enabled: true,
346            complexity_limit: None,
347            depth_limit: None,
348        }
349    }
350}
351
352/// Create a schema configuration with Query and Mutation types.
353///
354/// This is a convenience function for schemas with queries and mutations but no subscriptions.
355///
356/// # Returns
357///
358/// A `QueryMutationConfig` with default settings
359#[must_use]
360pub fn schema_query_mutation() -> QueryMutationConfig {
361    QueryMutationConfig::default()
362}
363
364/// Configuration for fully-featured schemas with Query, Mutation, and Subscription types
365#[derive(Debug, Clone, Serialize, Deserialize)]
366pub struct FullSchemaConfig {
367    /// Enable introspection queries
368    pub introspection_enabled: bool,
369    /// Maximum query complexity (None = unlimited)
370    pub complexity_limit: Option<usize>,
371    /// Maximum query depth (None = unlimited)
372    pub depth_limit: Option<usize>,
373}
374
375impl Default for FullSchemaConfig {
376    fn default() -> Self {
377        Self {
378            introspection_enabled: true,
379            complexity_limit: None,
380            depth_limit: None,
381        }
382    }
383}
384
385/// Create a schema configuration with all three root types.
386///
387/// This is a convenience function for fully-featured schemas.
388///
389/// # Returns
390///
391/// A `FullSchemaConfig` with default settings
392#[must_use]
393pub fn schema_full() -> FullSchemaConfig {
394    FullSchemaConfig::default()
395}
396
397#[cfg(test)]
398mod tests {
399    use super::*;
400
401    #[test]
402    fn test_schema_config_creation() {
403        let config = SchemaConfig::new();
404        assert!(config.introspection_enabled);
405        assert_eq!(config.complexity_limit, None);
406        assert_eq!(config.depth_limit, None);
407    }
408
409    #[test]
410    fn test_schema_config_validation() {
411        let config = SchemaConfig::new();
412        assert!(config.validate().is_ok());
413    }
414
415    #[test]
416    fn test_schema_builder_creation() {
417        struct DummyQuery;
418        struct DummyMutation;
419        struct DummySubscription;
420
421        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription);
422        assert!(builder.is_introspection_enabled());
423        assert_eq!(builder.get_complexity_limit(), None);
424        assert_eq!(builder.get_depth_limit(), None);
425    }
426
427    #[test]
428    fn test_schema_builder_disable_introspection() {
429        struct DummyQuery;
430        struct DummyMutation;
431        struct DummySubscription;
432
433        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription).enable_introspection(false);
434        assert!(!builder.is_introspection_enabled());
435    }
436
437    #[test]
438    fn test_schema_builder_complexity_limit() {
439        struct DummyQuery;
440        struct DummyMutation;
441        struct DummySubscription;
442
443        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription).complexity_limit(5000);
444        assert_eq!(builder.get_complexity_limit(), Some(5000));
445    }
446
447    #[test]
448    fn test_schema_builder_depth_limit() {
449        struct DummyQuery;
450        struct DummyMutation;
451        struct DummySubscription;
452
453        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription).depth_limit(50);
454        assert_eq!(builder.get_depth_limit(), Some(50));
455    }
456
457    #[test]
458    fn test_schema_builder_chaining() {
459        struct DummyQuery;
460        struct DummyMutation;
461        struct DummySubscription;
462
463        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
464            .enable_introspection(false)
465            .complexity_limit(3000)
466            .depth_limit(100);
467
468        assert!(!builder.is_introspection_enabled());
469        assert_eq!(builder.get_complexity_limit(), Some(3000));
470        assert_eq!(builder.get_depth_limit(), Some(100));
471    }
472
473    #[test]
474    fn test_schema_builder_zero_limits_are_ignored() {
475        struct DummyQuery;
476        struct DummyMutation;
477        struct DummySubscription;
478
479        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
480            .complexity_limit(0)
481            .depth_limit(0);
482
483        assert_eq!(builder.get_complexity_limit(), None);
484        assert_eq!(builder.get_depth_limit(), None);
485    }
486
487    #[test]
488    fn test_schema_builder_debug() {
489        struct DummyQuery;
490        struct DummyMutation;
491        struct DummySubscription;
492
493        let builder = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
494            .complexity_limit(5000)
495            .depth_limit(50);
496
497        let debug_str = format!("{builder:?}");
498        assert!(debug_str.contains("SchemaBuilder"));
499        assert!(debug_str.contains("config"));
500    }
501
502    #[test]
503    fn test_schema_builder_finish() {
504        struct DummyQuery;
505        struct DummyMutation;
506        struct DummySubscription;
507
508        let config = SchemaBuilder::new(DummyQuery, DummyMutation, DummySubscription)
509            .complexity_limit(5000)
510            .depth_limit(50)
511            .finish();
512
513        assert!(config.introspection_enabled);
514        assert_eq!(config.complexity_limit, Some(5000));
515        assert_eq!(config.depth_limit, Some(50));
516    }
517
518    #[test]
519    fn test_query_only_config() {
520        let config = schema_query_only();
521        assert!(config.introspection_enabled);
522        assert_eq!(config.complexity_limit, None);
523    }
524
525    #[test]
526    fn test_query_mutation_config() {
527        let config = schema_query_mutation();
528        assert!(config.introspection_enabled);
529        assert_eq!(config.complexity_limit, None);
530    }
531
532    #[test]
533    fn test_full_schema_config() {
534        let config = schema_full();
535        assert!(config.introspection_enabled);
536        assert_eq!(config.complexity_limit, None);
537    }
538
539    #[test]
540    fn test_schema_error_display() {
541        let err = SchemaError::ComplexityLimitExceeded {
542            limit: 5000,
543            actual: 6000,
544        };
545        let msg = err.to_string();
546        assert!(msg.contains("5000"));
547        assert!(msg.contains("6000"));
548    }
549}