Skip to main content

fraiseql_core/schema/compiled/
directive.rs

1use serde::{Deserialize, Serialize};
2
3use super::argument::ArgumentDefinition;
4
5/// A custom directive definition for schema extension.
6///
7/// Allows defining custom directives beyond the built-in `@skip`, `@include`,
8/// and `@deprecated` directives. Custom directives are exposed via introspection
9/// and can be evaluated at runtime via registered handlers.
10///
11/// # Example
12///
13/// ```
14/// use fraiseql_core::schema::{DirectiveDefinition, DirectiveLocationKind, ArgumentDefinition, FieldType};
15///
16/// let rate_limit = DirectiveDefinition {
17///     name: "rateLimit".to_string(),
18///     description: Some("Apply rate limiting to this field".to_string()),
19///     locations: vec![DirectiveLocationKind::FieldDefinition],
20///     arguments: vec![
21///         ArgumentDefinition::new("limit", FieldType::Int),
22///         ArgumentDefinition::optional("window", FieldType::String),
23///     ],
24///     is_repeatable: false,
25/// };
26/// ```
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct DirectiveDefinition {
29    /// Directive name (e.g., "rateLimit", "auth").
30    pub name: String,
31
32    /// Description of what this directive does.
33    #[serde(default, skip_serializing_if = "Option::is_none")]
34    pub description: Option<String>,
35
36    /// Valid locations where this directive can be applied.
37    pub locations: Vec<DirectiveLocationKind>,
38
39    /// Arguments this directive accepts.
40    #[serde(default)]
41    pub arguments: Vec<ArgumentDefinition>,
42
43    /// Whether this directive can be applied multiple times to the same location.
44    #[serde(default)]
45    pub is_repeatable: bool,
46}
47
48impl DirectiveDefinition {
49    /// Create a new directive definition.
50    #[must_use]
51    pub fn new(name: impl Into<String>, locations: Vec<DirectiveLocationKind>) -> Self {
52        Self {
53            name: name.into(),
54            description: None,
55            locations,
56            arguments: Vec::new(),
57            is_repeatable: false,
58        }
59    }
60
61    /// Set the description.
62    #[must_use]
63    pub fn with_description(mut self, description: impl Into<String>) -> Self {
64        self.description = Some(description.into());
65        self
66    }
67
68    /// Add an argument to this directive.
69    #[must_use]
70    pub fn with_argument(mut self, arg: ArgumentDefinition) -> Self {
71        self.arguments.push(arg);
72        self
73    }
74
75    /// Add multiple arguments to this directive.
76    #[must_use]
77    pub fn with_arguments(mut self, args: Vec<ArgumentDefinition>) -> Self {
78        self.arguments = args;
79        self
80    }
81
82    /// Mark this directive as repeatable.
83    #[must_use]
84    pub const fn repeatable(mut self) -> Self {
85        self.is_repeatable = true;
86        self
87    }
88
89    /// Check if this directive can be applied at the given location.
90    #[must_use]
91    pub fn valid_at(&self, location: DirectiveLocationKind) -> bool {
92        self.locations.contains(&location)
93    }
94
95    /// Find an argument by name.
96    #[must_use]
97    pub fn find_argument(&self, name: &str) -> Option<&ArgumentDefinition> {
98        self.arguments.iter().find(|a| a.name == name)
99    }
100}
101
102/// Directive location kinds for custom directive definitions.
103///
104/// This mirrors `DirectiveLocation` in introspection but is used for
105/// compiled schema definitions. The two types can be converted between
106/// each other for introspection purposes.
107///
108/// Per GraphQL spec ยง3.13, directive locations fall into two categories:
109/// - Executable locations (operations, fields, fragments)
110/// - Type system locations (schema definitions)
111#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
113#[non_exhaustive]
114pub enum DirectiveLocationKind {
115    // Executable directive locations
116    /// Directive on query operation.
117    Query,
118    /// Directive on mutation operation.
119    Mutation,
120    /// Directive on subscription operation.
121    Subscription,
122    /// Directive on field selection.
123    Field,
124    /// Directive on fragment definition.
125    FragmentDefinition,
126    /// Directive on fragment spread.
127    FragmentSpread,
128    /// Directive on inline fragment.
129    InlineFragment,
130    /// Directive on variable definition.
131    VariableDefinition,
132
133    // Type system directive locations
134    /// Directive on schema definition.
135    Schema,
136    /// Directive on scalar type definition.
137    Scalar,
138    /// Directive on object type definition.
139    Object,
140    /// Directive on field definition.
141    FieldDefinition,
142    /// Directive on argument definition.
143    ArgumentDefinition,
144    /// Directive on interface definition.
145    Interface,
146    /// Directive on union definition.
147    Union,
148    /// Directive on enum definition.
149    Enum,
150    /// Directive on enum value definition.
151    EnumValue,
152    /// Directive on input object definition.
153    InputObject,
154    /// Directive on input field definition.
155    InputFieldDefinition,
156}
157
158impl DirectiveLocationKind {
159    /// Check if this is an executable directive location.
160    #[must_use]
161    pub const fn is_executable(&self) -> bool {
162        matches!(
163            self,
164            Self::Query
165                | Self::Mutation
166                | Self::Subscription
167                | Self::Field
168                | Self::FragmentDefinition
169                | Self::FragmentSpread
170                | Self::InlineFragment
171                | Self::VariableDefinition
172        )
173    }
174
175    /// Check if this is a type system directive location.
176    #[must_use]
177    pub const fn is_type_system(&self) -> bool {
178        !self.is_executable()
179    }
180}