1use 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#[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 step: Option<Step>,
29 children: Vec<usize>,
30 reads: Vec<(Read, usize)>,
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35enum Read {
36 Whole = 1,
38 Presence = 2,
39 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#[derive(Clone, Copy, Debug, Default)]
62struct Seen {
63 shown: Reads,
64 hidden: Reads,
65 shown_below: bool,
66 hidden_below: bool,
67}
68
69#[derive(Debug)]
71pub struct Withheld<'t> {
72 paths: &'t GatedPaths,
73 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 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 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 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 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 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 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
390fn 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#[cfg(feature = "analysis")]
405#[derive(Clone, Debug, PartialEq, Eq)]
406pub struct Withholdable {
407 pub steps: Vec<Step>,
408 pub nullable: bool,
410 pub tested: bool,
412 pub named_fields: Vec<String>,
414}
415
416#[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 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 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}