Skip to main content

bevy_react/filters/
wire.rs

1//! The `filter` style's wire format: one `{"name", "params"}` object or an
2//! ordered array of them, decoded warn-don't-abort — a malformed value warns
3//! into the decode sink and degrades the whole chain to empty, never failing
4//! the containing `Style`'s deserialization (see the [`crate::filters`]
5//! module doc).
6
7use serde::{Deserialize, Deserializer};
8use serde_json::{Map, Value};
9
10/// One filter invocation in a chain: a registry name plus its raw, untyped
11/// parameter map (empty when the wire object has no `params` or `params` is
12/// `null`).
13#[derive(Debug, Clone, Default, PartialEq)]
14pub struct FilterUse {
15    pub name: String,
16    pub params: Map<String, Value>,
17}
18
19/// An ordered filter chain. Decodes from a single `{"name", "params"}` object
20/// (a 1-element chain) or an array of them (applied in order); any malformed
21/// entry — or non-object/array garbage — warns and degrades the whole value
22/// to an empty chain.
23#[derive(Debug, Clone, Default, PartialEq)]
24pub struct FilterChain(pub Vec<FilterUse>);
25
26impl<'de> Deserialize<'de> for FilterChain {
27    fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
28        Ok(chain_from_value(Value::deserialize(d)?))
29    }
30}
31
32/// Longest decodable chain.
33/// [`ResolvedFilterPass::wire_index`](crate::filters::ResolvedFilterPass::wire_index)
34/// is a `u8`, so only entry indices `0..=u8::MAX` are addressable; a longer
35/// chain is nonsense input and degrades whole-value like any other malformed
36/// chain.
37pub const MAX_CHAIN_LEN: usize = u8::MAX as usize + 1;
38
39fn chain_from_value(value: Value) -> FilterChain {
40    let entries: Vec<Value> = match value {
41        Value::Array(entries) => entries,
42        obj @ Value::Object(_) => vec![obj],
43        other => {
44            warn_decode(
45                &other,
46                &format!("filter must be an object or array of objects, got {other}"),
47            );
48            return FilterChain::default();
49        }
50    };
51    if entries.len() > MAX_CHAIN_LEN {
52        // Don't serialize the (huge) offending array back into the sink —
53        // its length is the whole story.
54        crate::protocol::decode_warn(
55            "filterParams",
56            &format!("[array of {} entries]", entries.len()),
57            &format!(
58                "filter chain has {} entries, over the cap of {MAX_CHAIN_LEN}",
59                entries.len()
60            ),
61        );
62        return FilterChain::default();
63    }
64    let mut uses = Vec::with_capacity(entries.len());
65    for entry in entries {
66        match filter_use(entry) {
67            Ok(fu) => uses.push(fu),
68            // Whole-value degradation: one bad entry empties the chain, so a
69            // half-applied filter stack can never render.
70            Err((offending, message)) => {
71                warn_decode(&offending, &message);
72                return FilterChain::default();
73            }
74        }
75    }
76    FilterChain(uses)
77}
78
79/// Decode one `{"name", "params"}` entry, consuming it — the accept path moves
80/// `name`/`params` out instead of cloning. An error hands back the most
81/// precise offending value alongside the message, for the decode-warning sink.
82fn filter_use(value: Value) -> Result<FilterUse, (Value, String)> {
83    let mut obj = match value {
84        Value::Object(obj) => obj,
85        other => {
86            let message = format!("filter entry must be an object, got {other}");
87            return Err((other, message));
88        }
89    };
90    let name = match obj.remove("name") {
91        Some(Value::String(name)) => name,
92        Some(other) => {
93            let message = format!("filter name must be a string, got {other}");
94            return Err((other, message));
95        }
96        None => {
97            let entry = Value::Object(obj);
98            let message = format!("filter entry {entry} is missing \"name\"");
99            return Err((entry, message));
100        }
101    };
102    let params = match obj.remove("params") {
103        // Deliberate null-leniency: JS callers naturally send `params: null`
104        // for "no params" — treat it exactly like an absent key.
105        None | Some(Value::Null) => Map::new(),
106        Some(Value::Object(params)) => params,
107        Some(other) => {
108            let message = format!("filter params must be an object, got {other}");
109            return Err((other, message));
110        }
111    };
112    Ok(FilterUse { name, params })
113}
114
115fn warn_decode(value: &Value, message: &str) {
116    // `Value`'s `Display` is compact JSON — the raw offending wire value.
117    crate::protocol::decode_warn("filterParams", &value.to_string(), message);
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    /// `FilterChain`'s decode never errors — malformed input degrades.
125    fn chain(json: &str) -> FilterChain {
126        serde_json::from_str(json).expect("FilterChain decode must not error")
127    }
128
129    fn blur_use(radius: u64) -> FilterUse {
130        let mut params = Map::new();
131        params.insert("radius".into(), Value::from(radius));
132        FilterUse {
133            name: "blur".into(),
134            params,
135        }
136    }
137
138    #[test]
139    fn single_object_decodes_to_one_element_chain() {
140        assert_eq!(
141            chain(r#"{"name":"blur","params":{"radius":4}}"#),
142            FilterChain(vec![blur_use(4)]),
143        );
144    }
145
146    #[test]
147    fn array_decodes_preserving_order() {
148        assert_eq!(
149            chain(
150                r#"[
151                    {"name":"blur","params":{"radius":4}},
152                    {"name":"grayscale","params":{"amount":1}}
153                ]"#
154            ),
155            FilterChain(vec![blur_use(4), {
156                let mut params = Map::new();
157                params.insert("amount".into(), Value::from(1u64));
158                FilterUse {
159                    name: "grayscale".into(),
160                    params,
161                }
162            }]),
163        );
164    }
165
166    #[test]
167    fn missing_params_decodes_to_empty_map() {
168        #[cfg(all(feature = "devtools", debug_assertions))]
169        let _ = crate::diag::take_decode_warnings();
170        let expected = FilterChain(vec![FilterUse {
171            name: "invert".into(),
172            params: Map::new(),
173        }]);
174        assert_eq!(chain(r#"{"name":"invert"}"#), expected);
175        // Deliberate null-leniency: `params: null` is exactly an absent key.
176        assert_eq!(chain(r#"{"name":"invert","params":null}"#), expected);
177        #[cfg(all(feature = "devtools", debug_assertions))]
178        assert!(crate::diag::take_decode_warnings().is_empty());
179    }
180
181    /// Whole-value semantics: one bad entry degrades the entire chain, so a
182    /// half-applied filter stack can never render.
183    #[test]
184    fn malformed_entry_degrades_whole_value_to_empty_chain() {
185        // A non-object entry in an otherwise valid array.
186        assert_eq!(chain(r#"[{"name":"blur"},3]"#), FilterChain::default());
187        // An entry with no name.
188        assert_eq!(chain(r#"{"params":{}}"#), FilterChain::default());
189        // A non-string name.
190        assert_eq!(chain(r#"{"name":7}"#), FilterChain::default());
191        // Non-object params.
192        assert_eq!(
193            chain(r#"{"name":"blur","params":3}"#),
194            FilterChain::default()
195        );
196    }
197
198    /// A chain longer than [`MAX_CHAIN_LEN`] (`wire_index` is a `u8`) warns
199    /// and degrades whole-value, like any other malformed chain.
200    #[test]
201    fn over_long_chain_degrades_to_empty_chain() {
202        #[cfg(all(feature = "devtools", debug_assertions))]
203        let _ = crate::diag::take_decode_warnings();
204        let at_cap = format!(
205            "[{}]",
206            vec![r#"{"name":"invert"}"#; MAX_CHAIN_LEN].join(",")
207        );
208        assert_eq!(chain(&at_cap).0.len(), MAX_CHAIN_LEN);
209        let over_cap = format!(
210            "[{}]",
211            vec![r#"{"name":"invert"}"#; MAX_CHAIN_LEN + 1].join(",")
212        );
213        assert_eq!(chain(&over_cap), FilterChain::default());
214        #[cfg(all(feature = "devtools", debug_assertions))]
215        {
216            let warns = crate::diag::take_decode_warnings();
217            assert_eq!(warns.len(), 1);
218            assert_eq!(warns[0].kind, "filterParams");
219            assert!(
220                warns[0].message.contains("over the cap"),
221                "{}",
222                warns[0].message
223            );
224        }
225    }
226
227    #[test]
228    fn garbage_top_level_value_degrades_to_empty_chain() {
229        assert_eq!(chain("42"), FilterChain::default());
230        assert_eq!(chain("true"), FilterChain::default());
231        assert_eq!(chain(r#""blur""#), FilterChain::default());
232    }
233
234    /// Warn-don't-abort: a garbage `filter` value must not fail the
235    /// deserialization of a containing struct (the eventual `Style`).
236    #[test]
237    fn malformed_chain_does_not_abort_containing_struct() {
238        #[derive(Deserialize)]
239        struct Holder {
240            filter: FilterChain,
241            width: f32,
242        }
243        let h: Holder =
244            serde_json::from_str(r#"{"filter":42,"width":16.0}"#).expect("holder decodes");
245        assert_eq!(h.filter, FilterChain::default());
246        assert_eq!(h.width, 16.0);
247    }
248
249    /// Malformed values are mirrored into the devtools decode sink (the sink
250    /// is thread-local, so draining it per-test is parallel-safe).
251    #[cfg(all(feature = "devtools", debug_assertions))]
252    #[test]
253    fn malformed_values_report_decode_warnings() {
254        let _ = crate::diag::take_decode_warnings();
255        let _ = chain(r#"[{"name":"blur"},3]"#);
256        let _ = chain("true");
257        let warns = crate::diag::take_decode_warnings();
258        let brief: Vec<_> = warns.iter().map(|w| (w.kind, w.value.as_str())).collect();
259        assert_eq!(brief, vec![("filterParams", "3"), ("filterParams", "true")]);
260        assert!(warns.iter().all(|w| !w.message.is_empty()));
261    }
262
263    /// A clean decode leaves the sink empty.
264    #[cfg(all(feature = "devtools", debug_assertions))]
265    #[test]
266    fn valid_values_report_nothing() {
267        let _ = crate::diag::take_decode_warnings();
268        let _ = chain(r#"{"name":"blur"}"#);
269        assert!(crate::diag::take_decode_warnings().is_empty());
270        // An explicit empty array is a valid empty chain, not a degradation —
271        // no warning.
272        assert_eq!(chain("[]"), FilterChain::default());
273        assert!(crate::diag::take_decode_warnings().is_empty());
274    }
275}