1use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
18#[serde(rename_all = "snake_case")]
19pub enum VisibilityOperator {
20 Exists,
21 NotExists,
22 Eq,
23 NotEq,
24 Gt,
25 Lt,
26 Gte,
27 Lte,
28 Contains,
29 NotEmpty,
30 Empty,
31 IsTrue,
34 IsFalse,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
41pub struct VisibilityCondition {
42 pub path: String,
44 pub operator: VisibilityOperator,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub value: Option<serde_json::Value>,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, JsonSchema)]
61#[serde(untagged)]
62pub enum Visibility {
63 And { and: Vec<Visibility> },
64 Or { or: Vec<Visibility> },
65 Not { not: Box<Visibility> },
66 Condition(VisibilityCondition),
67}
68
69impl<'de> serde::Deserialize<'de> for Visibility {
70 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
71 use serde::de::Error;
72 let v = serde_json::Value::deserialize(d)?;
73 if let Some(obj) = v.as_object() {
74 if obj.contains_key("and") {
75 #[derive(serde::Deserialize)]
76 struct AndShape {
77 and: Vec<Visibility>,
78 }
79 let shape: AndShape =
80 serde_json::from_value(v.clone()).map_err(D::Error::custom)?;
81 return Ok(Visibility::And { and: shape.and });
82 }
83 if obj.contains_key("or") {
84 #[derive(serde::Deserialize)]
85 struct OrShape {
86 or: Vec<Visibility>,
87 }
88 let shape: OrShape = serde_json::from_value(v.clone()).map_err(D::Error::custom)?;
89 return Ok(Visibility::Or { or: shape.or });
90 }
91 if obj.contains_key("not") {
92 #[derive(serde::Deserialize)]
93 struct NotShape {
94 not: Box<Visibility>,
95 }
96 let shape: NotShape =
97 serde_json::from_value(v.clone()).map_err(D::Error::custom)?;
98 return Ok(Visibility::Not { not: shape.not });
99 }
100 if obj.contains_key("path") && obj.contains_key("operator") {
101 let cond: VisibilityCondition =
102 serde_json::from_value(v).map_err(D::Error::custom)?;
103 return Ok(Visibility::Condition(cond));
104 }
105 }
106 Err(D::Error::custom(format!(
107 "invalid Visibility shape: {v}. Accepted shapes: \
108 {{\"and\": [...]}}, \
109 {{\"or\": [...]}}, \
110 {{\"not\": {{...}}}}, \
111 {{\"path\": \"/p\", \"operator\": \"...\", \"value\": ...}}"
112 )))
113 }
114}
115
116impl Visibility {
117 pub fn evaluate(&self, data: &serde_json::Value) -> bool {
135 match self {
136 Visibility::And { and } => and.iter().all(|v| v.evaluate(data)),
137 Visibility::Or { or } => or.iter().any(|v| v.evaluate(data)),
138 Visibility::Not { not } => !not.evaluate(data),
139 Visibility::Condition(c) => evaluate_condition(c, data),
140 }
141 }
142}
143
144fn evaluate_condition(c: &VisibilityCondition, data: &serde_json::Value) -> bool {
145 use crate::data::resolve_path;
146 let resolved = resolve_path(data, &c.path);
147 match c.operator {
148 VisibilityOperator::Exists => matches!(resolved, Some(v) if !v.is_null()),
149 VisibilityOperator::NotExists => !matches!(resolved, Some(v) if !v.is_null()),
150 VisibilityOperator::Eq => match (resolved, c.value.as_ref()) {
151 (Some(v), Some(target)) => v == target,
152 _ => false,
153 },
154 VisibilityOperator::NotEq => match (resolved, c.value.as_ref()) {
155 (Some(v), Some(target)) => v != target,
156 (None, _) => true, (Some(_), None) => true,
158 },
159 VisibilityOperator::Gt => numeric_cmp(resolved, c.value.as_ref(), |a, b| a > b),
160 VisibilityOperator::Lt => numeric_cmp(resolved, c.value.as_ref(), |a, b| a < b),
161 VisibilityOperator::Gte => numeric_cmp(resolved, c.value.as_ref(), |a, b| a >= b),
162 VisibilityOperator::Lte => numeric_cmp(resolved, c.value.as_ref(), |a, b| a <= b),
163 VisibilityOperator::Contains => match (resolved, c.value.as_ref()) {
164 (Some(serde_json::Value::String(s)), Some(serde_json::Value::String(t))) => {
165 s.contains(t)
166 }
167 (Some(serde_json::Value::Array(arr)), Some(target)) => arr.iter().any(|v| v == target),
168 _ => false,
169 },
170 VisibilityOperator::NotEmpty => match resolved {
171 Some(serde_json::Value::String(s)) => !s.is_empty(),
172 Some(serde_json::Value::Array(arr)) => !arr.is_empty(),
173 Some(serde_json::Value::Object(obj)) => !obj.is_empty(),
174 Some(serde_json::Value::Number(_)) | Some(serde_json::Value::Bool(_)) => true,
175 _ => false,
176 },
177 VisibilityOperator::Empty => match resolved {
178 Some(serde_json::Value::String(s)) => s.is_empty(),
179 Some(serde_json::Value::Array(arr)) => arr.is_empty(),
180 Some(serde_json::Value::Object(obj)) => obj.is_empty(),
181 Some(serde_json::Value::Number(_)) | Some(serde_json::Value::Bool(_)) => false,
182 None | Some(serde_json::Value::Null) => true,
183 },
184 VisibilityOperator::IsTrue => matches!(resolved, Some(serde_json::Value::Bool(true))),
186 VisibilityOperator::IsFalse => match resolved {
187 Some(serde_json::Value::Bool(false)) => true,
188 None | Some(serde_json::Value::Null) => true,
192 _ => false,
193 },
194 }
195}
196
197fn numeric_cmp(
198 resolved: Option<&serde_json::Value>,
199 target: Option<&serde_json::Value>,
200 op: fn(f64, f64) -> bool,
201) -> bool {
202 match (resolved, target) {
203 (Some(serde_json::Value::Number(a)), Some(serde_json::Value::Number(b))) => {
204 match (a.as_f64(), b.as_f64()) {
205 (Some(af), Some(bf)) => op(af, bf),
206 _ => false,
207 }
208 }
209 _ => false,
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use serde_json::json;
217
218 #[test]
219 fn simple_condition_round_trips() {
220 let json = r#"{"path": "/data/users", "operator": "not_empty"}"#;
221 let vis: Visibility = serde_json::from_str(json).unwrap();
222 match &vis {
223 Visibility::Condition(c) => {
224 assert_eq!(c.path, "/data/users");
225 assert_eq!(c.operator, VisibilityOperator::NotEmpty);
226 assert!(c.value.is_none());
227 }
228 _ => panic!("expected Condition variant"),
229 }
230 let serialized = serde_json::to_string(&vis).unwrap();
231 let reparsed: Visibility = serde_json::from_str(&serialized).unwrap();
232 assert_eq!(vis, reparsed);
233 }
234
235 #[test]
236 fn condition_with_value() {
237 let json = r#"{"path": "/auth/user/role", "operator": "eq", "value": "admin"}"#;
238 let vis: Visibility = serde_json::from_str(json).unwrap();
239 match &vis {
240 Visibility::Condition(c) => {
241 assert_eq!(c.operator, VisibilityOperator::Eq);
242 assert_eq!(
243 c.value,
244 Some(serde_json::Value::String("admin".to_string()))
245 );
246 }
247 _ => panic!("expected Condition variant"),
248 }
249 }
250
251 #[test]
252 fn compound_and_condition() {
253 let json = r#"{
254 "and": [
255 {"path": "/auth/user", "operator": "exists"},
256 {"path": "/auth/user/role", "operator": "eq", "value": "admin"}
257 ]
258 }"#;
259 let vis: Visibility = serde_json::from_str(json).unwrap();
260 match &vis {
261 Visibility::And { and } => {
262 assert_eq!(and.len(), 2);
263 }
264 _ => panic!("expected And variant"),
265 }
266 let serialized = serde_json::to_string(&vis).unwrap();
267 let reparsed: Visibility = serde_json::from_str(&serialized).unwrap();
268 assert_eq!(vis, reparsed);
269 }
270
271 #[test]
272 fn compound_or_condition() {
273 let json = r#"{
274 "or": [
275 {"path": "/data/status", "operator": "eq", "value": "active"},
276 {"path": "/data/status", "operator": "eq", "value": "pending"}
277 ]
278 }"#;
279 let vis: Visibility = serde_json::from_str(json).unwrap();
280 assert!(matches!(vis, Visibility::Or { .. }));
281 }
282
283 #[test]
284 fn nested_not_condition() {
285 let json = r#"{"not": {"path": "/data/deleted", "operator": "exists"}}"#;
286 let vis: Visibility = serde_json::from_str(json).unwrap();
287 match &vis {
288 Visibility::Not { not } => match not.as_ref() {
289 Visibility::Condition(c) => {
290 assert_eq!(c.path, "/data/deleted");
291 assert_eq!(c.operator, VisibilityOperator::Exists);
292 }
293 _ => panic!("expected Condition inside Not"),
294 },
295 _ => panic!("expected Not variant"),
296 }
297 }
298
299 fn eval(op: VisibilityOperator, data: serde_json::Value, path: &str) -> bool {
302 let v = Visibility::Condition(VisibilityCondition {
303 path: path.to_string(),
304 operator: op,
305 value: None,
306 });
307 v.evaluate(&data)
308 }
309
310 #[test]
311 fn is_true_matches_only_bool_true() {
312 assert!(eval(
313 VisibilityOperator::IsTrue,
314 serde_json::json!({"x": true}),
315 "/x"
316 ));
317 assert!(!eval(
318 VisibilityOperator::IsTrue,
319 serde_json::json!({"x": false}),
320 "/x"
321 ));
322 assert!(!eval(
323 VisibilityOperator::IsTrue,
324 serde_json::json!({}),
325 "/x"
326 ));
327 assert!(!eval(
328 VisibilityOperator::IsTrue,
329 serde_json::json!({"x": null}),
330 "/x"
331 ));
332 assert!(!eval(
333 VisibilityOperator::IsTrue,
334 serde_json::json!({"x": "true"}),
335 "/x"
336 ));
337 assert!(!eval(
338 VisibilityOperator::IsTrue,
339 serde_json::json!({"x": 1}),
340 "/x"
341 ));
342 }
343
344 #[test]
345 fn is_false_matches_bool_false_or_missing_or_null() {
346 assert!(eval(
347 VisibilityOperator::IsFalse,
348 serde_json::json!({"x": false}),
349 "/x"
350 ));
351 assert!(eval(
352 VisibilityOperator::IsFalse,
353 serde_json::json!({}),
354 "/x"
355 ));
356 assert!(eval(
357 VisibilityOperator::IsFalse,
358 serde_json::json!({"x": null}),
359 "/x"
360 ));
361 assert!(!eval(
362 VisibilityOperator::IsFalse,
363 serde_json::json!({"x": true}),
364 "/x"
365 ));
366 assert!(!eval(
367 VisibilityOperator::IsFalse,
368 serde_json::json!({"x": "false"}),
369 "/x"
370 ));
371 }
372
373 #[test]
374 fn is_true_is_false_round_trip() {
375 let t: VisibilityOperator = serde_json::from_str(r#""is_true""#).unwrap();
376 assert_eq!(t, VisibilityOperator::IsTrue);
377 let f: VisibilityOperator = serde_json::from_str(r#""is_false""#).unwrap();
378 assert_eq!(f, VisibilityOperator::IsFalse);
379 assert_eq!(serde_json::to_string(&t).unwrap(), r#""is_true""#);
380 assert_eq!(serde_json::to_string(&f).unwrap(), r#""is_false""#);
381 }
382
383 #[test]
384 fn all_operators_serialize() {
385 let operators = vec![
386 (VisibilityOperator::Exists, "exists"),
387 (VisibilityOperator::NotExists, "not_exists"),
388 (VisibilityOperator::Eq, "eq"),
389 (VisibilityOperator::NotEq, "not_eq"),
390 (VisibilityOperator::Gt, "gt"),
391 (VisibilityOperator::Lt, "lt"),
392 (VisibilityOperator::Gte, "gte"),
393 (VisibilityOperator::Lte, "lte"),
394 (VisibilityOperator::Contains, "contains"),
395 (VisibilityOperator::NotEmpty, "not_empty"),
396 (VisibilityOperator::Empty, "empty"),
397 (VisibilityOperator::IsTrue, "is_true"),
398 (VisibilityOperator::IsFalse, "is_false"),
399 ];
400 for (op, expected) in operators {
401 let json = serde_json::to_value(&op).unwrap();
402 assert_eq!(
403 json, expected,
404 "operator {op:?} should serialize to {expected}"
405 );
406 }
407 }
408
409 #[test]
410 fn evaluate_exists_true_for_present_non_null() {
411 let vis = Visibility::Condition(VisibilityCondition {
412 path: "/user".into(),
413 operator: VisibilityOperator::Exists,
414 value: None,
415 });
416 assert!(vis.evaluate(&json!({"user": "alice"})));
417 }
418
419 #[test]
420 fn evaluate_exists_false_for_missing() {
421 let vis = Visibility::Condition(VisibilityCondition {
422 path: "/missing".into(),
423 operator: VisibilityOperator::Exists,
424 value: None,
425 });
426 assert!(!vis.evaluate(&json!({"user": "alice"})));
427 }
428
429 #[test]
430 fn evaluate_exists_false_for_null() {
431 let vis = Visibility::Condition(VisibilityCondition {
432 path: "/user".into(),
433 operator: VisibilityOperator::Exists,
434 value: None,
435 });
436 assert!(!vis.evaluate(&json!({"user": null})));
437 }
438
439 #[test]
440 fn evaluate_not_exists_inverse_of_exists() {
441 let vis = Visibility::Condition(VisibilityCondition {
442 path: "/missing".into(),
443 operator: VisibilityOperator::NotExists,
444 value: None,
445 });
446 assert!(vis.evaluate(&json!({})));
447 }
448
449 #[test]
450 fn evaluate_eq_matches_value() {
451 let vis = Visibility::Condition(VisibilityCondition {
452 path: "/role".into(),
453 operator: VisibilityOperator::Eq,
454 value: Some(json!("admin")),
455 });
456 assert!(vis.evaluate(&json!({"role": "admin"})));
457 assert!(!vis.evaluate(&json!({"role": "user"})));
458 }
459
460 #[test]
461 fn evaluate_eq_missing_path_returns_false() {
462 let vis = Visibility::Condition(VisibilityCondition {
463 path: "/missing".into(),
464 operator: VisibilityOperator::Eq,
465 value: Some(json!("x")),
466 });
467 assert!(!vis.evaluate(&json!({})));
468 }
469
470 #[test]
471 fn evaluate_not_eq_missing_path_returns_true() {
472 let vis = Visibility::Condition(VisibilityCondition {
473 path: "/missing".into(),
474 operator: VisibilityOperator::NotEq,
475 value: Some(json!("x")),
476 });
477 assert!(vis.evaluate(&json!({})));
478 }
479
480 #[test]
481 fn evaluate_numeric_comparators() {
482 for (op, expect_for_5_vs_3) in [
483 (VisibilityOperator::Gt, true),
484 (VisibilityOperator::Gte, true),
485 (VisibilityOperator::Lt, false),
486 (VisibilityOperator::Lte, false),
487 ] {
488 let vis = Visibility::Condition(VisibilityCondition {
489 path: "/n".into(),
490 operator: op,
491 value: Some(json!(3)),
492 });
493 assert_eq!(vis.evaluate(&json!({"n": 5})), expect_for_5_vs_3);
494 }
495 }
496
497 #[test]
498 fn evaluate_numeric_comparator_on_string_returns_false() {
499 let vis = Visibility::Condition(VisibilityCondition {
500 path: "/n".into(),
501 operator: VisibilityOperator::Gt,
502 value: Some(json!(3)),
503 });
504 assert!(!vis.evaluate(&json!({"n": "five"})));
505 }
506
507 #[test]
508 fn evaluate_contains_string_substring() {
509 let vis = Visibility::Condition(VisibilityCondition {
510 path: "/s".into(),
511 operator: VisibilityOperator::Contains,
512 value: Some(json!("ell")),
513 });
514 assert!(vis.evaluate(&json!({"s": "hello"})));
515 assert!(!vis.evaluate(&json!({"s": "world"})));
516 }
517
518 #[test]
519 fn evaluate_contains_array_membership() {
520 let vis = Visibility::Condition(VisibilityCondition {
521 path: "/tags".into(),
522 operator: VisibilityOperator::Contains,
523 value: Some(json!("admin")),
524 });
525 assert!(vis.evaluate(&json!({"tags": ["user", "admin"]})));
526 assert!(!vis.evaluate(&json!({"tags": ["user", "guest"]})));
527 }
528
529 #[test]
530 fn evaluate_not_empty_for_string_array_object_number_bool() {
531 let make = |op| {
532 Visibility::Condition(VisibilityCondition {
533 path: "/v".into(),
534 operator: op,
535 value: None,
536 })
537 };
538 assert!(make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": "x"})));
539 assert!(make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": [1]})));
540 assert!(make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": {"a": 1}})));
541 assert!(make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": 0})));
542 assert!(make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": false})));
543 assert!(!make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": ""})));
544 assert!(!make(VisibilityOperator::NotEmpty).evaluate(&json!({"v": []})));
545 assert!(!make(VisibilityOperator::NotEmpty).evaluate(&json!({})));
546 }
547
548 #[test]
549 fn deserialize_condition_shape() {
550 let v: Visibility = serde_json::from_value(serde_json::json!({
551 "path": "/x", "operator": "exists"
552 }))
553 .expect("parses");
554 match v {
555 Visibility::Condition(_) => {}
556 other => panic!("expected Condition, got {other:?}"),
557 }
558 }
559
560 #[test]
561 fn deserialize_and_shape() {
562 let v: Visibility = serde_json::from_value(serde_json::json!({
563 "and": [{"path": "/a", "operator": "exists"}, {"path": "/b", "operator": "exists"}]
564 }))
565 .expect("parses");
566 match v {
567 Visibility::And { and } => assert_eq!(and.len(), 2),
568 other => panic!("expected And, got {other:?}"),
569 }
570 }
571
572 #[test]
573 fn deserialize_or_shape() {
574 let v: Visibility = serde_json::from_value(serde_json::json!({
575 "or": [{"path": "/a", "operator": "exists"}]
576 }))
577 .expect("parses");
578 assert!(matches!(v, Visibility::Or { .. }));
579 }
580
581 #[test]
582 fn deserialize_not_shape() {
583 let v: Visibility = serde_json::from_value(serde_json::json!({
584 "not": {"path": "/x", "operator": "exists"}
585 }))
586 .expect("parses");
587 assert!(matches!(v, Visibility::Not { .. }));
588 }
589
590 #[test]
591 fn visibility_roundtrip_all_shapes() {
592 let cases = vec![
593 serde_json::json!({"path": "/x", "operator": "exists"}),
594 serde_json::json!({"and": [{"path": "/a", "operator": "exists"}]}),
595 serde_json::json!({"or": [{"path": "/a", "operator": "exists"}]}),
596 serde_json::json!({"not": {"path": "/x", "operator": "exists"}}),
597 ];
598 for orig in cases {
599 let parsed: Visibility = serde_json::from_value(orig.clone()).expect("parses");
600 let back = serde_json::to_value(&parsed).expect("serializes");
601 assert_eq!(orig, back, "round-trip failed for {orig}");
602 }
603 }
604
605 #[test]
606 fn deserialize_unknown_shape_error_lists_accepted_forms() {
607 let bad = serde_json::json!({"expr": "foo"});
608 let err: serde_json::Error = serde_json::from_value::<Visibility>(bad).unwrap_err();
609 let msg = err.to_string();
610 assert!(msg.contains("and"), "error must mention 'and', got: {msg}");
611 assert!(msg.contains("or"), "error must mention 'or', got: {msg}");
612 assert!(msg.contains("not"), "error must mention 'not', got: {msg}");
613 assert!(
614 msg.contains("path"),
615 "error must mention 'path', got: {msg}"
616 );
617 assert!(
618 msg.contains("operator"),
619 "error must mention 'operator', got: {msg}"
620 );
621 assert!(
622 msg.contains("expr"),
623 "error must include the offending JSON, got: {msg}"
624 );
625 }
626
627 #[test]
628 fn evaluate_compound_and_or_not() {
629 let admin = Visibility::Condition(VisibilityCondition {
630 path: "/role".into(),
631 operator: VisibilityOperator::Eq,
632 value: Some(json!("admin")),
633 });
634 let active = Visibility::Condition(VisibilityCondition {
635 path: "/active".into(),
636 operator: VisibilityOperator::Eq,
637 value: Some(json!(true)),
638 });
639 let both = Visibility::And {
640 and: vec![admin.clone(), active.clone()],
641 };
642 let either = Visibility::Or {
643 or: vec![admin.clone(), active.clone()],
644 };
645 let neither = Visibility::Not {
646 not: Box::new(either.clone()),
647 };
648
649 let data = json!({"role": "admin", "active": false});
650 assert!(!both.evaluate(&data));
651 assert!(either.evaluate(&data));
652 assert!(!neither.evaluate(&data));
653 }
654}