lash_core/runtime/process/
materialization.rs1use crate::plugin::PluginError;
2
3use super::events::{
4 AbandonEvidence, AbandonWriter, ProcessAwaitOutput, ProcessEventSemantics,
5 ProcessEventSemanticsSpec, ProcessTerminalSemantics, ProcessTerminalSpec, ProcessTerminalState,
6 ProcessValueSelector, ProcessWake, ProcessWakeDedupeKey, ProcessWakeSpec,
7};
8
9pub fn materialize_process_event_semantics(
10 process_id: &str,
11 sequence: u64,
12 payload: &serde_json::Value,
13 spec: &ProcessEventSemanticsSpec,
14) -> Result<ProcessEventSemantics, PluginError> {
15 materialize_event_semantics(process_id, sequence, payload, spec)
16}
17
18pub(super) fn materialize_event_semantics(
19 process_id: &str,
20 sequence: u64,
21 payload: &serde_json::Value,
22 spec: &ProcessEventSemanticsSpec,
23) -> Result<ProcessEventSemantics, PluginError> {
24 let terminal = spec
25 .terminal
26 .as_ref()
27 .map(|terminal| materialize_terminal_semantics(payload, terminal))
28 .transpose()?;
29 let wake = spec
30 .wake
31 .as_ref()
32 .map(|wake| materialize_wake(process_id, sequence, payload, wake))
33 .transpose()?
34 .flatten();
35 Ok(ProcessEventSemantics { terminal, wake })
36}
37
38fn materialize_terminal_semantics(
39 payload: &serde_json::Value,
40 terminal: &ProcessTerminalSpec,
41) -> Result<ProcessTerminalSemantics, PluginError> {
42 let await_output = match &terminal.await_output {
43 Some(selector) => {
44 let selected = select_value(payload, selector)?;
45 serde_json::from_value::<ProcessAwaitOutput>(selected.clone())
46 .unwrap_or_else(|_| selected_value_to_await_output(terminal.state, selected))
47 }
48 None if terminal.state == ProcessTerminalState::Completed => ProcessAwaitOutput::Success {
49 value: payload.clone(),
50 control: None,
51 },
52 None => {
53 return Err(PluginError::Session(
54 "failed or cancelled terminal events must declare await output".to_string(),
55 ));
56 }
57 };
58 Ok(ProcessTerminalSemantics {
59 state: terminal.state,
60 await_output,
61 })
62}
63
64fn selected_value_to_await_output(
65 state: ProcessTerminalState,
66 value: serde_json::Value,
67) -> ProcessAwaitOutput {
68 match state {
69 ProcessTerminalState::Completed => ProcessAwaitOutput::Success {
70 value,
71 control: None,
72 },
73 ProcessTerminalState::Failed => ProcessAwaitOutput::Failure {
74 class: crate::ToolFailureClass::Execution,
75 code: "process_failed".to_string(),
76 message: selector_value_to_string(&value),
77 raw: Some(value),
78 control: None,
79 },
80 ProcessTerminalState::Cancelled => ProcessAwaitOutput::Cancelled {
81 message: selector_value_to_string(&value),
82 raw: Some(value),
83 control: None,
84 },
85 ProcessTerminalState::Abandoned => ProcessAwaitOutput::Abandoned {
91 evidence: Box::new(AbandonEvidence {
92 writer: AbandonWriter::OwnerDrain,
93 owner: None,
94 epoch_ms: 0,
95 }),
96 control: None,
97 },
98 }
99}
100
101fn materialize_wake(
102 process_id: &str,
103 sequence: u64,
104 payload: &serde_json::Value,
105 wake: &ProcessWakeSpec,
106) -> Result<Option<ProcessWake>, PluginError> {
107 if let Some(when) = &wake.when {
108 let selected = select_value(payload, when)?;
109 if !selector_value_is_truthy(&selected) {
110 return Ok(None);
111 }
112 }
113 let input = selector_value_to_string(&select_value(payload, &wake.input)?);
114 let dedupe_key = match &wake.dedupe_key {
115 ProcessWakeDedupeKey::EventIdentity => format!("{process_id}:{sequence}"),
116 ProcessWakeDedupeKey::Selector(selector) => {
117 selector_value_to_string(&select_value(payload, selector)?)
118 }
119 ProcessWakeDedupeKey::Const(value) => value.clone(),
120 };
121 Ok(Some(ProcessWake { input, dedupe_key }))
122}
123
124pub(super) fn select_value(
125 payload: &serde_json::Value,
126 selector: &ProcessValueSelector,
127) -> Result<serde_json::Value, PluginError> {
128 match selector {
129 ProcessValueSelector::Payload => Ok(payload.clone()),
130 ProcessValueSelector::Pointer(pointer) => {
131 payload.pointer(pointer).cloned().ok_or_else(|| {
132 PluginError::Session(format!("payload pointer `{pointer}` did not match"))
133 })
134 }
135 ProcessValueSelector::Const(value) => Ok(value.clone()),
136 ProcessValueSelector::Template { template, fields } => {
137 let mut rendered = template.clone();
138 for (name, selector) in fields {
139 let value = select_value(payload, selector)?;
140 rendered =
141 rendered.replace(&format!("{{{name}}}"), &selector_value_to_string(&value));
142 }
143 Ok(serde_json::Value::String(rendered))
144 }
145 ProcessValueSelector::Present(pointer) => {
146 Ok(serde_json::Value::Bool(payload.pointer(pointer).is_some()))
147 }
148 }
149}
150
151fn selector_value_to_string(value: &serde_json::Value) -> String {
152 value
153 .as_str()
154 .map(ToOwned::to_owned)
155 .unwrap_or_else(|| value.to_string())
156}
157
158fn selector_value_is_truthy(value: &serde_json::Value) -> bool {
159 match value {
160 serde_json::Value::Null => false,
161 serde_json::Value::Bool(value) => *value,
162 serde_json::Value::String(value) => !value.is_empty(),
163 serde_json::Value::Array(value) => !value.is_empty(),
164 serde_json::Value::Object(value) => !value.is_empty(),
165 serde_json::Value::Number(_) => true,
166 }
167}