fraiseql_core/compiler/
enum_validator.rs1use serde_json::Value;
7
8use crate::{
9 compiler::ir::{IREnum, IREnumValue},
10 error::{FraiseQLError, Result},
11};
12
13#[derive(Debug)]
21pub struct EnumValidator;
22
23impl EnumValidator {
24 pub fn parse_enums(enums_value: &Value) -> Result<Vec<IREnum>> {
68 let enums_arr = enums_value.as_array().ok_or_else(|| FraiseQLError::Validation {
69 message: "enums must be an array".to_string(),
70 path: Some("schema.enums".to_string()),
71 })?;
72
73 let mut enums = Vec::new();
74 for (idx, enum_def) in enums_arr.iter().enumerate() {
75 let enum_obj = enum_def.as_object().ok_or_else(|| FraiseQLError::Validation {
76 message: format!("enum at index {} must be an object", idx),
77 path: Some(format!("schema.enums[{}]", idx)),
78 })?;
79
80 let enum_type = Self::parse_single_enum(enum_obj, idx)?;
81 enums.push(enum_type);
82 }
83
84 Ok(enums)
85 }
86
87 fn parse_single_enum(
94 enum_obj: &serde_json::Map<String, Value>,
95 index: usize,
96 ) -> Result<IREnum> {
97 let name = enum_obj
99 .get("name")
100 .and_then(|v| v.as_str())
101 .ok_or_else(|| FraiseQLError::Validation {
102 message: "enum must have a name".to_string(),
103 path: Some(format!("schema.enums[{}].name", index)),
104 })?
105 .to_string();
106
107 Self::validate_enum_name(&name)?;
109
110 let description =
112 enum_obj.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
113
114 let values_value = enum_obj.get("values").ok_or_else(|| FraiseQLError::Validation {
116 message: format!("enum '{}' must have 'values' field", name),
117 path: Some(format!("schema.enums[{}].values", index)),
118 })?;
119
120 let values = Self::parse_enum_values(values_value, &name)?;
121
122 if values.is_empty() {
124 return Err(FraiseQLError::Validation {
125 message: format!("enum '{}' must have at least one value", name),
126 path: Some(format!("schema.enums[{}].values", index)),
127 });
128 }
129
130 Ok(IREnum {
131 name,
132 values,
133 description,
134 })
135 }
136
137 fn parse_enum_values(values_value: &Value, enum_name: &str) -> Result<Vec<IREnumValue>> {
144 let values_arr = values_value.as_array().ok_or_else(|| FraiseQLError::Validation {
145 message: format!("enum '{}' values must be an array", enum_name),
146 path: Some(format!("schema.enums.{}.values", enum_name)),
147 })?;
148
149 let mut values = Vec::new();
150 let mut seen_names = std::collections::HashSet::new();
151
152 for (idx, value_def) in values_arr.iter().enumerate() {
153 let value_obj = value_def.as_object().ok_or_else(|| FraiseQLError::Validation {
154 message: format!("enum '{}' value at index {} must be an object", enum_name, idx),
155 path: Some(format!("schema.enums.{}.values[{}]", enum_name, idx)),
156 })?;
157
158 let value_name = value_obj
160 .get("name")
161 .and_then(|v| v.as_str())
162 .ok_or_else(|| FraiseQLError::Validation {
163 message: format!(
164 "enum '{}' value at index {} must have a name",
165 enum_name, idx
166 ),
167 path: Some(format!("schema.enums.{}.values[{}].name", enum_name, idx)),
168 })?
169 .to_string();
170
171 Self::validate_enum_value_name(&value_name, enum_name)?;
173
174 if !seen_names.insert(value_name.clone()) {
176 return Err(FraiseQLError::Validation {
177 message: format!("enum '{}' has duplicate value '{}'", enum_name, value_name),
178 path: Some(format!("schema.enums.{}.values", enum_name)),
179 });
180 }
181
182 let description =
184 value_obj.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
185
186 let deprecation_reason = value_obj
188 .get("deprecationReason")
189 .and_then(|v| v.as_str())
190 .map(|s| s.to_string());
191
192 values.push(IREnumValue {
193 name: value_name,
194 description,
195 deprecation_reason,
196 });
197 }
198
199 Ok(values)
200 }
201
202 pub(crate) fn validate_enum_name(name: &str) -> Result<()> {
206 if name.is_empty() {
207 return Err(FraiseQLError::Validation {
208 message: "enum name cannot be empty".to_string(),
209 path: Some("schema.enums.name".to_string()),
210 });
211 }
212
213 if !name
214 .chars()
215 .next()
216 .expect("name is non-empty; empty was rejected above")
217 .is_alphabetic()
218 {
219 return Err(FraiseQLError::Validation {
220 message: format!("enum name '{}' must start with a letter", name),
221 path: Some("schema.enums.name".to_string()),
222 });
223 }
224
225 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
226 return Err(FraiseQLError::Validation {
227 message: format!(
228 "enum name '{}' contains invalid characters (use alphanumeric and underscore)",
229 name
230 ),
231 path: Some("schema.enums.name".to_string()),
232 });
233 }
234
235 Ok(())
236 }
237
238 pub(crate) fn validate_enum_value_name(name: &str, enum_name: &str) -> Result<()> {
242 if name.is_empty() {
243 return Err(FraiseQLError::Validation {
244 message: format!("enum '{}' value name cannot be empty", enum_name),
245 path: Some(format!("schema.enums.{}.values.name", enum_name)),
246 });
247 }
248
249 if !name.chars().all(|c| c.is_uppercase() || c.is_numeric() || c == '_') {
250 return Err(FraiseQLError::Validation {
251 message: format!(
252 "enum '{}' value '{}' should use SCREAMING_SNAKE_CASE (uppercase with underscores)",
253 enum_name, name
254 ),
255 path: Some(format!("schema.enums.{}.values.name", enum_name)),
256 });
257 }
258
259 if name.starts_with('_') {
261 return Err(FraiseQLError::Validation {
262 message: format!(
263 "enum '{}' value '{}' cannot start with underscore",
264 enum_name, name
265 ),
266 path: Some(format!("schema.enums.{}.values.name", enum_name)),
267 });
268 }
269
270 Ok(())
271 }
272}