ifc_lite_wasm/zero_copy/
symbolic_truncation.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The truncation surface of [`SymbolicRepresentationCollection`].
6//!
7//! Also home to the reasoning behind `is_empty`'s truncation guard: a per-item
8//! bound can fire having emitted nothing, so a truncated collection can hold no
9//! primitives at all. A consumer that skips an "empty" collection would then
10//! discard the only signal the browser gets that its drawing is clipped, since
11//! geometry is client-side only. `SymbolicData::is_empty` in
12//! `ifc-lite-processing` already refuses to call a truncated result empty; the
13//! test below pins the two together rather than trusting two definitions to
14//! stay in step.
15//!
16//! Split out of `symbolic.rs` rather than raising that file's ratchet budget:
17//! the budget exists so a god-file cannot grow one accessor at a time, and
18//! "the new field needed a getter" is exactly the increment it is meant to
19//! refuse. These read one field and belong together (#2938).
20
21use super::symbolic::SymbolicRepresentationCollection;
22use wasm_bindgen::prelude::*;
23
24#[wasm_bindgen]
25impl SymbolicRepresentationCollection {
26    /// Primitive count at which extraction stopped, else `undefined`.
27    #[wasm_bindgen(getter, js_name = truncatedAt)]
28    pub fn truncated_at(&self) -> Option<usize> {
29        self.truncated.as_ref().map(|t| t.emitted)
30    }
31
32    /// Which bound stopped extraction, else `undefined`. One of
33    /// `element-count`, `output-bytes`, `item-depth`, `item-revisits`,
34    /// `item-cycle` — the same kebab-case strings the JSON path emits, so a
35    /// consumer reading either surface reads one vocabulary.
36    #[wasm_bindgen(getter, js_name = truncatedReason)]
37    pub fn truncated_reason(&self) -> Option<String> {
38        self.truncated
39            .as_ref()
40            .map(|t| t.reason.as_wire_str().to_string())
41    }
42
43    /// The bound's numeric value, when the reason has one, else `undefined`.
44    /// Absent for the per-item reasons, whose bound is per item and not
45    /// comparable with `truncatedAt`.
46    #[wasm_bindgen(getter, js_name = truncatedLimit)]
47    pub fn truncated_limit(&self) -> Option<usize> {
48        self.truncated.as_ref().and_then(|t| t.limit)
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::SymbolicRepresentationCollection;
55    use ifc_lite_processing::{
56        SymbolicData, SymbolicTruncation, SymbolicTruncationReason,
57    };
58
59    fn truncated_but_empty() -> SymbolicData {
60        // A per-item bound can fire having emitted nothing at all: the walk
61        // refuses one representation item and the file contributes no
62        // primitives. That is the shape this guard exists for.
63        SymbolicData {
64            truncated: Some(SymbolicTruncation {
65                reason: SymbolicTruncationReason::ItemRevisits,
66                emitted: 0,
67                limit: None,
68            }),
69            ..SymbolicData::default()
70        }
71    }
72
73    /// `is_empty` decides whether a consumer keeps the collection at all, so
74    /// the two implementations disagreeing means the browser drops a
75    /// diagnostic the server keeps. Assert PARITY, not each side separately:
76    /// two independently-correct-looking definitions is exactly how they
77    /// drifted in the first place.
78    #[test]
79    fn is_empty_agrees_with_the_processing_side_for_a_truncated_but_empty_result() {
80        let data = truncated_but_empty();
81        let expected = data.is_empty();
82        let collection = SymbolicRepresentationCollection::from_data(data);
83
84        assert!(
85            !expected,
86            "SymbolicData::is_empty must not call a truncated result empty"
87        );
88        assert_eq!(
89            collection.is_empty(),
90            expected,
91            "the WASM collection must agree with SymbolicData::is_empty, or a \
92             consumer that skips an empty collection silently discards the \
93             truncation diagnostic (geometry is client-side only, so this is \
94             the browser's only signal the drawing is clipped)"
95        );
96        // The diagnostic the guard protects is actually reachable.
97        assert_eq!(collection.truncated_reason().as_deref(), Some("item-revisits"));
98    }
99
100    #[test]
101    fn a_genuinely_complete_and_empty_result_is_still_empty() {
102        // The other direction, so "always false" is not a passing fix.
103        let data = SymbolicData::default();
104        assert!(data.is_empty());
105        let collection = SymbolicRepresentationCollection::from_data(data);
106        assert!(collection.is_empty());
107        assert_eq!(collection.truncated_reason(), None);
108    }
109}