Skip to main content

ferro_json_ui/
visibility.rs

1//! Conditional visibility rules for JSON-UI components.
2//!
3//! Visibility rules determine whether a component is rendered based
4//! on data conditions. Conditions reference data paths (JSONPath-style)
5//! and support logical composition with AND, OR, and NOT operators.
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10/// Comparison operators for visibility conditions.
11///
12/// Phase 165 F13: `IsTrue` / `IsFalse` operate directly on booleans without
13/// requiring `{"operator": "eq", "value": false}`. They also handle missing
14/// paths cleanly (treated as `false`). For consumer patterns that pair a
15/// `has_X: bool` controller field with a visibility gate, prefer these over
16/// `Empty` (which returns `false` for booleans by design — see resolver).
17#[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    /// Match when the resolved value is the boolean `true`.
32    /// Missing path and non-boolean values are treated as `false`.
33    IsTrue,
34    /// Match when the resolved value is the boolean `false`, or when the
35    /// path is missing / resolves to `null`. Mirror of `IsTrue`.
36    IsFalse,
37}
38
39/// A single visibility condition comparing a data path against a value.
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
41pub struct VisibilityCondition {
42    /// JSONPath-style reference to data.
43    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/// Visibility rule with logical composition support.
50///
51/// Uses `#[serde(untagged)]` to support clean JSON:
52/// - Simple: `{"path": "/data/users", "operator": "not_empty"}`
53/// - Compound: `{"and": [...]}`
54/// - Nested: `{"not": {"path": ..., "operator": ...}}`
55///
56/// Deserialize is hand-rolled (not derived) to produce a shape-listing error
57/// message when an unknown JSON object is encountered. The derive-based
58/// untagged impl emits "data did not match any variant of untagged enum
59/// Visibility" — useless for debugging. See D-19/F5.
60#[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    /// Evaluates this visibility rule against handler data.
118    ///
119    /// Infallible: malformed conditions, missing paths, and type mismatches all
120    /// resolve to `false` (visibility hides the element) without panicking. This
121    /// is the contract Phase 116's renderer relies on per CONTEXT D-13.
122    ///
123    /// Edge cases (per Phase 116 RESEARCH §"Visibility ... TO BE ADDED" and
124    /// Assumptions Log A1):
125    /// - `Eq` against a missing path → `false` (no value to compare).
126    /// - `NotEq` against a missing path → `true` (no value, so it's "not equal" to anything).
127    /// - Numeric comparators (`Gt`/`Lt`/`Gte`/`Lte`) against non-numeric or missing → `false`.
128    /// - `Contains` on a scalar value → `false`.
129    /// - `NotEmpty`/`Empty` treat numbers and booleans as non-empty.
130    ///
131    /// Visibility is a presentation-layer concern only. Never treat
132    /// `visible: false` as a server-side access check — enforce authorization
133    /// in the routing/handler layer, not here.
134    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, // missing path is "not equal" per A1
157            (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        // F13: boolean-typed paths — match the booleans directly.
185        VisibilityOperator::IsTrue => matches!(resolved, Some(serde_json::Value::Bool(true))),
186        VisibilityOperator::IsFalse => match resolved {
187            Some(serde_json::Value::Bool(false)) => true,
188            // Treat missing / null as "not true" → matches IsFalse,
189            // mirroring the typical controller pattern where an absent
190            // bool field is the same as `false`.
191            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    // ── F13 boolean-operator tests ──────────────────────────────────────
300
301    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}