Skip to main content

sofie_engine/
client_instants.rs

1use std::collections::HashSet;
2
3use serde_json::{Map, Value};
4use sofie_core::Shown;
5
6use crate::render::items_of;
7
8/// Where the data a client area is sent holds instants, which its JSON writes as text: the sent
9/// data's shape with each instant `true`, a list standing for all its items, `null` where none
10/// is. Only an instant sent as text is marked, so a withheld one's name is not sent either.
11#[must_use]
12pub fn instant_fields(data: Shown<'_>, sent: &Value) -> Value {
13    shape(data, sent).unwrap_or(Value::Null)
14}
15
16fn shape(shown: Shown<'_>, sent: &Value) -> Option<Value> {
17    match (shown, sent) {
18        (Shown::Instant(_), Value::String(_)) => Some(Value::Bool(true)),
19        (Shown::List(_), Value::Array(sent_items)) => list_shape(shown, sent_items),
20        (Shown::Record(fields), Value::Object(sent_fields)) => {
21            let found: Map<String, Value> = sent_fields
22                .iter()
23                .filter_map(|(name, sent_field)| {
24                    Some((name.clone(), shape(fields.get(name)?, sent_field)?))
25                })
26                .collect();
27            (!found.is_empty()).then_some(Value::Object(found))
28        }
29        _ => None,
30    }
31}
32
33fn list_shape(list: Shown<'_>, sent_items: &[Value]) -> Option<Value> {
34    let mut found: Option<Value> = None;
35    let mut settled = Settled::default();
36    for (item, sent_item) in items_of(Some(list)).zip(sent_items) {
37        if let Some(item_shape) = shape(item, sent_item) {
38            found = Some(match found {
39                Some(shape) => merged(shape, item_shape),
40                None => item_shape,
41            });
42        }
43        if settled.all_after(item) {
44            break;
45        }
46    }
47    found
48}
49
50/// A list's items share a type, so once each of their fields has held a value that is neither
51/// empty, a record nor a list, later items mark nothing new.
52#[derive(Default)]
53struct Settled<'a> {
54    fields: HashSet<&'a str>,
55}
56
57impl<'a> Settled<'a> {
58    fn all_after(&mut self, item: Shown<'a>) -> bool {
59        let Shown::Record(fields) = item else {
60            return settles(item);
61        };
62        let mut all = true;
63        for name in fields.names() {
64            if self.fields.contains(name) {
65                continue;
66            }
67            if fields.get(name).is_some_and(settles) {
68                self.fields.insert(name);
69            } else {
70                all = false;
71            }
72        }
73        all
74    }
75}
76
77fn settles(shown: Shown<'_>) -> bool {
78    !matches!(shown, Shown::Null | Shown::Record(_) | Shown::List(_))
79}
80
81fn merged(mut shape: Value, other: Value) -> Value {
82    let (Value::Object(fields), Value::Object(other_fields)) = (&mut shape, other) else {
83        return shape;
84    };
85    for (name, other_shape) in other_fields {
86        let joined = match fields.remove(&name) {
87            Some(own) => merged(own, other_shape),
88            None => other_shape,
89        };
90        fields.insert(name, joined);
91    }
92    shape
93}
94
95#[cfg(test)]
96mod tests {
97    use std::cell::Cell;
98
99    use serde_json::json;
100    use sofie_core::show::{Fields, Items};
101    use sofie_core::{Instant, Show, Shown};
102
103    use super::instant_fields;
104
105    struct Row {
106        at: Option<Instant>,
107        note: &'static str,
108    }
109
110    impl Fields for Row {
111        fn name(&self, index: usize) -> Option<&str> {
112            ["at", "note"].get(index).copied()
113        }
114
115        fn get(&self, name: &str) -> Option<Shown<'_>> {
116            match name {
117                "at" => Some(self.at.show()),
118                "note" => Some(Shown::Text(self.note)),
119                _ => None,
120            }
121        }
122    }
123
124    impl Show for Row {
125        fn show(&self) -> Shown<'_> {
126            Shown::Record(self)
127        }
128    }
129
130    #[test]
131    fn marks_the_sent_fields_any_item_holds_an_instant_in_and_never_text() {
132        let at = Instant::parse("2026-10-11T22:30:00Z").unwrap();
133        let rows = vec![
134            Row {
135                at: None,
136                note: "2026-10-11T22:30:00Z",
137            },
138            Row {
139                at: Some(at),
140                note: "",
141            },
142        ];
143
144        let sent = serde_json::to_value(
145            rows.iter()
146                .map(|row| json!({"at": row.at, "note": row.note}))
147                .collect::<Vec<_>>(),
148        )
149        .unwrap();
150
151        assert_eq!(instant_fields(rows.show(), &sent), json!({"at": true}));
152        assert_eq!(instant_fields(rows.show(), &json!([{}, {}])), json!(null));
153        let text = json!({"at": "2026-10-11T22:30:00Z"});
154        assert_eq!(instant_fields(text.show(), &text), json!(null));
155    }
156
157    struct Watched {
158        rows: Vec<Row>,
159        asked: Cell<usize>,
160    }
161
162    impl Items for Watched {
163        fn item(&self, index: usize) -> Option<Shown<'_>> {
164            self.asked.set(self.asked.get().max(index + 1));
165            self.rows.get(index).map(Show::show)
166        }
167    }
168
169    #[test]
170    fn stops_once_every_field_of_the_items_has_shown_what_it_holds() {
171        let at = Instant::parse("2026-10-11T22:30:00Z").unwrap();
172        let row = |at| Row { at, note: "n" };
173        let watched = Watched {
174            rows: vec![row(None), row(Some(at)), row(Some(at)), row(None)],
175            asked: Cell::new(0),
176        };
177        let sent = json!([{ "at": null }, { "at": "t" }, { "at": "t" }, { "at": null }]);
178
179        assert_eq!(
180            instant_fields(Shown::List(&watched), &sent),
181            json!({ "at": true })
182        );
183        assert_eq!(watched.asked.get(), 2);
184    }
185
186    struct Owned {
187        at: Option<Instant>,
188        owner: Row,
189    }
190
191    impl Fields for Owned {
192        fn name(&self, index: usize) -> Option<&str> {
193            ["at", "owner"].get(index).copied()
194        }
195
196        fn get(&self, name: &str) -> Option<Shown<'_>> {
197            match name {
198                "at" => Some(self.at.show()),
199                "owner" => Some(self.owner.show()),
200                _ => None,
201            }
202        }
203    }
204
205    impl Show for Owned {
206        fn show(&self) -> Shown<'_> {
207            Shown::Record(self)
208        }
209    }
210
211    #[test]
212    fn walks_on_while_an_item_holds_a_nested_record() {
213        let at = Instant::parse("2026-10-11T22:30:00Z").unwrap();
214        let owned = |owner_at| Owned {
215            at: Some(at),
216            owner: Row {
217                at: owner_at,
218                note: "n",
219            },
220        };
221        let rows = vec![owned(None), owned(None), owned(Some(at))];
222        let sent = json!([
223            { "at": "t", "owner": { "at": null } },
224            { "at": "t", "owner": { "at": null } },
225            { "at": "t", "owner": { "at": "t" } },
226        ]);
227
228        assert_eq!(
229            instant_fields(rows.show(), &sent),
230            json!({ "at": true, "owner": { "at": true } })
231        );
232    }
233}