Skip to main content

sofie_engine/rendering/
withheld.rs

1//! Data the template binds only inside parts a viewer may not see is not sent to them either.
2//! Fail closed: where a viewer fails a gated read, only what they are shown of that part of the
3//! data is sent. Everything else is written `null`, so a record keeps its keys and stays truthy on
4//! both sides, or as whether it holds anything where only a condition the viewer sees tests it.
5
6use serde_json::Value;
7use sofie_core::Show;
8use sofie_core::listing::{ITEMS, LISTED_FIELDS};
9
10use super::{Gate, Gates};
11use crate::render::is_truthy;
12use crate::slots::{Slots, Step};
13use crate::syntax::{Expression, Op};
14use crate::walk::{Visit, walk};
15
16const ROOT: usize = 0;
17
18/// The template's bound paths as a tree; empty where nothing is gated.
19#[derive(Debug, Default)]
20pub(crate) struct GatedPaths {
21    nodes: Vec<Node>,
22    gate_sets: Vec<Vec<Gate>>,
23}
24
25#[derive(Debug)]
26struct Node {
27    /// `None` at the root.
28    step: Option<Step>,
29    children: Vec<usize>,
30    /// Each read with the index of the gates around it, all of which must hold.
31    reads: Vec<(Read, usize)>,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35enum Read {
36    /// Everything under it is sent.
37    Whole = 1,
38    Presence = 2,
39    /// Looped over: the list keeps its length.
40    Counted = 4,
41}
42
43#[derive(Clone, Copy, Debug, Default)]
44struct Reads(u8);
45
46impl Reads {
47    fn add(&mut self, read: Read) {
48        self.0 |= read as u8;
49    }
50
51    fn has(self, read: Read) -> bool {
52        self.0 & read as u8 != 0
53    }
54
55    fn any(self) -> bool {
56        self.0 != 0
57    }
58}
59
60/// How one viewer sees a node: its own reads, and whether any read below it is shown or hidden.
61#[derive(Clone, Copy, Debug, Default)]
62struct Seen {
63    shown: Reads,
64    hidden: Reads,
65    shown_below: bool,
66    hidden_below: bool,
67}
68
69/// What one viewer is not sent of a template's data.
70#[derive(Debug)]
71pub struct Withheld<'t> {
72    paths: &'t GatedPaths,
73    /// Empty where every gate holds.
74    seen: Vec<Seen>,
75}
76
77impl GatedPaths {
78    pub(crate) fn of(ops: &[Op]) -> Self {
79        let mut bound = Bound::default();
80        walk(ops, &mut bound);
81        if bound
82            .bindings
83            .iter()
84            .all(|binding| binding.gates.is_empty())
85        {
86            return Self::default();
87        }
88        let mut paths = Self {
89            nodes: vec![Node::new(None)],
90            gate_sets: Vec::new(),
91        };
92        for binding in bound.bindings {
93            let node = paths.node(&binding.steps);
94            let gates = paths.gate_set(&binding.gates);
95            paths.nodes[node].reads.push((binding.read, gates));
96        }
97        paths
98    }
99
100    fn node(&mut self, steps: &[Step]) -> usize {
101        let mut index = ROOT;
102        for step in steps {
103            index = self.child(index, step).unwrap_or_else(|| {
104                self.nodes.push(Node::new(Some(step.clone())));
105                let child = self.nodes.len() - 1;
106                self.nodes[index].children.push(child);
107                child
108            });
109        }
110        index
111    }
112
113    fn child(&self, index: usize, step: &Step) -> Option<usize> {
114        self.child_where(index, |reached| reached == step)
115    }
116
117    fn field_child(&self, index: usize, name: &str) -> Option<usize> {
118        self.child_where(
119            index,
120            |reached| matches!(reached, Step::Field(field) if field == name),
121        )
122    }
123
124    fn child_where(&self, index: usize, reached: impl Fn(&Step) -> bool) -> Option<usize> {
125        self.nodes[index]
126            .children
127            .iter()
128            .copied()
129            .find(|child| self.nodes[*child].step.as_ref().is_some_and(&reached))
130    }
131
132    fn gate_set(&mut self, gates: &[&Gate]) -> usize {
133        let owned: Vec<Gate> = gates.iter().map(|gate| (*gate).clone()).collect();
134        if let Some(index) = self.gate_sets.iter().position(|set| *set == owned) {
135            return index;
136        }
137        self.gate_sets.push(owned);
138        self.gate_sets.len() - 1
139    }
140
141    pub(crate) fn withheld(&self, gates: Option<&dyn Gates>) -> Withheld<'_> {
142        let holds: Vec<bool> = self
143            .gate_sets
144            .iter()
145            .map(|set| set.iter().all(|gate| gate.holds(gates)))
146            .collect();
147        let seen = if holds.iter().all(|holds| *holds) {
148            Vec::new()
149        } else {
150            self.seen(&holds)
151        };
152        Withheld { paths: self, seen }
153    }
154
155    /// A child is always pushed after its parent, so walking backwards sees children first.
156    fn seen(&self, holds: &[bool]) -> Vec<Seen> {
157        let mut seen = vec![Seen::default(); self.nodes.len()];
158        for (index, node) in self.nodes.iter().enumerate().rev() {
159            let mut here = Seen::default();
160            for (read, gates) in &node.reads {
161                let reads = if holds[*gates] {
162                    &mut here.shown
163                } else {
164                    &mut here.hidden
165                };
166                reads.add(*read);
167            }
168            for child in &node.children {
169                let below = seen[*child];
170                here.shown_below |= below.shown.any() || below.shown_below;
171                here.hidden_below |= below.hidden.any() || below.hidden_below;
172            }
173            seen[index] = here;
174        }
175        seen
176    }
177}
178
179impl Node {
180    fn new(step: Option<Step>) -> Self {
181        Self {
182            step,
183            children: Vec::new(),
184            reads: Vec::new(),
185        }
186    }
187}
188
189impl Withheld<'_> {
190    #[must_use]
191    pub fn is_empty(&self) -> bool {
192        self.seen.is_empty()
193    }
194
195    pub fn apply_to(&self, data: &mut Value) {
196        if !self.is_empty() {
197            self.withhold(ROOT, data, false);
198        }
199    }
200
201    /// `enclosed` is under a whole read the viewer fails, so nothing below it is sent unshown.
202    /// A part of the data with no failed read in or around it is sent untouched.
203    fn withhold(&self, index: usize, value: &mut Value, enclosed: bool) {
204        let seen = self.seen[index];
205        if seen.shown.has(Read::Whole) {
206            return;
207        }
208        let enclosed = enclosed || seen.hidden.has(Read::Whole);
209        let failed = enclosed || seen.hidden.any();
210        if !failed && !seen.hidden_below {
211            return;
212        }
213        // The data's root stays a record, as a client screen reads it.
214        if failed && !seen.shown_below && index != ROOT {
215            *value = withheld_value(seen, value);
216            return;
217        }
218        self.keep_shown(index, value, enclosed);
219    }
220
221    fn keep_shown(&self, index: usize, value: &mut Value, enclosed: bool) {
222        let item = self.paths.child(index, &Step::Item);
223        if let Value::Object(fields) = value {
224            for (name, field) in fields {
225                self.keep_field(index, item, name, field, enclosed);
226            }
227            return;
228        }
229        match item {
230            Some(item) if value.is_array() => self.keep_items(item, value, enclosed),
231            _ => *value = withheld_value(self.seen[index], value),
232        }
233    }
234
235    fn keep_field(
236        &self,
237        index: usize,
238        item: Option<usize>,
239        name: &str,
240        field: &mut Value,
241        enclosed: bool,
242    ) {
243        if let Some(child) = self.paths.field_child(index, name) {
244            return self.withhold(child, field, enclosed);
245        }
246        match item {
247            Some(item) if name == ITEMS => self.keep_items(item, field, enclosed),
248            _ => *field = Value::Null,
249        }
250    }
251
252    /// A list keeps its length only where a shown read counts its items.
253    fn keep_items(&self, item: usize, list: &mut Value, enclosed: bool) {
254        match list {
255            Value::Array(items) if self.seen[item].shown.any() => {
256                for value in items {
257                    self.withhold(item, value, enclosed);
258                }
259            }
260            Value::Array(_) => *list = Value::Null,
261            other => self.withhold(item, other, enclosed),
262        }
263    }
264}
265
266fn withheld_value(seen: Seen, value: &Value) -> Value {
267    if seen.shown.has(Read::Presence) {
268        Value::Bool(is_truthy(Some(value.show())))
269    } else {
270        Value::Null
271    }
272}
273
274struct Binding<'t> {
275    steps: Vec<Step>,
276    read: Read,
277    gates: Vec<&'t Gate>,
278}
279
280#[derive(Default)]
281struct Bound<'t> {
282    open: Vec<&'t Gate>,
283    slots: Slots,
284    bindings: Vec<Binding<'t>>,
285}
286
287impl<'t> Visit<'t> for Bound<'t> {
288    fn expression(&mut self, expression: &'t Expression) {
289        match expression {
290            Expression::Path(path) => self.bind(self.slots.steps_of(path), Read::Whole),
291            Expression::Interpolated(parts) => {
292                for part in parts {
293                    self.expression(part);
294                }
295            }
296            Expression::Call(call) => self.call(call),
297            _ => {}
298        }
299    }
300
301    fn presence(&mut self, expression: &'t Expression) {
302        match expression {
303            Expression::Path(path) => self.bind(self.slots.steps_of(path), Read::Presence),
304            _ => self.expression(expression),
305        }
306    }
307
308    fn looped(&mut self, list: &'t Expression) {
309        let Some(items) = self.rows_of(list) else {
310            return;
311        };
312        self.bind(Some(items[..items.len() - 1].to_vec()), Read::Presence);
313        self.bind(Some(items), Read::Counted);
314    }
315
316    fn enter_items(&mut self, list: &'t Expression, has_index: bool) {
317        self.slots.push_items(list);
318        if has_index {
319            self.slots.push_dataless();
320        }
321    }
322
323    fn leave_items(&mut self, has_index: bool) {
324        let depth = self.slots.depth() - 1 - usize::from(has_index);
325        self.slots.truncate(depth);
326    }
327
328    /// A `Table` of `data.orders.items` shows a plain list, so it reads no page keys.
329    fn columns(&mut self, rows: &'t Expression, columns: &'t [String]) {
330        let Some(reached) = self.slots.items_of(rows) else {
331            return;
332        };
333        let of_items =
334            matches!(reached.iter().rev().nth(1), Some(Step::Field(name)) if name == ITEMS);
335        let items = rows_reached_alike(reached);
336        if !of_items {
337            self.bind_page_keys(&items[..items.len() - 1]);
338        }
339        for column in columns {
340            let mut steps = items.clone();
341            steps.push(Step::Field(column.clone()));
342            self.bind(Some(steps), Read::Whole);
343        }
344    }
345
346    fn enter_gated(&mut self, gate: &'t Gate) {
347        self.open.push(gate);
348    }
349
350    fn leave_gated(&mut self) {
351        self.open.pop();
352    }
353}
354
355impl Bound<'_> {
356    fn bind_page_keys(&mut self, listing: &[Step]) {
357        for key in LISTED_FIELDS {
358            let mut steps = listing.to_vec();
359            steps.push(Step::Field(key.to_owned()));
360            self.bind(Some(steps), Read::Whole);
361        }
362    }
363
364    fn rows_of(&self, list: &Expression) -> Option<Vec<Step>> {
365        self.slots.items_of(list).map(rows_reached_alike)
366    }
367
368    /// `data.orders.items` is the list of rows a `Table` of `data.orders` shows, so it is read
369    /// as those rows: whole, each of them is; tested, they are counted.
370    fn bind(&mut self, steps: Option<Vec<Step>>, read: Read) {
371        let Some(mut steps) = steps.map(rows_reached_alike) else {
372            return;
373        };
374        let mut read = read;
375        if matches!(steps.last(), Some(Step::Field(name)) if name == ITEMS) {
376            steps.pop();
377            steps.push(Step::Item);
378            if read == Read::Presence {
379                read = Read::Counted;
380            }
381        }
382        self.bindings.push(Binding {
383            steps,
384            read,
385            gates: self.open.clone(),
386        });
387    }
388}
389
390/// `{#each data.orders.items as order}` loops over the rows a `Table` of `data.orders` shows.
391fn rows_reached_alike(steps: Vec<Step>) -> Vec<Step> {
392    let mut alike: Vec<Step> = Vec::with_capacity(steps.len());
393    for step in steps {
394        if step == Step::Item && matches!(alike.last(), Some(Step::Field(name)) if name == ITEMS) {
395            alike.pop();
396        }
397        alike.push(step);
398    }
399    alike
400}
401
402/// A path whose value some viewer may be sent otherwise than it is, so a client's types can say
403/// so.
404#[cfg(feature = "analysis")]
405#[derive(Clone, Debug, PartialEq, Eq)]
406pub struct Withholdable {
407    pub steps: Vec<Step>,
408    /// The value itself may be sent `null`, or as a boolean where `tested`.
409    pub nullable: bool,
410    /// Sent as whether it holds anything where only a condition the viewer sees tests it.
411    pub tested: bool,
412    /// The fields of the value a read names or a `Table` shows; any other may be sent `null`.
413    pub named_fields: Vec<String>,
414}
415
416/// The data paths a gate may withhold from some viewer, for a client's types.
417#[cfg(feature = "analysis")]
418#[must_use]
419pub fn withholdable(template: &crate::Template) -> Vec<Withholdable> {
420    template.precomputed.gated_paths.withholdable()
421}
422
423#[cfg(feature = "analysis")]
424impl GatedPaths {
425    fn withholdable(&self) -> Vec<Withholdable> {
426        let mut found = Vec::new();
427        if !self.nodes.is_empty() {
428            self.collect_withholdable(ROOT, &mut Vec::new(), &mut Vec::new(), &mut found);
429        }
430        found
431    }
432
433    /// `enclosing` holds the gates of each whole read above, any of which a viewer may fail.
434    fn collect_withholdable(
435        &self,
436        index: usize,
437        steps: &mut Vec<Step>,
438        enclosing: &mut Vec<usize>,
439        found: &mut Vec<Withholdable>,
440    ) {
441        let node = &self.nodes[index];
442        let ungated = |gates: usize| self.gate_sets[gates].is_empty();
443        if node
444            .reads
445            .iter()
446            .any(|(read, gates)| *read == Read::Whole && ungated(*gates))
447        {
448            return;
449        }
450        if enclosing.is_empty() && !self.any_read_within(index, &|gates| !ungated(gates)) {
451            return;
452        }
453        let gated_here = node.reads.iter().any(|(_, gates)| !ungated(*gates));
454        let shown_below = node
455            .children
456            .iter()
457            .any(|child| self.any_read_within(*child, &ungated));
458        let nullable = (gated_here || !enclosing.is_empty()) && !shown_below;
459        found.push(Withholdable {
460            steps: steps.clone(),
461            nullable,
462            tested: nullable && self.tested(node, enclosing),
463            named_fields: self.named_fields(index),
464        });
465        let depth = enclosing.len();
466        enclosing.extend(
467            node.reads
468                .iter()
469                .filter(|(read, gates)| *read == Read::Whole && !ungated(*gates))
470                .map(|(_, gates)| *gates),
471        );
472        for child in &node.children {
473            if let Some(step) = &self.nodes[*child].step {
474                steps.push(step.clone());
475                self.collect_withholdable(*child, steps, enclosing, found);
476                steps.pop();
477            }
478        }
479        enclosing.truncate(depth);
480    }
481
482    fn any_read_within(&self, index: usize, gated: &dyn Fn(usize) -> bool) -> bool {
483        let node = &self.nodes[index];
484        node.reads.iter().any(|(_, gates)| gated(*gates))
485            || node
486                .children
487                .iter()
488                .any(|child| self.any_read_within(*child, gated))
489    }
490
491    /// Whether a test of the value can hold while a read of it or around it fails.
492    fn tested(&self, node: &Node, enclosing: &[usize]) -> bool {
493        let outside = |gates: usize, test: usize| {
494            self.gate_sets[gates]
495                .iter()
496                .any(|gate| !self.gate_sets[test].contains(gate))
497        };
498        node.reads
499            .iter()
500            .filter(|(read, _)| *read == Read::Presence)
501            .any(|(_, test)| {
502                node.reads
503                    .iter()
504                    .map(|(_, gates)| *gates)
505                    .chain(enclosing.iter().copied())
506                    .any(|gates| outside(gates, *test))
507            })
508    }
509
510    fn named_fields(&self, index: usize) -> Vec<String> {
511        let ungated = |index: usize| {
512            self.nodes[index]
513                .reads
514                .iter()
515                .any(|(_, gates)| self.gate_sets[*gates].is_empty())
516        };
517        self.nodes[index]
518            .children
519            .iter()
520            .filter_map(|child| match &self.nodes[*child].step {
521                Some(Step::Field(name)) => Some(name.clone()),
522                Some(Step::Item) => ungated(*child).then(|| ITEMS.to_owned()),
523                None => None,
524            })
525            .collect()
526    }
527}
528
529#[cfg(not(feature = "i18n"))]
530impl<'t> Bound<'t> {
531    fn call(&mut self, call: &'t crate::language::Call) {
532        for value in call.values() {
533            self.expression(value);
534        }
535    }
536}
537
538#[cfg(feature = "i18n")]
539impl<'t> Bound<'t> {
540    fn call(&mut self, call: &'t crate::translations::Call) {
541        use crate::translations::Call;
542        match call {
543            Call::Translate { arguments, .. } => {
544                for argument in arguments {
545                    self.expression(&argument.value);
546                }
547            }
548            Call::Format { value, .. } => self.expression(value),
549            Call::Label { .. } => {}
550        }
551    }
552}
553
554#[cfg(all(test, feature = "parse"))]
555mod tests {
556    use serde_json::{Value, json};
557
558    use crate::Template;
559    use crate::rendering::{Gates, Rendering};
560
561    struct Viewer {
562        permissions: &'static [&'static str],
563    }
564
565    impl Gates for Viewer {
566        fn can(&self, permission: &str) -> bool {
567            self.permissions.contains(&permission)
568        }
569
570        fn enabled(&self, flag: &str) -> bool {
571            flag == "order.split_payments"
572        }
573    }
574
575    const COST: &str = "order.order.cost.read";
576
577    fn sent(source: &str, permissions: &'static [&'static str], mut data: Value) -> Value {
578        let template = Template::parse("screen", source).unwrap();
579        let viewer = Viewer { permissions };
580        Rendering::new(&template)
581            .gates(&viewer)
582            .withheld()
583            .apply_to(&mut data);
584        data
585    }
586
587    const SCREEN: &str = "<p>{data.name}</p>\
588        {#if can(\"order.order.total.read\")}<p>{data.total} {data.name}</p>{/if}\
589        {#if can(\"order.order.notes.read\")}{#if enabled(\"order.split_payments\")}\
590        <p>{data.notes}</p>{/if}{:else}<p>{data.hint}</p>{/if}";
591
592    #[test]
593    fn a_field_bound_only_inside_parts_the_viewer_may_not_see_or_bound_nowhere_is_null() {
594        let data = || json!({ "name": "a", "total": 1, "notes": "n", "hint": "h", "free": 2 });
595        let both = &["order.order.total.read", "order.order.notes.read"];
596
597        assert_eq!(
598            sent(SCREEN, &[], data()),
599            json!({ "name": "a", "total": null, "notes": null, "hint": "h", "free": null })
600        );
601        assert_eq!(
602            sent(SCREEN, &["order.order.total.read"], data())["total"],
603            1
604        );
605        assert_eq!(sent(SCREEN, both, data()), data());
606    }
607
608    #[test]
609    fn a_field_bound_in_two_gated_parts_is_sent_where_either_holds() {
610        let source = "{#if can(\"order.order.total.read\")}{data.total}{/if}\
611            {#if can(\"order.order.*.read\")}{data.total}{/if}";
612
613        assert_eq!(
614            sent(source, &[], json!({ "total": 1 })),
615            json!({ "total": null })
616        );
617        assert_eq!(
618            sent(source, &["order.order.*.read"], json!({ "total": 1 }))["total"],
619            1
620        );
621    }
622
623    #[test]
624    fn an_action_shown_only_through_a_gate_withholds_what_it_binds() {
625        let source = "<Action post=\"/orders/{data.id}/refund\" label=\"Refund\" \
626            when={can(\"order.order.*.refund\")} />";
627
628        assert_eq!(sent(source, &[], json!({ "id": 1 })), json!({ "id": null }));
629    }
630
631    #[test]
632    fn an_action_s_posted_fields_are_sent_only_where_it_is_shown() {
633        let source = "{#each data.slots as slot}<Action post=\"/slots/book\" label=\"Book\" \
634            with={{slot: slot.id}} />{#if can(\"order.order.cost.read\")}<Action \
635            post=\"/slots/hold\" label=\"Hold\" with={{code: slot.code}} />{/if}{/each}";
636        let data = || json!({ "slots": [{ "id": 1, "code": "c" }] });
637
638        assert_eq!(
639            sent(source, &[], data()),
640            json!({ "slots": [{ "id": 1, "code": null }] })
641        );
642        assert_eq!(sent(source, &[COST], data()), data());
643    }
644
645    #[test]
646    fn a_field_bound_whole_outside_a_gate_sends_what_it_holds() {
647        let source =
648            "<p>{data.order}</p>{#if can(\"order.order.cost.read\")}{data.order.cost}{/if}";
649        let data = json!({ "order": { "cost": 2 } });
650
651        assert_eq!(sent(source, &[], data.clone()), data);
652    }
653
654    #[test]
655    fn markdown_and_head_bind_their_fields() {
656        let gated = "{#if can(\"order.order.cost.read\")}<Markdown text={data.secret} />\
657            <Head title={data.title} description={data.summary} />{/if}";
658        let shown = "<Markdown text={data.body} /><Head title={data.title} image={data.image} />\
659            {#if can(\"order.order.cost.read\")}{data.body}{data.title}{data.image}{/if}";
660        let data = json!({ "secret": 1, "title": 2, "summary": 3, "body": 4, "image": 5 });
661
662        assert_eq!(
663            sent(gated, &[], data.clone()),
664            json!({ "secret": null, "title": null, "summary": null, "body": null, "image": null })
665        );
666        assert_eq!(
667            sent(shown, &[], data),
668            json!({ "secret": null, "title": 2, "summary": null, "body": 4, "image": 5 })
669        );
670    }
671
672    #[test]
673    fn a_gated_field_of_each_item_is_withheld_by_its_full_path() {
674        let source = "{#if data.orders}{#each data.orders as order, index}<p>{order.id}</p>\
675            {#if can(\"order.order.cost.read\")}<p>{order.cost} {index}</p>{/if}{/each}{/if}";
676        let data = || json!({ "orders": [{ "id": 1, "cost": 2 }] });
677
678        assert_eq!(
679            sent(source, &[], data()),
680            json!({ "orders": [{ "id": 1, "cost": null }] })
681        );
682        assert_eq!(sent(source, &[COST], data()), data());
683    }
684
685    #[test]
686    fn a_table_column_shown_only_through_a_gate_is_withheld() {
687        let source = "{#if can(\"order.order.cost.read\")}\
688            <Table id=\"all\" of={data.orders} columns=\"id cost\" />\
689            {:else}<Table id=\"few\" of={data.orders} columns=\"id\" />{/if}";
690        let data = || json!({ "orders": { "items": [{ "id": 1, "cost": 2 }], "page": 1 } });
691
692        assert_eq!(
693            sent(source, &[], data()),
694            json!({ "orders": { "items": [{ "id": 1, "cost": null }], "page": 1 } })
695        );
696        assert_eq!(sent(source, &[COST], data()), data());
697    }
698
699    #[test]
700    fn rows_looped_over_through_items_are_the_rows_a_table_shows() {
701        let source = "<Table id=\"all\" of={data.orders} columns=\"id\" />\
702            {#if can(\"order.order.cost.read\")}\
703            {#each data.orders.items as order}{order.id}{order.cost}{/each}{/if}";
704        let data = json!({ "orders": { "items": [{ "id": 1, "cost": 2 }], "page": 1 } });
705
706        assert_eq!(
707            sent(source, &[], data),
708            json!({ "orders": { "items": [{ "id": 1, "cost": null }], "page": 1 } })
709        );
710    }
711
712    #[test]
713    fn a_record_whose_fields_are_all_withheld_keeps_its_keys() {
714        let source = "{#if data.order}<p>shown</p>{/if}\
715            {#if can(\"order.order.cost.read\")}<p>{data.order.cost}</p>{/if}";
716
717        assert_eq!(
718            sent(source, &[], json!({ "order": { "cost": 2 } })),
719            json!({ "order": { "cost": null } })
720        );
721    }
722
723    #[test]
724    fn an_item_read_whole_inside_a_gate_keeps_only_what_is_shown_outside_it() {
725        let source = "{#each data.charts as chart}<p>{chart.title}</p>\
726            {#if can(\"order.order.cost.read\")}<p>{chart}</p>{/if}{/each}";
727        let data = || json!({ "charts": [{ "title": "a", "points": [1], "owner": { "id": 2 } }] });
728
729        assert_eq!(
730            sent(source, &[], data()),
731            json!({ "charts": [{ "title": "a", "points": null, "owner": null }] })
732        );
733        assert_eq!(sent(source, &[COST], data()), data());
734    }
735
736    #[test]
737    fn the_data_read_whole_inside_a_gate_sends_nothing_else() {
738        let source = "{#if can(\"order.order.cost.read\")}<p>{data}</p>{/if}";
739        let shown = "<p>{data.name}</p>{#if can(\"order.order.cost.read\")}<p>{data}</p>{/if}";
740        let data = || json!({ "name": "a", "order": { "cost": 2 } });
741
742        assert_eq!(
743            sent(source, &[], data()),
744            json!({ "name": null, "order": null })
745        );
746        assert_eq!(
747            sent(shown, &[], data()),
748            json!({ "name": "a", "order": null })
749        );
750        assert_eq!(sent(source, &[COST], data()), data());
751    }
752
753    #[test]
754    fn a_value_only_tested_outside_a_gate_is_sent_as_whether_it_holds_anything() {
755        let source = "{#if data.discount}<p>off</p>{/if}\
756            {#if can(\"order.order.cost.read\")}<p>{data.discount}</p>{/if}";
757
758        assert_eq!(
759            sent(source, &[], json!({ "discount": 5 })),
760            json!({ "discount": true })
761        );
762        assert_eq!(
763            sent(source, &[], json!({ "discount": "" })),
764            json!({ "discount": false })
765        );
766        assert_eq!(
767            sent(source, &[], json!({ "discount": {} })),
768            json!({ "discount": false })
769        );
770        assert_eq!(
771            sent(source, &[COST], json!({ "discount": 5 }))["discount"],
772            5
773        );
774    }
775
776    #[cfg(feature = "islands")]
777    #[test]
778    fn an_island_given_an_item_inside_a_gate_is_given_only_what_is_shown_outside() {
779        let source = "{#each data.charts as chart}<p>{chart.title}</p>\
780            {#if can(\"order.order.cost.read\")}<Island name=\"order-chart\" props={chart} />\
781            {/if}{/each}";
782        let data = json!({ "charts": [{ "title": "a", "points": [1] }] });
783
784        assert_eq!(
785            sent(source, &[], data),
786            json!({ "charts": [{ "title": "a", "points": null }] })
787        );
788    }
789
790    #[test]
791    fn a_list_looped_over_outside_a_gate_keeps_its_length() {
792        let source = "<p>{data.order.id}</p>{#each data.lines as line}{line.id}{/each}\
793            {#each data.tags as tag}<i></i>{/each}{#if can(\"order.order.cost.read\")}\
794            {data.order.cost}{#each data.lines as line}{line.cost}{/each}\
795            {#each data.tags as tag}{tag}{/each}{/if}";
796        let data = json!({
797            "order": { "id": 1, "cost": 2 },
798            "lines": { "items": [{ "id": 3, "cost": 4 }], "page": 1 },
799            "tags": ["a", "b"],
800        });
801
802        let expected = json!({
803            "order": { "id": 1, "cost": null },
804            "lines": { "items": [{ "id": 3, "cost": null }], "page": null },
805            "tags": [null, null],
806        });
807        assert_eq!(sent(source, &[], data), expected);
808    }
809
810    #[cfg(feature = "analysis")]
811    #[test]
812    fn names_the_paths_a_gate_may_withhold() {
813        use crate::rendering::Withholdable;
814        use crate::slots::Step;
815
816        let source = "<p>{data.name}</p>{#if data.discount}<p>off</p>{/if}\
817            {#each data.charts as chart}<p>{chart.title}</p>\
818            {#if can(\"order.order.cost.read\")}{chart}{data.discount}{data.name}{/if}{/each}\
819            {#if can(\"order.order.cost.read\")}{#each data.tags as tag}{tag}{/each}{/if}";
820        let template = Template::parse("screen", source).unwrap();
821        let field = |name: &str| Step::Field(name.to_owned());
822        let named = |names: &[&str]| names.iter().map(|name| (*name).to_owned()).collect();
823        let path = |steps: Vec<Step>, nullable, tested, names: &[&str]| Withholdable {
824            steps,
825            nullable,
826            tested,
827            named_fields: named(names),
828        };
829
830        let expected = [
831            path(
832                Vec::new(),
833                false,
834                false,
835                &["name", "discount", "charts", "tags"],
836            ),
837            path(vec![field("discount")], true, true, &[]),
838            path(vec![field("charts")], false, false, &["items"]),
839            path(vec![field("charts"), Step::Item], false, false, &["title"]),
840            path(vec![field("tags")], true, false, &[]),
841            path(vec![field("tags"), Step::Item], true, false, &[]),
842        ];
843        assert_eq!(super::withholdable(&template), expected);
844    }
845
846    #[test]
847    fn a_record_read_whole_inside_a_gate_beside_its_looped_items_sends_only_the_rows_shown() {
848        let source = "{#if can(\"order.order.cost.read\")}<p>{data.order}</p>{/if}\
849            {#each data.order.items as line}<p>{line.id}</p>{/each}";
850        let data =
851            || json!({ "order": { "id": 1, "note": "n", "items": [{ "id": 2, "cost": 3 }] } });
852
853        assert_eq!(
854            sent(source, &[], data()),
855            json!({ "order": { "id": null, "note": null, "items": [{ "id": 2, "cost": null }] } })
856        );
857        assert_eq!(sent(source, &[COST], data()), data());
858    }
859
860    #[test]
861    fn the_data_read_whole_inside_a_gate_beside_its_looped_items_sends_only_the_rows_shown() {
862        let source = "{#if can(\"order.order.cost.read\")}<p>{data}</p>{/if}\
863            {#each data.items as line}<p>{line.id}</p>{/each}";
864        let data = json!({ "name": "a", "items": [{ "id": 2, "cost": 3 }] });
865
866        assert_eq!(
867            sent(source, &[], data),
868            json!({ "name": null, "items": [{ "id": 2, "cost": null }] })
869        );
870    }
871
872    #[test]
873    fn a_list_looped_over_only_inside_a_gate_keeps_no_length() {
874        let source = "<p>{data.order.id}</p>{#if can(\"order.order.cost.read\")}\
875            {#each data.order.lines as line}<i></i>{/each}\
876            {#each data.order.items as line}<i></i>{/each}{/if}";
877        let data = json!({ "order": { "id": 1, "lines": [1, 2], "items": [3, 4] } });
878
879        assert_eq!(
880            sent(source, &[], data),
881            json!({ "order": { "id": 1, "lines": null, "items": null } })
882        );
883    }
884
885    #[test]
886    fn a_gated_read_of_a_pages_items_is_a_read_of_the_rows_a_table_shows() {
887        let source = "<Table id=\"all\" of={data.orders} columns=\"id\" />\
888            {#if can(\"order.order.cost.read\")}<p>{data.orders.items}</p>{/if}";
889        let data = json!({ "orders": { "items": [{ "id": 1, "cost": 2 }], "page": 1 } });
890
891        assert_eq!(
892            sent(source, &[], data),
893            json!({ "orders": { "items": [{ "id": 1, "cost": null }], "page": 1 } })
894        );
895    }
896
897    #[test]
898    fn a_page_looped_over_without_a_table_keeps_no_page_keys() {
899        let source = "{#each data.lines as line}{line.id}{/each}\
900            {#if can(\"order.order.cost.read\")}{#each data.lines as line}{line.cost}{/each}{/if}";
901        let data = json!({ "lines": { "items": [{ "id": 3, "cost": 4 }], "page": 1 } });
902
903        assert_eq!(
904            sent(source, &[], data),
905            json!({ "lines": { "items": [{ "id": 3, "cost": null }], "page": null } })
906        );
907    }
908
909    #[test]
910    fn a_page_key_a_gated_part_binds_is_sent_where_the_table_is_shown() {
911        let source = "<Table id=\"all\" of={data.orders} columns=\"id\" />\
912            {#if can(\"order.order.cost.read\")}<p>{data.orders.page} {data.orders.has_next}</p>\
913            {/if}";
914        let data = json!({ "orders": { "items": [{ "id": 1 }], "page": 2, "has_next": true } });
915
916        assert_eq!(sent(source, &[], data.clone()), data);
917    }
918
919    #[test]
920    fn a_table_of_a_pages_items_sends_no_page_keys() {
921        let source = "{#if can(\"order.order.cost.read\")}\
922            <Table id=\"all\" of={data.orders} columns=\"id\" />{/if}\
923            <Table id=\"rows\" of={data.orders.items} columns=\"id\" />";
924        let data = || {
925            json!({ "orders": {
926                "items": [{ "id": 1 }], "page": 1, "controls": {}, "filter": {}, "errors": {},
927            } })
928        };
929
930        assert_eq!(
931            sent(source, &[], data()),
932            json!({ "orders": {
933                "items": [{ "id": 1 }], "page": null, "controls": null, "filter": null,
934                "errors": null,
935            } })
936        );
937        assert_eq!(sent(source, &[COST], data()), data());
938    }
939
940    #[test]
941    fn a_grandchild_record_under_a_failed_listing_read_is_withheld() {
942        let source = "{#each data.orders as order}{order.id}{/each}\
943            {#if can(\"order.order.cost.read\")}\
944            <Table id=\"all\" of={data.orders} columns=\"id owner\" />{/if}";
945        let data = || {
946            json!({ "orders": {
947                "items": [{ "id": 1, "owner": { "name": "a", "address": { "city": "c" } } }],
948                "page": 1,
949            } })
950        };
951
952        assert_eq!(
953            sent(source, &[], data()),
954            json!({ "orders": { "items": [{ "id": 1, "owner": null }], "page": null } })
955        );
956        assert_eq!(sent(source, &[COST], data()), data());
957    }
958
959    #[test]
960    fn nothing_is_withheld_where_nothing_is_gated() {
961        let template = Template::parse("screen", "<p>{data.name}</p>").unwrap();
962        let viewer = Viewer { permissions: &[] };
963
964        assert!(
965            Rendering::new(&template)
966                .gates(&viewer)
967                .withheld()
968                .is_empty()
969        );
970    }
971}