1use super::engine::slots;
12use crate::base::{Change, Error, ErrorKind, ErrorRecord, Result};
13use crate::expr::{Path, Root};
14use crate::state::RunContext;
15use datavalue::OwnedDataValue;
16use std::borrow::Cow;
17
18#[derive(Clone, Debug, PartialEq, Eq)]
48pub struct ErrorContext {
49 path: Path,
50 limit: usize,
51}
52
53impl ErrorContext {
54 pub const DEFAULT_LIMIT: usize = 32;
56
57 pub fn new(path: Path, limit: usize) -> Result<Self> {
68 if path.is_root() {
69 return Err(Error::new(
70 ErrorKind::Config,
71 format!(
72 "error context path must name a slot inside `{}`, got `{path}`",
73 path.root().as_str()
74 ),
75 ));
76 }
77 if path.root() == Root::Metadata
78 && let Some(slot) = path.segs().get(1)
79 && slots::ALL.contains(&&*slot.key)
80 {
81 return Err(Error::new(
82 ErrorKind::Config,
83 format!(
84 "error context path may not be at or below `metadata.{}` — the engine owns \
85 that slot",
86 slot.key
87 ),
88 ));
89 }
90 if limit == 0 {
91 return Err(Error::new(
92 ErrorKind::Config,
93 "error context limit must be at least 1",
94 ));
95 }
96 Ok(Self { path, limit })
97 }
98
99 pub fn path(&self) -> &Path {
101 &self.path
102 }
103
104 pub fn limit(&self) -> usize {
106 self.limit
107 }
108
109 pub(crate) fn reset(&self, context: &mut OwnedDataValue) {
113 let Some((last, parents)) = self.path.segs().split_last() else {
114 return;
115 };
116 let mut node = context;
117 for seg in parents {
118 node = match node {
119 OwnedDataValue::Object(pairs) => {
120 match pairs.iter_mut().find(|(k, _)| **k == *seg.key) {
121 Some((_, child)) => child,
122 None => return,
123 }
124 }
125 _ => return,
126 };
127 }
128 if let OwnedDataValue::Object(pairs) = node
129 && let Some(at) = pairs.iter().position(|(k, _)| **k == *last.key)
130 {
131 pairs.remove(at);
132 }
133 }
134
135 pub(crate) fn mirror(
141 &self,
142 rc: &mut RunContext<'_, '_>,
143 records: &[ErrorRecord],
144 workflow: &str,
145 task: &str,
146 status: u16,
147 traced: bool,
148 ) -> Option<Change> {
149 if records.is_empty() {
150 return None;
151 }
152 let fresh = || {
153 records
154 .iter()
155 .map(|r| record(workflow, task, &r.code, status))
156 };
157 let segs = self.path.segs();
158 if segs[1..].iter().all(|seg| seg.index.is_none()) {
164 let old = rc.get(&self.path).map(Cow::into_owned);
165 let (value, change) = self.extend(&self.path, 0, old, fresh(), traced);
166 if rc.set(&self.path, value, None) {
167 return change;
168 }
169 }
170 let (at, old) = self.walk(rc);
171 let below = segs.len() - at.segs().len();
172 let (value, change) = self.extend(&at, below, old, fresh(), traced);
173 rc.set(&at, value, None);
174 change
175 }
176
177 fn walk(&self, rc: &RunContext<'_, '_>) -> (Cow<'_, Path>, Option<OwnedDataValue>) {
182 let segs = self.path.segs();
183 for len in 1..segs.len() {
184 if !rc.is_object_at(&segs[..len]) {
185 let hop = self.path.prefix(len);
186 let found = rc.get(&hop).map(Cow::into_owned);
187 return (Cow::Owned(hop), found);
188 }
189 }
190 let old = rc.get(&self.path).map(Cow::into_owned);
191 (Cow::Borrowed(&self.path), old)
192 }
193
194 fn extend(
199 &self,
200 at: &Path,
201 below: usize,
202 old: Option<OwnedDataValue>,
203 fresh: impl Iterator<Item = OwnedDataValue>,
204 traced: bool,
205 ) -> (OwnedDataValue, Option<Change>) {
206 let old_value = traced.then(|| old.clone().unwrap_or(OwnedDataValue::Null));
207 let mut items = match old {
208 Some(OwnedDataValue::Array(items)) if below == 0 => items,
209 Some(_) if below == 0 => {
210 ::log::warn!(
211 "error context path `{}` held a non-array — replacing it",
212 self.path
213 );
214 Vec::new()
215 }
216 Some(_) => {
217 ::log::warn!(
218 "error context path `{}` runs through a non-object at `{at}` — replacing it",
219 self.path
220 );
221 Vec::new()
222 }
223 None => Vec::new(),
224 };
225 items.extend(fresh);
226 if items.len() > self.limit {
229 items.drain(..items.len() - self.limit);
230 }
231 let segs = self.path.segs();
232 let value = segs[segs.len() - below..]
233 .iter()
234 .rev()
235 .fold(OwnedDataValue::Array(items), |inner, seg| {
236 OwnedDataValue::Object(vec![(String::from(&*seg.key), inner)])
237 });
238 let change = old_value.map(|old| Change::assign(at, old, value.clone()));
240 (value, change)
241 }
242}
243
244fn record(workflow: &str, task: &str, code: &str, status: u16) -> OwnedDataValue {
246 OwnedDataValue::Object(vec![
247 (
248 "workflow_id".to_owned(),
249 OwnedDataValue::String(workflow.to_owned()),
250 ),
251 (
252 "task_id".to_owned(),
253 OwnedDataValue::String(task.to_owned()),
254 ),
255 ("code".to_owned(), OwnedDataValue::String(code.to_owned())),
256 ("status".to_owned(), OwnedDataValue::from(status)),
257 ])
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263 use crate::state::Tuning;
264
265 fn v3_append(context: &mut OwnedDataValue, path: &str, limit: usize, new: &[OwnedDataValue]) {
268 fn object(node: &mut OwnedDataValue) -> &mut Vec<(String, OwnedDataValue)> {
269 if !matches!(node, OwnedDataValue::Object(_)) {
270 *node = OwnedDataValue::Object(Vec::new());
271 }
272 match node {
273 OwnedDataValue::Object(fields) => fields,
274 _ => unreachable!(),
275 }
276 }
277 fn child<'v>(node: &'v mut OwnedDataValue, key: &str) -> &'v mut OwnedDataValue {
278 let fields = object(node);
279 let at = match fields.iter().position(|(k, _)| k == key) {
280 Some(at) => at,
281 None => {
282 fields.push((key.to_owned(), OwnedDataValue::Null));
283 fields.len() - 1
284 }
285 };
286 &mut fields[at].1
287 }
288 let mut node = context;
289 for raw in path.split('.') {
290 node = child(node, raw.strip_prefix('#').unwrap_or(raw));
291 }
292 if !matches!(node, OwnedDataValue::Array(_)) {
293 *node = OwnedDataValue::Array(Vec::new());
294 }
295 let OwnedDataValue::Array(items) = node else {
296 unreachable!()
297 };
298 items.extend(new.iter().cloned());
299 if items.len() > limit {
300 items.drain(..items.len() - limit);
301 }
302 }
303
304 fn parse(text: &str) -> OwnedDataValue {
305 serde_json::from_str(text).unwrap()
306 }
307
308 fn text(v: &OwnedDataValue) -> String {
309 serde_json::to_string(v).unwrap()
310 }
311
312 #[test]
313 fn with_and_without_a_view_the_mirror_leaves_what_v3_leaves() {
314 let seeds = [
315 r#"{"data":{},"metadata":{},"temp_data":{}}"#,
316 r#"{"data":{"items":[1,2],"n":5,"z":null,"o":{"k":1}},"metadata":{"errors":"s"},"temp_data":{}}"#,
317 r#"{"data":[1],"metadata":{"errors":[{"old":1}]},"temp_data":{}}"#,
318 ];
319 let paths = [
320 "metadata.errors",
321 "data.a.b.c",
322 "data.#x",
323 "data.items.0",
324 "data.items.k",
325 "data.items.0.deep",
326 "data.n.x",
327 "data.z.x",
328 "data.o.list",
329 "temp_data.e",
330 ];
331 let calls: [&[&str]; 3] = [&["A"], &["B", "C"], &["D"]];
332 for seed in seeds {
333 for path in paths {
334 let ec = ErrorContext::new(path.parse().unwrap(), 3).unwrap();
335 let mut expected = parse(seed);
336 for codes in calls {
337 let new: Vec<_> = codes.iter().map(|c| record("w", "t", c, 500)).collect();
338 v3_append(&mut expected, path, 3, &new);
339 }
340 for viewed in [false, true] {
341 let mut ctx = parse(seed);
342 let mut rc = RunContext::new(&mut ctx, Tuning::default());
343 for codes in calls {
344 if viewed {
345 rc.read(|_, _| ());
346 }
347 let records: Vec<_> =
348 codes.iter().map(|c| ErrorRecord::new(*c, "m")).collect();
349 ec.mirror(&mut rc, &records, "w", "t", 500, false);
350 }
351 assert_eq!(rc.has_view(), viewed);
352 rc.finish();
353 assert_eq!(
354 text(&ctx),
355 text(&expected),
356 "{path} over {seed}, viewed {viewed}"
357 );
358 }
359 }
360 }
361 }
362
363 #[test]
364 fn a_write_at_a_root_says_it_replaced_it() {
365 for (path, seed, at, replaced) in [
369 (
370 "data.errors",
371 r#"{"data":[1],"metadata":{},"temp_data":{}}"#,
372 "data",
373 true,
374 ),
375 (
376 "data.n.list",
377 r#"{"data":{"n":5},"metadata":{},"temp_data":{}}"#,
378 "data.n",
379 false,
380 ),
381 ] {
382 let ec = ErrorContext::new(path.parse().unwrap(), 3).unwrap();
383 let mut ctx = parse(seed);
384 let mut rc = RunContext::new(&mut ctx, Tuning::default());
385 let records = [ErrorRecord::new("A", "m")];
386 let change = ec
387 .mirror(&mut rc, &records, "w", "t", 500, true)
388 .expect("a traced mirror records its change");
389 rc.finish();
390 assert_eq!((&*change.path, change.replaced), (at, replaced), "{path}");
391 let written: Path = at.parse().unwrap();
392 assert_eq!(
393 text(written.get(&ctx).unwrap()),
394 text(&change.new_value),
395 "{path}"
396 );
397 }
398 }
399
400 #[test]
401 fn the_reset_removes_only_what_the_write_would_find() {
402 let seed = r#"{"data":{"items":[1,2],"o":{"k":1,"j":2}},"metadata":{"errors":[1]},"temp_data":{}}"#;
403 for (path, expected) in [
404 (
405 "metadata.errors",
406 r#"{"data":{"items":[1,2],"o":{"k":1,"j":2}},"metadata":{},"temp_data":{}}"#,
407 ),
408 (
409 "data.o.k",
410 r#"{"data":{"items":[1,2],"o":{"j":2}},"metadata":{"errors":[1]},"temp_data":{}}"#,
411 ),
412 ("data.items.0", seed),
414 ("data.missing.x", seed),
415 ] {
416 let mut ctx = parse(seed);
417 ErrorContext::new(path.parse().unwrap(), 1)
418 .unwrap()
419 .reset(&mut ctx);
420 assert_eq!(text(&ctx), expected, "{path}");
421 }
422 }
423}