wowlab_engine_ports/
decision_trace.rs1use std::sync::{Arc, Mutex};
4
5#[cfg(feature = "wasm")]
6use tsify::Tsify;
7
8#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
10#[cfg_attr(feature = "wasm", derive(Tsify))]
11#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
12#[serde(rename_all = "camelCase")]
13pub struct DecisionTrace {
14 pub decisions: Vec<Decision>,
15}
16
17#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
19#[cfg_attr(feature = "wasm", derive(Tsify))]
20#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
21#[serde(rename_all = "camelCase")]
22pub struct Decision {
23 pub time_ms: u32,
24 pub list_id: String,
25 pub fired_action_index: Option<usize>,
26 pub evaluations: Vec<ActionEvaluation>,
27 pub nested: Vec<Decision>,
28}
29
30#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
32#[cfg_attr(feature = "wasm", derive(Tsify))]
33#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
34#[serde(rename_all = "camelCase")]
35pub struct ActionEvaluation {
36 pub list_id: String,
37 pub action_index: usize,
38 pub status: EvaluationStatus,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub rejection_reason: Option<String>,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
45#[cfg_attr(feature = "wasm", derive(Tsify))]
46#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
47#[serde(rename_all = "snake_case")]
48#[non_exhaustive]
49pub enum EvaluationStatus {
50 Fired,
51 Executed,
52 Rejected,
53 NotReached,
54 Disabled,
55}
56
57pub trait DecisionTraceSink: std::fmt::Debug + Send + Sync {
59 fn record(&self, decision: Decision);
60}
61
62impl<T> DecisionTraceSink for Arc<T>
63where
64 T: DecisionTraceSink + ?Sized,
65{
66 fn record(&self, decision: Decision) {
67 (**self).record(decision);
68 }
69}
70
71#[derive(Clone, Debug)]
73pub struct DecisionTraceTarget(Arc<dyn DecisionTraceSink>);
74
75impl DecisionTraceTarget {
76 pub fn new(sink: impl DecisionTraceSink + 'static) -> Self {
78 Self(Arc::new(sink))
79 }
80}
81
82impl DecisionTraceSink for DecisionTraceTarget {
83 fn record(&self, decision: Decision) {
84 self.0.record(decision);
85 }
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct VecSink {
91 inner: Arc<Mutex<Vec<Decision>>>,
92}
93
94impl VecSink {
95 #[must_use]
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 #[must_use]
106 pub fn drain(&self) -> Vec<Decision> {
107 let mut guard = self
108 .inner
109 .lock()
110 .expect("decision-trace sink mutex poisoned");
111
112 std::mem::take(&mut *guard)
113 }
114
115 #[must_use]
121 pub fn snapshot(&self) -> Vec<Decision> {
122 self.inner
123 .lock()
124 .expect("decision-trace sink mutex poisoned")
125 .clone()
126 }
127}
128
129impl DecisionTraceSink for VecSink {
130 fn record(&self, decision: Decision) {
131 self.inner
132 .lock()
133 .expect("decision-trace sink mutex poisoned")
134 .push(decision);
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use googletest::prelude::*;
141
142 use super::*;
143
144 fn sample_decision() -> Decision {
145 Decision {
146 time_ms: 0,
147 list_id: "a".into(),
148 fired_action_index: None,
149 evaluations: vec![],
150 nested: vec![],
151 }
152 }
153
154 #[gtest]
155 fn vecsink_snapshot_is_nondestructive_drain_is_destructive() -> Result<()> {
156 let sink = VecSink::new();
157
158 sink.record(sample_decision());
159 sink.record(sample_decision());
160
161 verify_that!(sink.snapshot(), len(eq(2)))?;
162 verify_that!(sink.snapshot(), len(eq(2)))?;
163
164 verify_that!(sink.drain(), len(eq(2)))?;
165 verify_that!(sink.drain(), len(eq(0)))?;
166
167 verify_that!(sink.snapshot(), len(eq(0)))
168 }
169}