1use 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>> {
57 let enums_arr = enums_value.as_array().ok_or_else(|| FraiseQLError::Validation {
58 message: "enums must be an array".to_string(),
59 path: Some("schema.enums".to_string()),
60 })?;
61
62 let mut enums = Vec::new();
63 for (idx, enum_def) in enums_arr.iter().enumerate() {
64 let enum_obj = enum_def.as_object().ok_or_else(|| FraiseQLError::Validation {
65 message: format!("enum at index {} must be an object", idx),
66 path: Some(format!("schema.enums[{}]", idx)),
67 })?;
68
69 let enum_type = Self::parse_single_enum(enum_obj, idx)?;
70 enums.push(enum_type);
71 }
72
73 Ok(enums)
74 }
75
76 fn parse_single_enum(
83 enum_obj: &serde_json::Map<String, Value>,
84 index: usize,
85 ) -> Result<IREnum> {
86 let name = enum_obj
88 .get("name")
89 .and_then(|v| v.as_str())
90 .ok_or_else(|| FraiseQLError::Validation {
91 message: "enum must have a name".to_string(),
92 path: Some(format!("schema.enums[{}].name", index)),
93 })?
94 .to_string();
95
96 Self::validate_enum_name(&name)?;
98
99 let description =
101 enum_obj.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
102
103 let values_value = enum_obj.get("values").ok_or_else(|| FraiseQLError::Validation {
105 message: format!("enum '{}' must have 'values' field", name),
106 path: Some(format!("schema.enums[{}].values", index)),
107 })?;
108
109 let values = Self::parse_enum_values(values_value, &name)?;
110
111 if values.is_empty() {
113 return Err(FraiseQLError::Validation {
114 message: format!("enum '{}' must have at least one value", name),
115 path: Some(format!("schema.enums[{}].values", index)),
116 });
117 }
118
119 Ok(IREnum {
120 name,
121 values,
122 description,
123 })
124 }
125
126 fn parse_enum_values(values_value: &Value, enum_name: &str) -> Result<Vec<IREnumValue>> {
133 let values_arr = values_value.as_array().ok_or_else(|| FraiseQLError::Validation {
134 message: format!("enum '{}' values must be an array", enum_name),
135 path: Some(format!("schema.enums.{}.values", enum_name)),
136 })?;
137
138 let mut values = Vec::new();
139 let mut seen_names = std::collections::HashSet::new();
140
141 for (idx, value_def) in values_arr.iter().enumerate() {
142 let value_obj = value_def.as_object().ok_or_else(|| FraiseQLError::Validation {
143 message: format!("enum '{}' value at index {} must be an object", enum_name, idx),
144 path: Some(format!("schema.enums.{}.values[{}]", enum_name, idx)),
145 })?;
146
147 let value_name = value_obj
149 .get("name")
150 .and_then(|v| v.as_str())
151 .ok_or_else(|| FraiseQLError::Validation {
152 message: format!(
153 "enum '{}' value at index {} must have a name",
154 enum_name, idx
155 ),
156 path: Some(format!("schema.enums.{}.values[{}].name", enum_name, idx)),
157 })?
158 .to_string();
159
160 Self::validate_enum_value_name(&value_name, enum_name)?;
162
163 if !seen_names.insert(value_name.clone()) {
165 return Err(FraiseQLError::Validation {
166 message: format!("enum '{}' has duplicate value '{}'", enum_name, value_name),
167 path: Some(format!("schema.enums.{}.values", enum_name)),
168 });
169 }
170
171 let description =
173 value_obj.get("description").and_then(|v| v.as_str()).map(|s| s.to_string());
174
175 let deprecation_reason = value_obj
177 .get("deprecationReason")
178 .and_then(|v| v.as_str())
179 .map(|s| s.to_string());
180
181 values.push(IREnumValue {
182 name: value_name,
183 description,
184 deprecation_reason,
185 });
186 }
187
188 Ok(values)
189 }
190
191 fn validate_enum_name(name: &str) -> Result<()> {
195 if name.is_empty() {
196 return Err(FraiseQLError::Validation {
197 message: "enum name cannot be empty".to_string(),
198 path: Some("schema.enums.name".to_string()),
199 });
200 }
201
202 if !name.chars().next().unwrap().is_alphabetic() {
203 return Err(FraiseQLError::Validation {
204 message: format!("enum name '{}' must start with a letter", name),
205 path: Some("schema.enums.name".to_string()),
206 });
207 }
208
209 if !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
210 return Err(FraiseQLError::Validation {
211 message: format!(
212 "enum name '{}' contains invalid characters (use alphanumeric and underscore)",
213 name
214 ),
215 path: Some("schema.enums.name".to_string()),
216 });
217 }
218
219 Ok(())
220 }
221
222 fn validate_enum_value_name(name: &str, enum_name: &str) -> Result<()> {
226 if name.is_empty() {
227 return Err(FraiseQLError::Validation {
228 message: format!("enum '{}' value name cannot be empty", enum_name),
229 path: Some(format!("schema.enums.{}.values.name", enum_name)),
230 });
231 }
232
233 if !name.chars().all(|c| c.is_uppercase() || c.is_numeric() || c == '_') {
234 return Err(FraiseQLError::Validation {
235 message: format!(
236 "enum '{}' value '{}' should use SCREAMING_SNAKE_CASE (uppercase with underscores)",
237 enum_name, name
238 ),
239 path: Some(format!("schema.enums.{}.values.name", enum_name)),
240 });
241 }
242
243 if name.starts_with('_') {
245 return Err(FraiseQLError::Validation {
246 message: format!(
247 "enum '{}' value '{}' cannot start with underscore",
248 enum_name, name
249 ),
250 path: Some(format!("schema.enums.{}.values.name", enum_name)),
251 });
252 }
253
254 Ok(())
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn test_parse_simple_enum() {
264 let json = serde_json::json!([
265 {
266 "name": "Status",
267 "values": [
268 {"name": "ACTIVE"},
269 {"name": "INACTIVE"}
270 ]
271 }
272 ]);
273
274 let result = EnumValidator::parse_enums(&json);
275 assert!(result.is_ok());
276 let enums = result.unwrap();
277 assert_eq!(enums.len(), 1);
278 assert_eq!(enums[0].name, "Status");
279 assert_eq!(enums[0].values.len(), 2);
280 }
281
282 #[test]
283 fn test_parse_enum_with_description() {
284 let json = serde_json::json!([
285 {
286 "name": "UserStatus",
287 "description": "User account status",
288 "values": [
289 {
290 "name": "ACTIVE",
291 "description": "User is active"
292 }
293 ]
294 }
295 ]);
296
297 let result = EnumValidator::parse_enums(&json);
298 assert!(result.is_ok());
299 let enums = result.unwrap();
300 assert_eq!(enums[0].description, Some("User account status".to_string()));
301 assert_eq!(enums[0].values[0].description, Some("User is active".to_string()));
302 }
303
304 #[test]
305 fn test_parse_enum_with_deprecation() {
306 let json = serde_json::json!([
307 {
308 "name": "Status",
309 "values": [
310 {
311 "name": "OLD_STATUS",
312 "deprecationReason": "Use NEW_STATUS instead"
313 }
314 ]
315 }
316 ]);
317
318 let result = EnumValidator::parse_enums(&json);
319 assert!(result.is_ok());
320 let enums = result.unwrap();
321 assert_eq!(
322 enums[0].values[0].deprecation_reason,
323 Some("Use NEW_STATUS instead".to_string())
324 );
325 }
326
327 #[test]
328 fn test_parse_multiple_enums() {
329 let json = serde_json::json!([
330 {
331 "name": "Status",
332 "values": [{"name": "ACTIVE"}]
333 },
334 {
335 "name": "Priority",
336 "values": [{"name": "HIGH"}, {"name": "LOW"}]
337 }
338 ]);
339
340 let result = EnumValidator::parse_enums(&json);
341 assert!(result.is_ok());
342 let enums = result.unwrap();
343 assert_eq!(enums.len(), 2);
344 }
345
346 #[test]
347 fn test_enum_not_array() {
348 let json = serde_json::json!({"name": "Status"});
349 let result = EnumValidator::parse_enums(&json);
350 assert!(result.is_err());
351 }
352
353 #[test]
354 fn test_enum_missing_name() {
355 let json = serde_json::json!([
356 {
357 "values": [{"name": "ACTIVE"}]
358 }
359 ]);
360
361 let result = EnumValidator::parse_enums(&json);
362 assert!(result.is_err());
363 }
364
365 #[test]
366 fn test_enum_missing_values() {
367 let json = serde_json::json!([
368 {
369 "name": "Status"
370 }
371 ]);
372
373 let result = EnumValidator::parse_enums(&json);
374 assert!(result.is_err());
375 }
376
377 #[test]
378 fn test_enum_empty_values() {
379 let json = serde_json::json!([
380 {
381 "name": "Status",
382 "values": []
383 }
384 ]);
385
386 let result = EnumValidator::parse_enums(&json);
387 assert!(result.is_err());
388 }
389
390 #[test]
391 fn test_enum_duplicate_values() {
392 let json = serde_json::json!([
393 {
394 "name": "Status",
395 "values": [
396 {"name": "ACTIVE"},
397 {"name": "ACTIVE"}
398 ]
399 }
400 ]);
401
402 let result = EnumValidator::parse_enums(&json);
403 assert!(result.is_err());
404 }
405
406 #[test]
407 fn test_enum_value_missing_name() {
408 let json = serde_json::json!([
409 {
410 "name": "Status",
411 "values": [
412 {"description": "Active status"}
413 ]
414 }
415 ]);
416
417 let result = EnumValidator::parse_enums(&json);
418 assert!(result.is_err());
419 }
420
421 #[test]
422 fn test_validate_enum_name_valid() {
423 assert!(EnumValidator::validate_enum_name("Status").is_ok());
424 assert!(EnumValidator::validate_enum_name("UserStatus").is_ok());
425 assert!(EnumValidator::validate_enum_name("Status2").is_ok());
426 }
427
428 #[test]
429 fn test_validate_enum_name_invalid_start() {
430 assert!(EnumValidator::validate_enum_name("2Status").is_err());
431 }
432
433 #[test]
434 fn test_validate_enum_name_invalid_chars() {
435 assert!(EnumValidator::validate_enum_name("Status-Type").is_err());
436 assert!(EnumValidator::validate_enum_name("Status Type").is_err());
437 }
438
439 #[test]
440 fn test_validate_enum_value_valid() {
441 assert!(EnumValidator::validate_enum_value_name("ACTIVE", "Status").is_ok());
442 assert!(EnumValidator::validate_enum_value_name("ACTIVE_STATUS", "Status").is_ok());
443 assert!(EnumValidator::validate_enum_value_name("ACTIVE_STATUS_2", "Status").is_ok());
444 }
445
446 #[test]
447 fn test_validate_enum_value_invalid_lowercase() {
448 assert!(EnumValidator::validate_enum_value_name("Active", "Status").is_err());
449 }
450
451 #[test]
452 fn test_validate_enum_value_invalid_start_underscore() {
453 assert!(EnumValidator::validate_enum_value_name("_ACTIVE", "Status").is_err());
454 }
455
456 #[test]
457 fn test_enum_name_empty() {
458 assert!(EnumValidator::validate_enum_name("").is_err());
459 }
460
461 #[test]
462 fn test_parse_complex_enum_scenario() {
463 let json = serde_json::json!([
464 {
465 "name": "OrderStatus",
466 "description": "Order processing status",
467 "values": [
468 {
469 "name": "PENDING",
470 "description": "Order awaiting processing"
471 },
472 {
473 "name": "PROCESSING",
474 "description": "Order is being processed"
475 },
476 {
477 "name": "COMPLETED",
478 "description": "Order has been completed"
479 },
480 {
481 "name": "CANCELLED",
482 "description": "Order was cancelled",
483 "deprecationReason": "Use VOID instead"
484 }
485 ]
486 }
487 ]);
488
489 let result = EnumValidator::parse_enums(&json);
490 assert!(result.is_ok());
491 let enums = result.unwrap();
492 assert_eq!(enums[0].name, "OrderStatus");
493 assert_eq!(enums[0].values.len(), 4);
494 assert!(enums[0].values[3].deprecation_reason.is_some());
495 }
496
497 #[test]
498 fn test_serialization_roundtrip() {
499 let enum_val = IREnum {
500 name: "Status".to_string(),
501 values: vec![IREnumValue {
502 name: "ACTIVE".to_string(),
503 description: Some("Active status".to_string()),
504 deprecation_reason: None,
505 }],
506 description: Some("Status enum".to_string()),
507 };
508
509 let json = serde_json::to_string(&enum_val).expect("serialize should work");
510 let restored: IREnum = serde_json::from_str(&json).expect("deserialize should work");
511
512 assert_eq!(restored.name, enum_val.name);
513 assert_eq!(restored.values.len(), enum_val.values.len());
514 }
515}