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}