1use crate::model_ast::{
4 AssignableTarget, ConditionDef, ConditionParam, ModelFile, RelationDef, RelationExpr, TypeDef,
5};
6use pest::Parser;
7use pest::iterators::Pair;
8use pest_derive::Parser;
9
10#[derive(Parser)]
11#[grammar = "model.pest"]
12pub struct ModelParser;
13
14pub fn parse_dsl(dsl: &str) -> Result<ModelFile, pest::error::Error<Rule>> {
16 let mut pairs = ModelParser::parse(Rule::file, dsl)?;
17 let file = pairs.next().expect("parser should return file root");
18 let mut type_defs = Vec::new();
19 let mut condition_defs = Vec::new();
20
21 for pair in file.into_inner() {
22 match pair.as_rule() {
23 Rule::type_def => type_defs.push(build_type_def(pair)?),
24 Rule::condition_def => condition_defs.push(build_condition_def(pair)?),
25 Rule::EOI => (),
26 _ => unreachable!("Unexpected rule: {:?}", pair.as_rule()),
27 }
28 }
29
30 Ok(ModelFile {
31 type_defs,
32 condition_defs,
33 })
34}
35
36fn build_type_def(pair: Pair<Rule>) -> Result<TypeDef, pest::error::Error<Rule>> {
37 let mut inner = pair.into_inner();
38 let name = inner.next().unwrap().as_str().to_string();
39 let mut relations = Vec::new();
40 let mut permissions = Vec::new();
41
42 for pair in inner {
43 match pair.as_rule() {
44 Rule::relations_block => relations = build_relations_block(pair)?,
45 Rule::permissions_block => permissions = build_permissions_block(pair)?,
46 _ => {}
47 }
48 }
49
50 Ok(TypeDef {
51 name,
52 relations,
53 permissions,
54 })
55}
56
57fn build_relations_block(pair: Pair<Rule>) -> Result<Vec<RelationDef>, pest::error::Error<Rule>> {
58 pair.into_inner().map(build_relation_def).collect()
59}
60
61fn build_permissions_block(pair: Pair<Rule>) -> Result<Vec<RelationDef>, pest::error::Error<Rule>> {
62 pair.into_inner().map(build_permission_def).collect()
63}
64
65fn build_relation_def(pair: Pair<Rule>) -> Result<RelationDef, pest::error::Error<Rule>> {
66 let mut inner = pair.into_inner();
67 let name = inner.next().unwrap().as_str().to_string();
68 let expression = build_relation_expr(inner.next().unwrap())?;
69 Ok(RelationDef { name, expression })
70}
71
72fn build_permission_def(pair: Pair<Rule>) -> Result<RelationDef, pest::error::Error<Rule>> {
73 let mut inner = pair.into_inner();
74 let name = inner.next().unwrap().as_str().to_string();
75 let expression = build_relation_expr(inner.next().unwrap())?;
76 Ok(RelationDef { name, expression })
77}
78
79fn build_relation_expr(pair: Pair<Rule>) -> Result<RelationExpr, pest::error::Error<Rule>> {
80 let exclusion_pair = pair.into_inner().next().unwrap();
82 build_exclusion_expr(exclusion_pair)
83}
84
85fn build_union_expr(pair: Pair<Rule>) -> Result<RelationExpr, pest::error::Error<Rule>> {
86 let mut exprs = Vec::new();
87 for p in pair.into_inner() {
88 if p.as_rule() == Rule::primary_expr {
89 exprs.push(build_primary_expr(p)?);
90 }
91 }
92 if exprs.len() > 1 {
93 Ok(RelationExpr::Union(exprs))
94 } else {
95 Ok(exprs.pop().unwrap())
96 }
97}
98
99fn build_intersection_expr(pair: Pair<Rule>) -> Result<RelationExpr, pest::error::Error<Rule>> {
100 let mut exprs = Vec::new();
101 for p in pair.into_inner() {
102 if p.as_rule() == Rule::union_expr {
103 exprs.push(build_union_expr(p)?);
104 }
105 }
106 if exprs.len() > 1 {
107 Ok(RelationExpr::Intersection(exprs))
108 } else {
109 Ok(exprs.pop().unwrap())
110 }
111}
112
113fn build_exclusion_expr(pair: Pair<Rule>) -> Result<RelationExpr, pest::error::Error<Rule>> {
114 let mut inner = pair.into_inner();
115 let base = build_intersection_expr(inner.next().unwrap())?;
116 if let Some(subtract) = inner.next() {
117 Ok(RelationExpr::Exclusion {
118 base: Box::new(base),
119 subtract: Box::new(build_intersection_expr(subtract)?),
120 })
121 } else {
122 Ok(base)
123 }
124}
125
126fn build_primary_expr(pair: Pair<Rule>) -> Result<RelationExpr, pest::error::Error<Rule>> {
127 let inner = pair.into_inner().next().unwrap();
128 match inner.as_rule() {
129 Rule::computed_userset => Ok(RelationExpr::ComputedUserset(inner.as_str().to_string())),
130 Rule::tuple_to_userset => {
131 let mut parts = inner.into_inner();
132 let tupleset = parts.next().unwrap().as_str().to_string();
133 let computed_userset = parts.next().unwrap().as_str().to_string();
134 Ok(RelationExpr::TupleToUserset {
135 computed_userset,
136 tupleset,
137 })
138 }
139 Rule::direct_assignment => {
140 let targets = inner
141 .into_inner()
142 .map(build_assignable_target)
143 .collect::<Result<_, _>>()?;
144 Ok(RelationExpr::DirectAssignment(targets))
145 }
146 _ => unreachable!(),
147 }
148}
149
150fn build_assignable_target(pair: Pair<Rule>) -> Result<AssignableTarget, pest::error::Error<Rule>> {
151 let span = pair.as_span();
152 let text = span.as_str();
153 let mut inner = pair.into_inner();
154 let type_spec = inner.next().unwrap();
155 let type_name = type_spec.as_str().to_string();
156
157 if text.ends_with(":*") {
162 Ok(AssignableTarget::Wildcard(type_name))
164 } else if let Some(next) = inner.next() {
165 if text.contains(" with ") {
167 let condition = next.as_str().to_string();
169 Ok(AssignableTarget::Conditional {
170 target: Box::new(AssignableTarget::Type(type_name)),
171 condition,
172 })
173 } else {
174 let relation = next.as_str().to_string();
176 Ok(AssignableTarget::Userset {
177 type_name,
178 relation,
179 })
180 }
181 } else {
182 Ok(AssignableTarget::Type(type_name))
184 }
185}
186
187fn build_condition_def(pair: Pair<Rule>) -> Result<ConditionDef, pest::error::Error<Rule>> {
188 let mut inner = pair.into_inner();
189 let name = inner.next().unwrap().as_str().to_string();
190 let mut params = Vec::new();
191 let mut expression = "".to_string();
192
193 for part in inner {
194 match part.as_rule() {
195 Rule::condition_param => params.push(build_condition_param(part)?),
196 Rule::condition_expr => expression = part.as_str().to_string(),
197 _ => (),
198 }
199 }
200
201 Ok(ConditionDef {
202 name,
203 params,
204 expression,
205 })
206}
207
208fn build_condition_param(pair: Pair<Rule>) -> Result<ConditionParam, pest::error::Error<Rule>> {
209 let mut inner = pair.into_inner();
210 let name = inner.next().unwrap().as_str().to_string();
211 let param_type = inner.next().unwrap().as_str().to_string();
212 Ok(ConditionParam { name, param_type })
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218 use pretty_assertions::assert_eq;
219
220 #[test]
221 fn test_parse_simple_type() {
222 let dsl = "type user {}";
223 let expected = ModelFile {
224 type_defs: vec![TypeDef {
225 name: "user".to_string(),
226 relations: vec![],
227 permissions: vec![],
228 }],
229 condition_defs: vec![],
230 };
231 assert_eq!(parse_dsl(dsl).unwrap(), expected);
232 }
233
234 #[test]
235 fn test_parse_type_with_relations() {
236 let dsl = r#"
237 type document {
238 relations
239 define viewer: [user]
240 define editor: [user | group#member]
241 }
242 "#;
243 let expected = ModelFile {
244 type_defs: vec![TypeDef {
245 name: "document".to_string(),
246 relations: vec![
247 RelationDef {
248 name: "viewer".to_string(),
249 expression: RelationExpr::DirectAssignment(vec![AssignableTarget::Type(
250 "user".to_string(),
251 )]),
252 },
253 RelationDef {
254 name: "editor".to_string(),
255 expression: RelationExpr::DirectAssignment(vec![
256 AssignableTarget::Type("user".to_string()),
257 AssignableTarget::Userset {
258 type_name: "group".to_string(),
259 relation: "member".to_string(),
260 },
261 ]),
262 },
263 ],
264 permissions: vec![],
265 }],
266 condition_defs: vec![],
267 };
268 assert_eq!(parse_dsl(dsl).unwrap(), expected);
269 }
270
271 #[test]
272 fn test_parse_computed_userset() {
273 let dsl = "type folder { relations define can_view: owner }";
274 let expected = ModelFile {
275 type_defs: vec![TypeDef {
276 name: "folder".to_string(),
277 relations: vec![RelationDef {
278 name: "can_view".to_string(),
279 expression: RelationExpr::ComputedUserset("owner".to_string()),
280 }],
281 permissions: vec![],
282 }],
283 condition_defs: vec![],
284 };
285 assert_eq!(parse_dsl(dsl).unwrap(), expected);
286 }
287
288 #[test]
289 fn test_parse_ttu() {
290 let dsl = "type document { relations define viewer: parent->viewer }";
291 let expected = ModelFile {
292 type_defs: vec![TypeDef {
293 name: "document".to_string(),
294 relations: vec![RelationDef {
295 name: "viewer".to_string(),
296 expression: RelationExpr::TupleToUserset {
297 computed_userset: "viewer".to_string(),
298 tupleset: "parent".to_string(),
299 },
300 }],
301 permissions: vec![],
302 }],
303 condition_defs: vec![],
304 };
305 assert_eq!(parse_dsl(dsl).unwrap(), expected);
306 }
307
308 #[test]
309 fn test_parse_union() {
310 let dsl = "type document { relations define viewer: [user] + editor }";
311 let expected = RelationExpr::Union(vec![
312 RelationExpr::DirectAssignment(vec![AssignableTarget::Type("user".to_string())]),
313 RelationExpr::ComputedUserset("editor".to_string()),
314 ]);
315 let model = parse_dsl(dsl).unwrap();
316 assert_eq!(model.type_defs[0].relations[0].expression, expected);
317 }
318
319 #[test]
320 fn test_parse_whitespace_only() {
321 let whitespace_model = " \n\t ";
322 let result = parse_dsl(whitespace_model);
323 assert!(
324 result.is_ok(),
325 "Whitespace-only model should parse successfully as empty model"
326 );
327
328 let model = result.unwrap();
329 assert_eq!(
330 model.type_defs.len(),
331 0,
332 "Empty model should have no type definitions"
333 );
334 assert_eq!(
335 model.condition_defs.len(),
336 0,
337 "Empty model should have no condition definitions"
338 );
339 }
340
341 #[test]
342 fn test_parse_comment_only() {
343 let comment_model = "// This is just a comment\n/* Another comment */";
344 let result = parse_dsl(comment_model);
345 assert!(
346 result.is_ok(),
347 "Comment-only model should parse successfully as empty model"
348 );
349
350 let model = result.unwrap();
351 assert_eq!(
352 model.type_defs.len(),
353 0,
354 "Comment-only model should have no type definitions"
355 );
356 assert_eq!(
357 model.condition_defs.len(),
358 0,
359 "Comment-only model should have no condition definitions"
360 );
361 }
362
363 #[test]
364 fn test_parse_invalid_syntax() {
365 let invalid_model = "type user { relations define viewer: [ }";
366 let result = parse_dsl(invalid_model);
367 assert!(result.is_err(), "Invalid syntax should fail to parse");
368 }
369
370 #[test]
371 fn test_parse_condition() {
372 let dsl = r#"
373 condition ip_check(allowed_cidrs: list<string>, request_ip: string) {
374 request_ip in allowed_cidrs
375 }
376 "#;
377 let expected = ModelFile {
378 type_defs: vec![],
379 condition_defs: vec![ConditionDef {
380 name: "ip_check".to_string(),
381 params: vec![
382 ConditionParam {
383 name: "allowed_cidrs".to_string(),
384 param_type: "list<string>".to_string(),
385 },
386 ConditionParam {
387 name: "request_ip".to_string(),
388 param_type: "string".to_string(),
389 },
390 ],
391 expression: "request_ip in allowed_cidrs".to_string(),
392 }],
393 };
394 assert_eq!(parse_dsl(dsl).unwrap(), expected);
395 }
396
397 #[test]
398 fn test_parse_intersection() {
399 let dsl = "type document { relations define viewer: [user] & editor }";
400 let expected = RelationExpr::Intersection(vec![
401 RelationExpr::DirectAssignment(vec![AssignableTarget::Type("user".to_string())]),
402 RelationExpr::ComputedUserset("editor".to_string()),
403 ]);
404 let model = parse_dsl(dsl).unwrap();
405 assert_eq!(model.type_defs[0].relations[0].expression, expected);
406 }
407
408 #[test]
409 fn test_parse_exclusion() {
410 let dsl = "type document { relations define viewer: [user] - banned }";
411 let expected = RelationExpr::Exclusion {
412 base: Box::new(RelationExpr::DirectAssignment(vec![
413 AssignableTarget::Type("user".to_string()),
414 ])),
415 subtract: Box::new(RelationExpr::ComputedUserset("banned".to_string())),
416 };
417 let model = parse_dsl(dsl).unwrap();
418 assert_eq!(model.type_defs[0].relations[0].expression, expected);
419 }
420
421 #[test]
422 fn test_parse_nested_set_ops() {
423 let dsl = "type document { relations define viewer: [user] + editor - banned }";
428 let model = parse_dsl(dsl).unwrap();
429
430 match &model.type_defs[0].relations[0].expression {
432 RelationExpr::Exclusion { base, subtract } => {
433 match &**base {
434 RelationExpr::Union(exprs) => {
435 assert_eq!(exprs.len(), 2);
436 assert!(matches!(exprs[0], RelationExpr::DirectAssignment(_)));
437 assert!(matches!(exprs[1], RelationExpr::ComputedUserset(_)));
438 }
439 _ => panic!("Expected Union expression"),
440 }
441 assert!(matches!(**subtract, RelationExpr::ComputedUserset(_)));
442 }
443 _ => panic!("Expected Exclusion expression"),
444 }
445 }
446
447 #[test]
448 fn test_parse_wildcard() {
449 let dsl = "type document { relations define viewer: [user:*] }";
450 let expected = ModelFile {
451 type_defs: vec![TypeDef {
452 name: "document".to_string(),
453 relations: vec![RelationDef {
454 name: "viewer".to_string(),
455 expression: RelationExpr::DirectAssignment(vec![AssignableTarget::Wildcard(
456 "user".to_string(),
457 )]),
458 }],
459 permissions: vec![],
460 }],
461 condition_defs: vec![],
462 };
463 assert_eq!(parse_dsl(dsl).unwrap(), expected);
464 }
465
466 #[test]
467 fn test_parse_conditional_type() {
468 let dsl = "type document { relations define viewer: [user with ip_check] }";
469 let expected = ModelFile {
470 type_defs: vec![TypeDef {
471 name: "document".to_string(),
472 relations: vec![RelationDef {
473 name: "viewer".to_string(),
474 expression: RelationExpr::DirectAssignment(vec![
475 AssignableTarget::Conditional {
476 target: Box::new(AssignableTarget::Type("user".to_string())),
477 condition: "ip_check".to_string(),
478 },
479 ]),
480 }],
481 permissions: vec![],
482 }],
483 condition_defs: vec![],
484 };
485 assert_eq!(parse_dsl(dsl).unwrap(), expected);
486 }
487
488 #[test]
489 fn test_parse_multiple_types() {
490 let dsl = r#"
491 type user {}
492 type document {
493 relations
494 define viewer: [user]
495 }
496 type folder {
497 relations
498 define parent: [folder]
499 }
500 "#;
501 let model = parse_dsl(dsl).unwrap();
502 assert_eq!(model.type_defs.len(), 3);
503 assert_eq!(model.type_defs[0].name, "user");
504 assert_eq!(model.type_defs[1].name, "document");
505 assert_eq!(model.type_defs[2].name, "folder");
506 }
507
508 #[test]
509 fn test_parse_complex_real_world() {
510 let dsl = r#"
512 type user {}
513
514 type organization {
515 relations
516 define member: [user]
517 define admin: [user]
518 }
519
520 type folder {
521 relations
522 define parent: [folder]
523 define owner: [user]
524 define editor: [user | organization#member]
525 define viewer: [user | organization#member]
526 define can_view: viewer + editor + owner
527 define can_edit: editor + owner
528 define can_delete: owner
529 define can_share: owner
530 }
531
532 type document {
533 relations
534 define parent: [folder]
535 define owner: [user]
536 define editor: [user | group#member | team#member]
537 define viewer: [user | group#member]
538 define can_view: viewer + editor + owner + parent->can_view
539 define can_edit: editor + owner + parent->can_edit
540 define can_delete: owner
541 define can_comment: can_view
542 }
543 "#;
544 let model = parse_dsl(dsl).unwrap();
545 assert_eq!(model.type_defs.len(), 4);
546
547 let folder = model.type_defs.iter().find(|t| t.name == "folder").unwrap();
549 assert_eq!(folder.relations.len(), 8);
550
551 let document = model
553 .type_defs
554 .iter()
555 .find(|t| t.name == "document")
556 .unwrap();
557 assert_eq!(document.relations.len(), 8);
558 }
559
560 #[test]
561 fn test_parse_empty_string() {
562 let dsl = "";
563 let result = parse_dsl(dsl).unwrap();
564 assert_eq!(result.type_defs.len(), 0);
565 assert_eq!(result.condition_defs.len(), 0);
566 }
567}
568
569#[test]
570fn test_parse_mixed_precedence_first_and_second_plus_third() {
571 let dsl = "type document {
575 relations
576 define first: [user]
577 define second: [user]
578 define third: [user]
579 permissions
580 define mixed_precedence2 = first & second + third
581 }";
582
583 let model = parse_dsl(dsl).unwrap();
584
585 match &model.type_defs[0].permissions[0].expression {
587 RelationExpr::Intersection(exprs) => {
588 assert_eq!(exprs.len(), 2);
589 assert!(matches!(&exprs[0], RelationExpr::ComputedUserset(name) if name == "first"));
590 match &exprs[1] {
591 RelationExpr::Union(union_exprs) => {
592 assert_eq!(union_exprs.len(), 2);
593 assert!(
594 matches!(&union_exprs[0], RelationExpr::ComputedUserset(name) if name == "second")
595 );
596 assert!(
597 matches!(&union_exprs[1], RelationExpr::ComputedUserset(name) if name == "third")
598 );
599 }
600 _ => panic!("Expected Union expression as second operand of Intersection"),
601 }
602 }
603 _ => panic!(
604 "Expected Intersection expression, got: {:?}",
605 model.type_defs[0].permissions[0].expression
606 ),
607 }
608}