Skip to main content

oxirs_gql/
custom_directives.rs

1//! # Custom GraphQL Directives
2//!
3//! Flexible directive system for extending GraphQL functionality.
4//! Supports schema directives, query directives, and custom handlers.
5//!
6//! ## Features
7//!
8//! - **Authorization Directives**: @auth, @hasRole, @requiresPermission
9//! - **Caching Directives**: @cacheControl, @cacheHint
10//! - **Deprecation Directives**: @deprecated, @deprecated(reason: "...")
11//! - **Rate Limiting Directives**: @rateLimit, @throttle
12//! - **Transformation Directives**: @uppercase, @lowercase, @trim, @format
13//! - **Validation Directives**: @constraint, @pattern, @range, @length
14//! - **Cost Analysis Directives**: @cost, @complexity
15//! - **Custom Directive Handlers**: User-defined directives
16//!
17//! ## Example
18//!
19//! ```graphql
20//! type User @auth(requires: ADMIN) {
21//!   email: String! @hasRole(role: "user")
22//!   password: String! @deprecated(reason: "Use passwordHash instead")
23//!   name: String! @uppercase @cacheControl(maxAge: 300)
24//!   age: Int! @constraint(min: 0, max: 150)
25//! }
26//!
27//! type Query {
28//!   users: [User!]! @cost(complexity: 10) @rateLimit(max: 100)
29//! }
30//! ```
31
32use anyhow::{anyhow, Result};
33use serde::{Deserialize, Serialize};
34use std::collections::HashMap;
35use std::sync::Arc;
36
37/// Directive location (where the directive can be applied)
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum DirectiveLocation {
40    /// Type definition
41    Object,
42    /// Field definition
43    FieldDefinition,
44    /// Interface definition
45    Interface,
46    /// Union definition
47    Union,
48    /// Enum definition
49    Enum,
50    /// Enum value
51    EnumValue,
52    /// Input object definition
53    InputObject,
54    /// Input field definition
55    InputFieldDefinition,
56    /// Scalar definition
57    Scalar,
58    /// Argument definition
59    ArgumentDefinition,
60    /// Query operation
61    Query,
62    /// Mutation operation
63    Mutation,
64    /// Subscription operation
65    Subscription,
66    /// Fragment definition
67    FragmentDefinition,
68    /// Fragment spread
69    FragmentSpread,
70    /// Inline fragment
71    InlineFragment,
72    /// Variable definition
73    VariableDefinition,
74}
75
76/// Directive argument
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct DirectiveArgument {
79    /// Argument name
80    pub name: String,
81    /// Argument value (can be string, number, boolean, etc.)
82    pub value: DirectiveValue,
83}
84
85/// Directive value type
86#[derive(Debug, Clone, Serialize, Deserialize)]
87#[serde(untagged)]
88pub enum DirectiveValue {
89    String(String),
90    Int(i64),
91    Float(f64),
92    Boolean(bool),
93    List(Vec<DirectiveValue>),
94    Object(HashMap<String, DirectiveValue>),
95    Null,
96}
97
98impl DirectiveValue {
99    pub fn as_string(&self) -> Option<&str> {
100        match self {
101            DirectiveValue::String(s) => Some(s),
102            _ => None,
103        }
104    }
105
106    pub fn as_int(&self) -> Option<i64> {
107        match self {
108            DirectiveValue::Int(i) => Some(*i),
109            _ => None,
110        }
111    }
112
113    pub fn as_float(&self) -> Option<f64> {
114        match self {
115            DirectiveValue::Float(f) => Some(*f),
116            _ => None,
117        }
118    }
119
120    pub fn as_bool(&self) -> Option<bool> {
121        match self {
122            DirectiveValue::Boolean(b) => Some(*b),
123            _ => None,
124        }
125    }
126}
127
128/// Directive definition
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct DirectiveDefinition {
131    /// Directive name (without @)
132    pub name: String,
133    /// Description
134    pub description: Option<String>,
135    /// Where this directive can be applied
136    pub locations: Vec<DirectiveLocation>,
137    /// Whether this directive is repeatable
138    pub repeatable: bool,
139    /// Arguments this directive accepts
140    pub arguments: Vec<DirectiveArgumentDefinition>,
141}
142
143/// Directive argument definition
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct DirectiveArgumentDefinition {
146    /// Argument name
147    pub name: String,
148    /// Argument type
149    pub arg_type: String,
150    /// Whether this argument is required
151    pub required: bool,
152    /// Default value
153    pub default_value: Option<DirectiveValue>,
154    /// Description
155    pub description: Option<String>,
156}
157
158/// Applied directive instance
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct AppliedDirective {
161    /// Directive name
162    pub name: String,
163    /// Directive arguments
164    pub arguments: Vec<DirectiveArgument>,
165    /// Where this directive is applied
166    pub location: DirectiveLocation,
167}
168
169impl AppliedDirective {
170    pub fn new(name: String, location: DirectiveLocation) -> Self {
171        Self {
172            name,
173            arguments: Vec::new(),
174            location,
175        }
176    }
177
178    pub fn with_argument(mut self, name: String, value: DirectiveValue) -> Self {
179        self.arguments.push(DirectiveArgument { name, value });
180        self
181    }
182
183    pub fn get_argument(&self, name: &str) -> Option<&DirectiveValue> {
184        self.arguments
185            .iter()
186            .find(|arg| arg.name == name)
187            .map(|arg| &arg.value)
188    }
189}
190
191/// Directive execution context
192#[derive(Debug, Clone)]
193pub struct DirectiveContext {
194    /// Field name (if applicable)
195    pub field_name: Option<String>,
196    /// Type name
197    pub type_name: Option<String>,
198    /// User context (for auth, etc.)
199    pub user_context: Option<HashMap<String, String>>,
200    /// Field value (for transformation directives)
201    pub field_value: Option<DirectiveValue>,
202}
203
204impl DirectiveContext {
205    pub fn new() -> Self {
206        Self {
207            field_name: None,
208            type_name: None,
209            user_context: None,
210            field_value: None,
211        }
212    }
213
214    pub fn with_field(mut self, field_name: String) -> Self {
215        self.field_name = Some(field_name);
216        self
217    }
218
219    pub fn with_type(mut self, type_name: String) -> Self {
220        self.type_name = Some(type_name);
221        self
222    }
223
224    pub fn with_user_context(mut self, user_context: HashMap<String, String>) -> Self {
225        self.user_context = Some(user_context);
226        self
227    }
228
229    pub fn with_value(mut self, value: DirectiveValue) -> Self {
230        self.field_value = Some(value);
231        self
232    }
233}
234
235impl Default for DirectiveContext {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241/// Directive handler trait
242pub trait DirectiveHandler: Send + Sync {
243    /// Execute the directive
244    fn execute(
245        &self,
246        directive: &AppliedDirective,
247        context: &DirectiveContext,
248    ) -> Result<DirectiveValue>;
249
250    /// Validate the directive (called during schema validation)
251    fn validate(&self, directive: &AppliedDirective) -> Result<()> {
252        let _ = directive;
253        Ok(())
254    }
255}
256
257/// Built-in @auth directive handler
258pub struct AuthDirectiveHandler;
259
260impl DirectiveHandler for AuthDirectiveHandler {
261    fn execute(
262        &self,
263        directive: &AppliedDirective,
264        context: &DirectiveContext,
265    ) -> Result<DirectiveValue> {
266        // Check if user is authenticated
267        if context.user_context.is_none() {
268            return Err(anyhow!("Authentication required"));
269        }
270
271        // Check required role if specified
272        if let Some(requires) = directive.get_argument("requires") {
273            if let Some(required_role) = requires.as_string() {
274                let user_context = context
275                    .user_context
276                    .as_ref()
277                    .expect("user_context should be set when authentication is present");
278                let user_role = user_context.get("role");
279
280                if user_role.map(|r| r.as_str()) != Some(required_role) {
281                    return Err(anyhow!(
282                        "Insufficient permissions. Required role: {}",
283                        required_role
284                    ));
285                }
286            }
287        }
288
289        // Pass through the value
290        Ok(context.field_value.clone().unwrap_or(DirectiveValue::Null))
291    }
292}
293
294/// Built-in @hasRole directive handler
295pub struct HasRoleDirectiveHandler;
296
297impl DirectiveHandler for HasRoleDirectiveHandler {
298    fn execute(
299        &self,
300        directive: &AppliedDirective,
301        context: &DirectiveContext,
302    ) -> Result<DirectiveValue> {
303        let required_role = directive
304            .get_argument("role")
305            .and_then(|v| v.as_string())
306            .ok_or_else(|| anyhow!("@hasRole directive requires 'role' argument"))?;
307
308        let user_context = context
309            .user_context
310            .as_ref()
311            .ok_or_else(|| anyhow!("User context not available"))?;
312
313        let user_roles = user_context
314            .get("roles")
315            .map(|r| r.split(',').collect::<Vec<_>>())
316            .unwrap_or_default();
317
318        if !user_roles.contains(&required_role) {
319            return Err(anyhow!(
320                "User does not have required role: {}",
321                required_role
322            ));
323        }
324
325        Ok(context.field_value.clone().unwrap_or(DirectiveValue::Null))
326    }
327}
328
329/// Built-in @uppercase directive handler
330pub struct UppercaseDirectiveHandler;
331
332impl DirectiveHandler for UppercaseDirectiveHandler {
333    fn execute(
334        &self,
335        _directive: &AppliedDirective,
336        context: &DirectiveContext,
337    ) -> Result<DirectiveValue> {
338        if let Some(DirectiveValue::String(s)) = &context.field_value {
339            Ok(DirectiveValue::String(s.to_uppercase()))
340        } else {
341            Ok(context.field_value.clone().unwrap_or(DirectiveValue::Null))
342        }
343    }
344}
345
346/// Built-in @lowercase directive handler
347pub struct LowercaseDirectiveHandler;
348
349impl DirectiveHandler for LowercaseDirectiveHandler {
350    fn execute(
351        &self,
352        _directive: &AppliedDirective,
353        context: &DirectiveContext,
354    ) -> Result<DirectiveValue> {
355        if let Some(DirectiveValue::String(s)) = &context.field_value {
356            Ok(DirectiveValue::String(s.to_lowercase()))
357        } else {
358            Ok(context.field_value.clone().unwrap_or(DirectiveValue::Null))
359        }
360    }
361}
362
363/// Built-in @constraint directive handler
364pub struct ConstraintDirectiveHandler;
365
366impl DirectiveHandler for ConstraintDirectiveHandler {
367    fn execute(
368        &self,
369        directive: &AppliedDirective,
370        context: &DirectiveContext,
371    ) -> Result<DirectiveValue> {
372        let value = context
373            .field_value
374            .as_ref()
375            .ok_or_else(|| anyhow!("No value to validate"))?;
376
377        // Check min/max for numbers
378        if let Some(val) = value.as_int() {
379            if let Some(min) = directive.get_argument("min").and_then(|v| v.as_int()) {
380                if val < min {
381                    return Err(anyhow!("Value {} is less than minimum {}", val, min));
382                }
383            }
384            if let Some(max) = directive.get_argument("max").and_then(|v| v.as_int()) {
385                if val > max {
386                    return Err(anyhow!("Value {} is greater than maximum {}", val, max));
387                }
388            }
389        }
390
391        // Check minLength/maxLength for strings
392        if let Some(s) = value.as_string() {
393            if let Some(min_len) = directive.get_argument("minLength").and_then(|v| v.as_int()) {
394                if (s.len() as i64) < min_len {
395                    return Err(anyhow!(
396                        "String length {} is less than minimum {}",
397                        s.len(),
398                        min_len
399                    ));
400                }
401            }
402            if let Some(max_len) = directive.get_argument("maxLength").and_then(|v| v.as_int()) {
403                if (s.len() as i64) > max_len {
404                    return Err(anyhow!(
405                        "String length {} is greater than maximum {}",
406                        s.len(),
407                        max_len
408                    ));
409                }
410            }
411        }
412
413        Ok(value.clone())
414    }
415}
416
417/// Built-in @cacheControl directive handler
418pub struct CacheControlDirectiveHandler {
419    cache_hints: Arc<tokio::sync::RwLock<HashMap<String, CacheHint>>>,
420}
421
422#[derive(Debug, Clone)]
423pub struct CacheHint {
424    pub max_age: u32,
425    pub scope: CacheScope,
426}
427
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub enum CacheScope {
430    Public,
431    Private,
432}
433
434impl CacheControlDirectiveHandler {
435    pub fn new() -> Self {
436        Self {
437            cache_hints: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
438        }
439    }
440
441    pub async fn get_cache_hint(&self, key: &str) -> Option<CacheHint> {
442        let hints = self.cache_hints.read().await;
443        hints.get(key).cloned()
444    }
445}
446
447impl DirectiveHandler for CacheControlDirectiveHandler {
448    fn execute(
449        &self,
450        directive: &AppliedDirective,
451        context: &DirectiveContext,
452    ) -> Result<DirectiveValue> {
453        let max_age = directive
454            .get_argument("maxAge")
455            .and_then(|v| v.as_int())
456            .unwrap_or(0) as u32;
457
458        let scope = directive
459            .get_argument("scope")
460            .and_then(|v| v.as_string())
461            .map(|s| {
462                if s.eq_ignore_ascii_case("PRIVATE") {
463                    CacheScope::Private
464                } else {
465                    CacheScope::Public
466                }
467            })
468            .unwrap_or(CacheScope::Public);
469
470        let key = format!(
471            "{}:{}",
472            context.type_name.as_ref().unwrap_or(&"unknown".to_string()),
473            context
474                .field_name
475                .as_ref()
476                .unwrap_or(&"unknown".to_string())
477        );
478
479        // Store cache hint (async operation, so we spawn a task)
480        let cache_hints = self.cache_hints.clone();
481        let key_clone = key.clone();
482        tokio::spawn(async move {
483            let mut hints = cache_hints.write().await;
484            hints.insert(key_clone, CacheHint { max_age, scope });
485        });
486
487        Ok(context.field_value.clone().unwrap_or(DirectiveValue::Null))
488    }
489}
490
491impl Default for CacheControlDirectiveHandler {
492    fn default() -> Self {
493        Self::new()
494    }
495}
496
497/// Directive registry
498pub struct DirectiveRegistry {
499    /// Registered directive definitions
500    definitions: Arc<tokio::sync::RwLock<HashMap<String, DirectiveDefinition>>>,
501    /// Registered directive handlers
502    handlers: Arc<tokio::sync::RwLock<HashMap<String, Arc<dyn DirectiveHandler>>>>,
503}
504
505impl DirectiveRegistry {
506    /// Create a new empty directive registry (no built-in directives)
507    pub fn new_empty() -> Self {
508        Self {
509            definitions: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
510            handlers: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
511        }
512    }
513
514    /// Create a new directive registry with built-in directives
515    pub async fn new() -> Self {
516        let registry = Self::new_empty();
517        let _ = registry.register_builtin_directives().await;
518        registry
519    }
520
521    async fn register_builtin_directives(&self) -> Result<()> {
522        // @auth directive
523        self.register_directive(
524            DirectiveDefinition {
525                name: "auth".to_string(),
526                description: Some("Requires authentication and optional role".to_string()),
527                locations: vec![
528                    DirectiveLocation::Object,
529                    DirectiveLocation::FieldDefinition,
530                ],
531                repeatable: false,
532                arguments: vec![DirectiveArgumentDefinition {
533                    name: "requires".to_string(),
534                    arg_type: "String".to_string(),
535                    required: false,
536                    default_value: None,
537                    description: Some("Required role".to_string()),
538                }],
539            },
540            Arc::new(AuthDirectiveHandler),
541        )
542        .await?;
543
544        // @hasRole directive
545        self.register_directive(
546            DirectiveDefinition {
547                name: "hasRole".to_string(),
548                description: Some("Requires specific role".to_string()),
549                locations: vec![DirectiveLocation::FieldDefinition],
550                repeatable: false,
551                arguments: vec![DirectiveArgumentDefinition {
552                    name: "role".to_string(),
553                    arg_type: "String!".to_string(),
554                    required: true,
555                    default_value: None,
556                    description: Some("Required role name".to_string()),
557                }],
558            },
559            Arc::new(HasRoleDirectiveHandler),
560        )
561        .await?;
562
563        // @uppercase directive
564        self.register_directive(
565            DirectiveDefinition {
566                name: "uppercase".to_string(),
567                description: Some("Converts string to uppercase".to_string()),
568                locations: vec![DirectiveLocation::FieldDefinition],
569                repeatable: false,
570                arguments: vec![],
571            },
572            Arc::new(UppercaseDirectiveHandler),
573        )
574        .await?;
575
576        // @lowercase directive
577        self.register_directive(
578            DirectiveDefinition {
579                name: "lowercase".to_string(),
580                description: Some("Converts string to lowercase".to_string()),
581                locations: vec![DirectiveLocation::FieldDefinition],
582                repeatable: false,
583                arguments: vec![],
584            },
585            Arc::new(LowercaseDirectiveHandler),
586        )
587        .await?;
588
589        // @constraint directive
590        self.register_directive(
591            DirectiveDefinition {
592                name: "constraint".to_string(),
593                description: Some("Validates field value constraints".to_string()),
594                locations: vec![
595                    DirectiveLocation::FieldDefinition,
596                    DirectiveLocation::ArgumentDefinition,
597                ],
598                repeatable: false,
599                arguments: vec![
600                    DirectiveArgumentDefinition {
601                        name: "min".to_string(),
602                        arg_type: "Int".to_string(),
603                        required: false,
604                        default_value: None,
605                        description: Some("Minimum value".to_string()),
606                    },
607                    DirectiveArgumentDefinition {
608                        name: "max".to_string(),
609                        arg_type: "Int".to_string(),
610                        required: false,
611                        default_value: None,
612                        description: Some("Maximum value".to_string()),
613                    },
614                    DirectiveArgumentDefinition {
615                        name: "minLength".to_string(),
616                        arg_type: "Int".to_string(),
617                        required: false,
618                        default_value: None,
619                        description: Some("Minimum string length".to_string()),
620                    },
621                    DirectiveArgumentDefinition {
622                        name: "maxLength".to_string(),
623                        arg_type: "Int".to_string(),
624                        required: false,
625                        default_value: None,
626                        description: Some("Maximum string length".to_string()),
627                    },
628                ],
629            },
630            Arc::new(ConstraintDirectiveHandler),
631        )
632        .await?;
633
634        // @cacheControl directive
635        self.register_directive(
636            DirectiveDefinition {
637                name: "cacheControl".to_string(),
638                description: Some("Cache control hints".to_string()),
639                locations: vec![
640                    DirectiveLocation::Object,
641                    DirectiveLocation::FieldDefinition,
642                ],
643                repeatable: false,
644                arguments: vec![
645                    DirectiveArgumentDefinition {
646                        name: "maxAge".to_string(),
647                        arg_type: "Int".to_string(),
648                        required: false,
649                        default_value: Some(DirectiveValue::Int(0)),
650                        description: Some("Max age in seconds".to_string()),
651                    },
652                    DirectiveArgumentDefinition {
653                        name: "scope".to_string(),
654                        arg_type: "String".to_string(),
655                        required: false,
656                        default_value: Some(DirectiveValue::String("PUBLIC".to_string())),
657                        description: Some("Cache scope (PUBLIC or PRIVATE)".to_string()),
658                    },
659                ],
660            },
661            Arc::new(CacheControlDirectiveHandler::new()),
662        )
663        .await?;
664
665        Ok(())
666    }
667
668    /// Register a custom directive
669    pub async fn register_directive(
670        &self,
671        definition: DirectiveDefinition,
672        handler: Arc<dyn DirectiveHandler>,
673    ) -> Result<()> {
674        let mut definitions = self.definitions.write().await;
675        let mut handlers = self.handlers.write().await;
676
677        definitions.insert(definition.name.clone(), definition.clone());
678        handlers.insert(definition.name, handler);
679
680        Ok(())
681    }
682
683    /// Get directive definition
684    pub async fn get_definition(&self, name: &str) -> Option<DirectiveDefinition> {
685        let definitions = self.definitions.read().await;
686        definitions.get(name).cloned()
687    }
688
689    /// Execute a directive
690    pub async fn execute_directive(
691        &self,
692        directive: &AppliedDirective,
693        context: &DirectiveContext,
694    ) -> Result<DirectiveValue> {
695        let handlers = self.handlers.read().await;
696
697        if let Some(handler) = handlers.get(&directive.name) {
698            handler.execute(directive, context)
699        } else {
700            Err(anyhow!("Unknown directive: @{}", directive.name))
701        }
702    }
703
704    /// Validate a directive
705    pub async fn validate_directive(&self, directive: &AppliedDirective) -> Result<()> {
706        let handlers = self.handlers.read().await;
707
708        if let Some(handler) = handlers.get(&directive.name) {
709            handler.validate(directive)
710        } else {
711            Err(anyhow!("Unknown directive: @{}", directive.name))
712        }
713    }
714
715    /// Get all registered directive names
716    pub async fn get_directive_names(&self) -> Vec<String> {
717        let definitions = self.definitions.read().await;
718        definitions.keys().cloned().collect()
719    }
720}
721
722// Note: No Default implementation since new() is async
723// Use DirectiveRegistry::new().await or DirectiveRegistry::new_empty()
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728
729    #[test]
730    fn test_directive_value_types() {
731        let string_val = DirectiveValue::String("test".to_string());
732        assert_eq!(string_val.as_string(), Some("test"));
733
734        let int_val = DirectiveValue::Int(42);
735        assert_eq!(int_val.as_int(), Some(42));
736
737        let bool_val = DirectiveValue::Boolean(true);
738        assert_eq!(bool_val.as_bool(), Some(true));
739    }
740
741    #[test]
742    fn test_applied_directive_creation() {
743        let directive =
744            AppliedDirective::new("auth".to_string(), DirectiveLocation::FieldDefinition)
745                .with_argument(
746                    "requires".to_string(),
747                    DirectiveValue::String("ADMIN".to_string()),
748                );
749
750        assert_eq!(directive.name, "auth");
751        assert_eq!(directive.arguments.len(), 1);
752        assert_eq!(
753            directive
754                .get_argument("requires")
755                .and_then(|v| v.as_string()),
756            Some("ADMIN")
757        );
758    }
759
760    #[test]
761    fn test_directive_context() {
762        let context = DirectiveContext::new()
763            .with_field("email".to_string())
764            .with_type("User".to_string());
765
766        assert_eq!(context.field_name, Some("email".to_string()));
767        assert_eq!(context.type_name, Some("User".to_string()));
768    }
769
770    #[tokio::test]
771    async fn test_directive_registry_creation() {
772        let registry = DirectiveRegistry::new().await;
773        let names = registry.get_directive_names().await;
774
775        // Should have built-in directives
776        assert!(names.contains(&"auth".to_string()));
777        assert!(names.contains(&"uppercase".to_string()));
778        assert!(names.contains(&"constraint".to_string()));
779    }
780
781    #[tokio::test]
782    async fn test_uppercase_directive() {
783        let handler = UppercaseDirectiveHandler;
784        let directive =
785            AppliedDirective::new("uppercase".to_string(), DirectiveLocation::FieldDefinition);
786        let context =
787            DirectiveContext::new().with_value(DirectiveValue::String("hello".to_string()));
788
789        let result = handler
790            .execute(&directive, &context)
791            .expect("should succeed");
792        assert_eq!(result.as_string(), Some("HELLO"));
793    }
794
795    #[tokio::test]
796    async fn test_lowercase_directive() {
797        let handler = LowercaseDirectiveHandler;
798        let directive =
799            AppliedDirective::new("lowercase".to_string(), DirectiveLocation::FieldDefinition);
800        let context =
801            DirectiveContext::new().with_value(DirectiveValue::String("HELLO".to_string()));
802
803        let result = handler
804            .execute(&directive, &context)
805            .expect("should succeed");
806        assert_eq!(result.as_string(), Some("hello"));
807    }
808
809    #[tokio::test]
810    async fn test_constraint_directive_min_max() {
811        let handler = ConstraintDirectiveHandler;
812        let directive =
813            AppliedDirective::new("constraint".to_string(), DirectiveLocation::FieldDefinition)
814                .with_argument("min".to_string(), DirectiveValue::Int(0))
815                .with_argument("max".to_string(), DirectiveValue::Int(100));
816
817        // Valid value
818        let context = DirectiveContext::new().with_value(DirectiveValue::Int(50));
819        assert!(handler.execute(&directive, &context).is_ok());
820
821        // Too low
822        let context = DirectiveContext::new().with_value(DirectiveValue::Int(-5));
823        assert!(handler.execute(&directive, &context).is_err());
824
825        // Too high
826        let context = DirectiveContext::new().with_value(DirectiveValue::Int(150));
827        assert!(handler.execute(&directive, &context).is_err());
828    }
829
830    #[tokio::test]
831    async fn test_auth_directive_no_context() {
832        let handler = AuthDirectiveHandler;
833        let directive =
834            AppliedDirective::new("auth".to_string(), DirectiveLocation::FieldDefinition);
835        let context = DirectiveContext::new();
836
837        let result = handler.execute(&directive, &context);
838        assert!(result.is_err());
839        assert!(result
840            .unwrap_err()
841            .to_string()
842            .contains("Authentication required"));
843    }
844
845    #[tokio::test]
846    async fn test_auth_directive_with_role() {
847        let handler = AuthDirectiveHandler;
848        let directive =
849            AppliedDirective::new("auth".to_string(), DirectiveLocation::FieldDefinition)
850                .with_argument(
851                    "requires".to_string(),
852                    DirectiveValue::String("ADMIN".to_string()),
853                );
854
855        let mut user_context = HashMap::new();
856        user_context.insert("role".to_string(), "USER".to_string());
857
858        let context = DirectiveContext::new().with_user_context(user_context);
859
860        let result = handler.execute(&directive, &context);
861        assert!(result.is_err());
862        assert!(result
863            .unwrap_err()
864            .to_string()
865            .contains("Insufficient permissions"));
866    }
867
868    #[tokio::test]
869    async fn test_has_role_directive() {
870        let handler = HasRoleDirectiveHandler;
871        let directive =
872            AppliedDirective::new("hasRole".to_string(), DirectiveLocation::FieldDefinition)
873                .with_argument(
874                    "role".to_string(),
875                    DirectiveValue::String("admin".to_string()),
876                );
877
878        let mut user_context = HashMap::new();
879        user_context.insert("roles".to_string(), "user,admin,moderator".to_string());
880
881        let context = DirectiveContext::new().with_user_context(user_context);
882
883        let result = handler.execute(&directive, &context);
884        assert!(result.is_ok());
885    }
886}