1use std::collections::HashSet;
2
3use serde_json::{Map, Value};
4use sofie_core::Shown;
5
6use crate::render::items_of;
7
8#[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#[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}