1use std::sync::Arc;
8
9use crate::adapter::WrappingDispenser;
10use crate::adapter::{ExecutionError, OpDispenser, OpResult};
11use crate::wrapper_registry::{WrapperName, WrapperRegistration, WrapperSubject};
12
13pub const NAME: WrapperName = WrapperName::new("memo");
15
16fn triggers(s: WrapperSubject) -> bool {
19 let Some(template) = s.op() else {
20 return false;
21 };
22 template
23 .params
24 .get("memo")
25 .map(|v| v.is_string() || v.is_object())
26 .unwrap_or(false)
27}
28
29fn describe_assignment(s: WrapperSubject) -> Option<String> {
30 let template = s.op()?;
31 let v = template.params.get("memo")?;
32 if let Some(s) = v.as_str() {
33 if s.is_empty() {
34 return None;
35 }
36 Some(format!("memo: \"{s}\" (before+after)"))
37 } else if let Some(obj) = v.as_object() {
38 let before = obj.get("before").and_then(|x| x.as_str());
39 let after = obj.get("after").and_then(|x| x.as_str());
40 match (before, after) {
41 (Some(b), Some(a)) => Some(format!("memo: before \"{b}\" / after \"{a}\"")),
42 (Some(b), None) => Some(format!("memo: before \"{b}\"")),
43 (None, Some(a)) => Some(format!("memo: after \"{a}\"")),
44 (None, None) => None,
45 }
46 } else {
47 None
48 }
49}
50
51inventory::submit! {
52 WrapperRegistration {
53 name: NAME,
54 owned_fields: &["memo"],
60 triggers,
61 requires_inner: &[super::traverse::NAME],
62 forbids_outer: &[],
63 mutually_exclusive_with: &[],
64 describe_assignment,
65 levels: &[crate::wrapper_registry::WrapperLevel::Op],
66 }
67}
68
69pub struct MemoDispenser {
89 inner: Arc<dyn OpDispenser>,
90 before_template: Option<String>,
91 after_template: Option<String>,
92 memo_state: Arc<arc_swap::ArcSwap<String>>,
98}
99
100impl MemoDispenser {
101 pub fn wrap(
102 inner: Arc<dyn OpDispenser>,
103 before_template: Option<String>,
104 after_template: Option<String>,
105 memo_state: Arc<arc_swap::ArcSwap<String>>,
106 ) -> Arc<dyn OpDispenser> {
107 Arc::new(Self {
108 inner,
109 before_template,
110 after_template,
111 memo_state,
112 })
113 }
114
115 fn publish(&self, template: &str, wires: &dyn crate::wires::WireSource) {
116 match crate::wires::substitute_via_wires(template, wires) {
117 Ok(rendered) => {
118 self.memo_state.store(Arc::new(rendered));
119 }
120 Err(e) => {
121 crate::diag!(
122 crate::observer::LogLevel::Debug,
123 "memo: substitution failed for '{template}': {e}"
124 );
125 }
126 }
127 }
128}
129
130impl WrappingDispenser for MemoDispenser {}
131
132impl OpDispenser for MemoDispenser {
133 fn execute<'a>(
134 &'a self,
135 cycle: u64,
136 ctx: &'a crate::fixture::ExecCtx<'a>,
137 ) -> std::pin::Pin<
138 Box<dyn std::future::Future<Output = Result<OpResult, ExecutionError>> + Send + 'a>,
139 > {
140 Box::pin(async move {
141 if let Some(t) = &self.before_template {
142 self.publish(t, ctx.wires);
143 }
144 let result = self.inner.execute(cycle, ctx).await?;
145 if let Some(t) = &self.after_template {
146 self.publish(t, ctx.wires);
147 }
148 Ok(result)
149 })
150 }
151
152 fn inner_dispenser(&self) -> Option<&dyn OpDispenser> {
153 Some(self.inner.as_ref())
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::adapter::{AdapterError, ExecutionError, OpResult, ResultBody};
161 use crate::fixture::{ExecCtx, ResolvedPulls};
162
163 #[derive(Debug)]
164 struct ResultDispBody {
165 value: serde_json::Value,
166 count: u64,
167 }
168 impl ResultBody for ResultDispBody {
169 fn to_json(&self) -> serde_json::Value {
170 self.value.clone()
171 }
172 fn as_any(&self) -> &dyn std::any::Any {
173 self
174 }
175 fn element_count(&self) -> u64 {
176 self.count
177 }
178 }
179
180 struct FakeInner {
183 body: Option<ResultDispBody>,
184 error: Option<&'static str>,
185 }
186
187 impl OpDispenser for FakeInner {
188 fn execute<'a>(
189 &'a self,
190 _cycle: u64,
191 _ctx: &'a ExecCtx<'a>,
192 ) -> std::pin::Pin<
193 Box<dyn std::future::Future<Output = Result<OpResult, ExecutionError>> + Send + 'a>,
194 > {
195 Box::pin(async move {
196 if let Some(msg) = self.error {
197 return Err(ExecutionError::Op(AdapterError {
198 error_name: "test".into(),
199 message: msg.into(),
200 retryable: false,
201 }));
202 }
203 Ok(OpResult {
204 body: self.body.as_ref().map(|b| {
205 Box::new(ResultDispBody {
206 value: b.value.clone(),
207 count: b.count,
208 }) as Box<dyn ResultBody>
209 }),
210 skipped: false,
211 })
212 })
213 }
214 }
215
216 fn empty_ctx() -> (crate::adapter::ResolvedFields, ResolvedPulls) {
217 let fields = crate::adapter::ResolvedFields::new(vec![], vec![]);
218 let pulls = ResolvedPulls::empty();
219 (fields, pulls)
220 }
221
222 #[tokio::test]
223 async fn memo_wrapper_publishes_before_and_after() {
224 let memo = Arc::new(arc_swap::ArcSwap::from_pointee(String::new()));
225 let inner = Arc::new(FakeInner {
226 body: None,
227 error: None,
228 });
229 let dispenser = MemoDispenser::wrap(
230 inner,
231 Some("before-state".into()),
232 Some("after-state".into()),
233 memo.clone(),
234 );
235 let (fields, pulls) = empty_ctx();
236 let ctx = ExecCtx::new(&fields, &pulls);
237 let _ = dispenser.execute(0, &ctx).await.expect("inner ok");
238 assert_eq!(memo.load().as_str(), "after-state");
239 }
240
241 #[tokio::test]
242 async fn memo_wrapper_only_before_when_after_unset() {
243 let memo = Arc::new(arc_swap::ArcSwap::from_pointee(String::new()));
244 let inner = Arc::new(FakeInner {
245 body: None,
246 error: None,
247 });
248 let dispenser = MemoDispenser::wrap(inner, Some("ready".into()), None, memo.clone());
249 let (fields, pulls) = empty_ctx();
250 let ctx = ExecCtx::new(&fields, &pulls);
251 let _ = dispenser.execute(0, &ctx).await.expect("inner ok");
252 assert_eq!(memo.load().as_str(), "ready");
253 }
254
255 #[tokio::test]
256 async fn memo_wrapper_does_not_run_after_on_inner_error() {
257 let memo = Arc::new(arc_swap::ArcSwap::from_pointee(String::new()));
258 let inner = Arc::new(FakeInner {
259 body: None,
260 error: Some("boom"),
261 });
262 let dispenser = MemoDispenser::wrap(
263 inner,
264 Some("attempting".into()),
265 Some("finished".into()),
266 memo.clone(),
267 );
268 let (fields, pulls) = empty_ctx();
269 let ctx = ExecCtx::new(&fields, &pulls);
270 let res = dispenser.execute(0, &ctx).await;
271 assert!(res.is_err());
272 assert_eq!(
273 memo.load().as_str(),
274 "attempting",
275 "after-template must not run on inner error"
276 );
277 }
278}