1use crate::model_ast::{AssignableTarget, ModelFile, RelationExpr};
4use std::collections::{HashMap, HashSet};
5
6#[derive(Clone, Copy, PartialEq, Eq)]
7enum VisitState {
8 Unvisited,
9 Visiting,
10 Visited,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum ValidationCategory {
15 DuplicateType,
16 DuplicateRelation,
17 UndefinedRelation,
18 UndefinedType,
19 UndefinedCondition,
20 CycleDetected,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ValidationError {
25 pub category: ValidationCategory,
26 pub message: String,
27}
28
29fn check_computed_userset_cycles(model: &ModelFile, errors: &mut Vec<ValidationError>) {
30 for type_def in &model.type_defs {
31 let graph = build_relation_dependency_graph(type_def);
32
33 let mut state: HashMap<String, VisitState> = graph
34 .keys()
35 .map(|k| (k.clone(), VisitState::Unvisited))
36 .collect();
37
38 for relation in graph.keys() {
39 if state.get(relation) == Some(&VisitState::Unvisited) {
40 let mut stack = Vec::new();
41 detect_cycle_dfs(type_def, relation, &graph, &mut state, &mut stack, errors);
42 }
43 }
44 }
45}
46
47fn build_relation_dependency_graph(
48 type_def: &crate::model_ast::TypeDef,
49) -> HashMap<String, Vec<String>> {
50 let mut graph = HashMap::new();
51
52 for relation in &type_def.relations {
53 let deps = extract_computed_userset_dependencies(&relation.expression);
54 graph.insert(relation.name.clone(), deps);
55 }
56
57 graph
58}
59
60fn extract_computed_userset_dependencies(expr: &RelationExpr) -> Vec<String> {
61 match expr {
62 RelationExpr::ComputedUserset(relation_name) => vec![relation_name.clone()],
63 RelationExpr::Union(exprs) | RelationExpr::Intersection(exprs) => exprs
64 .iter()
65 .flat_map(extract_computed_userset_dependencies)
66 .collect(),
67 RelationExpr::Exclusion { base, subtract } => {
68 let mut deps = extract_computed_userset_dependencies(base);
69 deps.extend(extract_computed_userset_dependencies(subtract));
70 deps
71 }
72 RelationExpr::DirectAssignment(_) | RelationExpr::TupleToUserset { .. } => Vec::new(),
73 }
74}
75
76fn detect_cycle_dfs(
77 type_def: &crate::model_ast::TypeDef,
78 relation: &str,
79 graph: &HashMap<String, Vec<String>>,
80 state: &mut HashMap<String, VisitState>,
81 stack: &mut Vec<String>,
82 errors: &mut Vec<ValidationError>,
83) {
84 state.insert(relation.to_string(), VisitState::Visiting);
85 stack.push(relation.to_string());
86
87 if let Some(neighbors) = graph.get(relation) {
88 for next in neighbors {
89 if !graph.contains_key(next) {
91 continue;
92 }
93
94 let next_state = state.get(next).copied();
95 if next_state == Some(VisitState::Visiting) {
96 if let Some(start_idx) = stack.iter().position(|r| r == next) {
97 let mut cycle_nodes = stack[start_idx..].to_vec();
98 cycle_nodes.push(next.clone());
99 let cycle_path = cycle_nodes
100 .iter()
101 .map(|r| format!("{}#{}", type_def.name, r))
102 .collect::<Vec<_>>()
103 .join(" -> ");
104 errors.push(ValidationError::new(
105 ValidationCategory::CycleDetected,
106 format!("Cycle detected in computed usersets: {}", cycle_path),
107 ));
108 }
109 continue;
110 }
111
112 if next_state != Some(VisitState::Visited) {
113 detect_cycle_dfs(type_def, next, graph, state, stack, errors);
114 }
115 }
116 }
117
118 stack.pop();
119 state.insert(relation.to_string(), VisitState::Visited);
120}
121
122impl ValidationError {
123 fn new(category: ValidationCategory, message: impl Into<String>) -> Self {
124 Self {
125 category,
126 message: message.into(),
127 }
128 }
129}
130
131pub fn validate_model(model: &ModelFile) -> Result<(), Vec<ValidationError>> {
134 let mut errors = Vec::new();
135
136 check_duplicate_types(model, &mut errors);
138
139 check_duplicate_relations(model, &mut errors);
141
142 check_undefined_relations(model, &mut errors);
144
145 check_undefined_types(model, &mut errors);
147
148 check_undefined_conditions(model, &mut errors);
150
151 check_computed_userset_cycles(model, &mut errors);
153
154 if errors.is_empty() {
155 Ok(())
156 } else {
157 Err(errors)
158 }
159}
160
161fn check_duplicate_types(model: &ModelFile, errors: &mut Vec<ValidationError>) {
162 let mut seen = HashSet::new();
163 for type_def in &model.type_defs {
164 if !seen.insert(&type_def.name) {
165 errors.push(ValidationError::new(
166 ValidationCategory::DuplicateType,
167 format!("Duplicate type definition: '{}'", type_def.name),
168 ));
169 }
170 }
171}
172
173fn check_duplicate_relations(model: &ModelFile, errors: &mut Vec<ValidationError>) {
174 for type_def in &model.type_defs {
175 let mut seen = HashSet::new();
176 for relation in &type_def.relations {
177 if !seen.insert(&relation.name) {
178 errors.push(ValidationError::new(
179 ValidationCategory::DuplicateRelation,
180 format!(
181 "Duplicate relation '{}' in type '{}'",
182 relation.name, type_def.name
183 ),
184 ));
185 }
186 }
187 }
188}
189
190fn check_undefined_relations(model: &ModelFile, errors: &mut Vec<ValidationError>) {
191 let type_relations: HashMap<&str, HashSet<&str>> = model
193 .type_defs
194 .iter()
195 .map(|t| {
196 let relations: HashSet<&str> = t.relations.iter().map(|r| r.name.as_str()).collect();
197 (t.name.as_str(), relations)
198 })
199 .collect();
200
201 for type_def in &model.type_defs {
202 for relation in &type_def.relations {
203 check_expr_for_undefined_relations(
204 &relation.expression,
205 &type_def.name,
206 &type_relations,
207 errors,
208 );
209 }
210 }
211}
212
213fn check_expr_for_undefined_relations(
214 expr: &RelationExpr,
215 current_type: &str,
216 type_relations: &HashMap<&str, HashSet<&str>>,
217 errors: &mut Vec<ValidationError>,
218) {
219 match expr {
220 RelationExpr::ComputedUserset(relation_name) => {
221 if let Some(relations) = type_relations.get(current_type)
222 && !relations.contains(relation_name.as_str())
223 {
224 errors.push(ValidationError::new(
225 ValidationCategory::UndefinedRelation,
226 format!(
227 "Undefined relation '{}' in computed userset for type '{}'",
228 relation_name, current_type
229 ),
230 ));
231 }
232 }
233 RelationExpr::TupleToUserset {
234 computed_userset: _,
235 tupleset,
236 } => {
237 if let Some(relations) = type_relations.get(current_type)
239 && !relations.contains(tupleset.as_str())
240 {
241 errors.push(ValidationError::new(
242 ValidationCategory::UndefinedRelation,
243 format!(
244 "Undefined relation '{}' in tuple-to-userset tupleset for type '{}'",
245 tupleset, current_type
246 ),
247 ));
248 }
249 }
252 RelationExpr::Union(exprs) | RelationExpr::Intersection(exprs) => {
253 for e in exprs {
254 check_expr_for_undefined_relations(e, current_type, type_relations, errors);
255 }
256 }
257 RelationExpr::Exclusion { base, subtract } => {
258 check_expr_for_undefined_relations(base, current_type, type_relations, errors);
259 check_expr_for_undefined_relations(subtract, current_type, type_relations, errors);
260 }
261 RelationExpr::DirectAssignment(_) => {
262 }
264 }
265}
266
267fn check_undefined_types(model: &ModelFile, errors: &mut Vec<ValidationError>) {
268 let type_names: HashSet<&str> = model.type_defs.iter().map(|t| t.name.as_str()).collect();
269
270 for type_def in &model.type_defs {
271 for relation in &type_def.relations {
272 check_expr_for_undefined_types(
273 &relation.expression,
274 &type_names,
275 &type_def.name,
276 &relation.name,
277 errors,
278 );
279 }
280 }
281}
282
283fn check_expr_for_undefined_types(
284 expr: &RelationExpr,
285 type_names: &HashSet<&str>,
286 current_type: &str,
287 relation_name: &str,
288 errors: &mut Vec<ValidationError>,
289) {
290 match expr {
291 RelationExpr::DirectAssignment(targets) => {
292 for target in targets {
293 check_target_for_undefined_types(
294 target,
295 type_names,
296 current_type,
297 relation_name,
298 errors,
299 );
300 }
301 }
302 RelationExpr::Union(exprs) | RelationExpr::Intersection(exprs) => {
303 for e in exprs {
304 check_expr_for_undefined_types(e, type_names, current_type, relation_name, errors);
305 }
306 }
307 RelationExpr::Exclusion { base, subtract } => {
308 check_expr_for_undefined_types(base, type_names, current_type, relation_name, errors);
309 check_expr_for_undefined_types(
310 subtract,
311 type_names,
312 current_type,
313 relation_name,
314 errors,
315 );
316 }
317 RelationExpr::ComputedUserset(_) | RelationExpr::TupleToUserset { .. } => {
318 }
320 }
321}
322
323fn check_target_for_undefined_types(
324 target: &AssignableTarget,
325 type_names: &HashSet<&str>,
326 current_type: &str,
327 relation_name: &str,
328 errors: &mut Vec<ValidationError>,
329) {
330 let is_known_type = |type_name: &str| type_name == "user" || type_names.contains(type_name);
331
332 match target {
333 AssignableTarget::Type(type_name) | AssignableTarget::Wildcard(type_name) => {
334 if !is_known_type(type_name.as_str()) {
335 errors.push(ValidationError::new(
336 ValidationCategory::UndefinedType,
337 format!(
338 "Undefined type '{}' in relation '{}' of type '{}'",
339 type_name, relation_name, current_type
340 ),
341 ));
342 }
343 }
344 AssignableTarget::Userset { type_name, .. } => {
345 if !is_known_type(type_name.as_str()) {
346 errors.push(ValidationError::new(
347 ValidationCategory::UndefinedType,
348 format!(
349 "Undefined type '{}' in userset for relation '{}' of type '{}'",
350 type_name, relation_name, current_type
351 ),
352 ));
353 }
354 }
355 AssignableTarget::Conditional { target, .. } => {
356 check_target_for_undefined_types(
357 target,
358 type_names,
359 current_type,
360 relation_name,
361 errors,
362 );
363 }
364 }
365}
366
367fn check_undefined_conditions(model: &ModelFile, errors: &mut Vec<ValidationError>) {
368 let condition_names: HashSet<&str> = model
369 .condition_defs
370 .iter()
371 .map(|c| c.name.as_str())
372 .collect();
373
374 for type_def in &model.type_defs {
375 for relation in &type_def.relations {
376 check_expr_for_undefined_conditions(
377 &relation.expression,
378 &condition_names,
379 &type_def.name,
380 &relation.name,
381 errors,
382 );
383 }
384 }
385}
386
387fn check_expr_for_undefined_conditions(
388 expr: &RelationExpr,
389 condition_names: &HashSet<&str>,
390 current_type: &str,
391 relation_name: &str,
392 errors: &mut Vec<ValidationError>,
393) {
394 match expr {
395 RelationExpr::DirectAssignment(targets) => {
396 for target in targets {
397 check_target_for_undefined_conditions(
398 target,
399 condition_names,
400 current_type,
401 relation_name,
402 errors,
403 );
404 }
405 }
406 RelationExpr::Union(exprs) | RelationExpr::Intersection(exprs) => {
407 for e in exprs {
408 check_expr_for_undefined_conditions(
409 e,
410 condition_names,
411 current_type,
412 relation_name,
413 errors,
414 );
415 }
416 }
417 RelationExpr::Exclusion { base, subtract } => {
418 check_expr_for_undefined_conditions(
419 base,
420 condition_names,
421 current_type,
422 relation_name,
423 errors,
424 );
425 check_expr_for_undefined_conditions(
426 subtract,
427 condition_names,
428 current_type,
429 relation_name,
430 errors,
431 );
432 }
433 RelationExpr::ComputedUserset(_) | RelationExpr::TupleToUserset { .. } => {
434 }
436 }
437}
438
439fn check_target_for_undefined_conditions(
440 target: &AssignableTarget,
441 condition_names: &HashSet<&str>,
442 current_type: &str,
443 relation_name: &str,
444 errors: &mut Vec<ValidationError>,
445) {
446 if let AssignableTarget::Conditional { target, condition } = target {
447 if !condition_names.contains(condition.as_str()) {
448 errors.push(ValidationError::new(
449 ValidationCategory::UndefinedCondition,
450 format!(
451 "Undefined condition '{}' in relation '{}' of type '{}'",
452 condition, relation_name, current_type
453 ),
454 ));
455 }
456 check_target_for_undefined_conditions(
458 target,
459 condition_names,
460 current_type,
461 relation_name,
462 errors,
463 );
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470 use crate::model_parser::parse_dsl;
471
472 #[test]
473 fn test_reject_duplicate_type_names() {
474 let dsl = r#"
475 type user {}
476 type user {}
477 "#;
478 let model = parse_dsl(dsl).unwrap();
479 let result = validate_model(&model);
480 assert!(result.is_err());
481 let errors = result.unwrap_err();
482 assert_eq!(errors.len(), 1);
483 assert!(
484 errors[0]
485 .message
486 .contains("Duplicate type definition: 'user'")
487 );
488 }
489
490 #[test]
491 fn test_reject_undefined_relation_in_computed_userset() {
492 let dsl = r#"
493 type document {
494 relations
495 define viewer: editors
496 }
497 "#;
498 let model = parse_dsl(dsl).unwrap();
499 let result = validate_model(&model);
500 assert!(result.is_err());
501 let errors = result.unwrap_err();
502 assert!(
503 errors
504 .iter()
505 .any(|e| e.message.contains("Undefined relation 'editors'"))
506 );
507 }
508
509 #[test]
510 fn test_reject_undefined_relation_in_ttu() {
511 let dsl = r#"
512 type document {
513 relations
514 define viewer: parents->viewer
515 }
516 "#;
517 let model = parse_dsl(dsl).unwrap();
518 let result = validate_model(&model);
519 assert!(result.is_err());
520 let errors = result.unwrap_err();
521 assert!(
522 errors
523 .iter()
524 .any(|e| e.message.contains("Undefined relation 'parents'"))
525 );
526 }
527
528 #[test]
529 fn test_reject_undefined_type_in_direct_assignment() {
530 let dsl = r#"
531 type document {
532 relations
533 define viewer: [nonexistent]
534 }
535 "#;
536 let model = parse_dsl(dsl).unwrap();
537 let result = validate_model(&model);
538 assert!(result.is_err());
539 let errors = result.unwrap_err();
540 assert!(
541 errors
542 .iter()
543 .any(|e| e.message.contains("Undefined type 'nonexistent'"))
544 );
545 }
546
547 #[test]
548 fn test_reject_invalid_condition_reference() {
549 let dsl = r#"
550 type document {
551 relations
552 define viewer: [user with unknown_cond]
553 }
554 "#;
555 let model = parse_dsl(dsl).unwrap();
556 let result = validate_model(&model);
557 assert!(result.is_err());
558 let errors = result.unwrap_err();
559 assert!(
560 errors
561 .iter()
562 .any(|e| e.message.contains("Undefined condition 'unknown_cond'"))
563 );
564 }
565
566 #[test]
567 fn test_reject_duplicate_relation_names() {
568 let dsl = r#"
569 type document {
570 relations
571 define viewer: [user]
572 define viewer: [user]
573 }
574 "#;
575 let model = parse_dsl(dsl).unwrap();
576 let result = validate_model(&model);
577 assert!(result.is_err());
578 let errors = result.unwrap_err();
579 assert!(
580 errors
581 .iter()
582 .any(|e| e.message.contains("Duplicate relation 'viewer'"))
583 );
584 }
585
586 #[test]
587 fn test_valid_complex_model_passes() {
588 let dsl = r#"
589 type user {}
590
591 type folder {
592 relations
593 define parent: [folder]
594 define owner: [user]
595 define viewer: [user]
596 }
597
598 type document {
599 relations
600 define parent: [folder]
601 define owner: [user]
602 define viewer: [user]
603 define can_view: viewer + owner + parent->can_view
604 }
605
606 condition ip_check(ip: string) {
607 ip == "127.0.0.1"
608 }
609 "#;
610 let model = parse_dsl(dsl).unwrap();
611 let result = validate_model(&model);
612 assert!(
613 result.is_ok(),
614 "Expected valid model to pass, got: {:?}",
615 result
616 );
617 }
618
619 #[test]
620 fn test_reject_self_cycle_in_computed_userset() {
621 let dsl = r#"
622 type document {
623 relations
624 define viewer: viewer
625 }
626 "#;
627 let model = parse_dsl(dsl).unwrap();
628 let result = validate_model(&model);
629 assert!(result.is_err());
630 let errors = result.unwrap_err();
631 assert!(
632 errors
633 .iter()
634 .any(|e| e.message.contains("Cycle detected in computed usersets"))
635 );
636 }
637
638 #[test]
639 fn test_reject_two_node_cycle_in_computed_userset() {
640 let dsl = r#"
641 type document {
642 relations
643 define viewer: editor
644 define editor: viewer
645 }
646 "#;
647 let model = parse_dsl(dsl).unwrap();
648 let result = validate_model(&model);
649 assert!(result.is_err());
650 let errors = result.unwrap_err();
651 assert!(
652 errors
653 .iter()
654 .any(|e| e.message.contains("Cycle detected in computed usersets"))
655 );
656 }
657
658 #[test]
659 fn test_accept_acyclic_computed_userset_chain() {
660 let dsl = r#"
661 type document {
662 relations
663 define owner: [user]
664 define editor: owner
665 define viewer: editor
666 }
667
668 type user {}
669 "#;
670 let model = parse_dsl(dsl).unwrap();
671 let result = validate_model(&model);
672 assert!(
673 result.is_ok(),
674 "Expected acyclic model to pass, got: {:?}",
675 result
676 );
677 }
678}