1use anyhow::{Context, Result};
2use minicbor::encode::{self, Encoder, Write};
3
4pub struct Trigger<'a> {
6 pub source_node_id: &'a str,
7 pub source_step: u64,
8 pub edge_id: &'a str,
9}
10
11pub const ROOT_TRIGGER: Trigger<'static> = Trigger {
13 source_node_id: "__root__",
14 source_step: 0,
15 edge_id: "__root__",
16};
17
18#[allow(clippy::too_many_arguments)]
33pub fn build_envelope(
34 json_input: &serde_json::Value,
35 graph_id: &str,
36 graph_version: &str,
37 node_id: &str,
38 trace_id: &str,
39 session_id: &str,
40 step: u64,
41 trigger: &Trigger<'_>,
42) -> Result<Vec<u8>> {
43 let input_value = if let Some(inner) = json_input.get("input") {
45 inner
46 } else {
47 json_input
48 };
49
50 let mut buf = Vec::with_capacity(1024);
51 let mut enc = Encoder::new(&mut buf);
52
53 enc.map(4).context("encoding envelope map")?;
55
56 enc.str("carry_state")
58 .context("encoding 'carry_state' key")?;
59 enc.null().context("encoding carry_state null")?;
60
61 enc.str("ctx").context("encoding 'ctx' key")?;
63 enc.map(7).context("encoding ctx map")?;
64 enc.str("graph_id").context("encoding ctx.graph_id key")?;
65 enc.str(graph_id).context("encoding ctx.graph_id value")?;
66 enc.str("graph_version")
67 .context("encoding ctx.graph_version key")?;
68 enc.str(graph_version)
69 .context("encoding ctx.graph_version value")?;
70 enc.str("node_id").context("encoding ctx.node_id key")?;
71 enc.str(node_id).context("encoding ctx.node_id value")?;
72 enc.str("session_id")
73 .context("encoding ctx.session_id key")?;
74 enc.str(session_id)
75 .context("encoding ctx.session_id value")?;
76 enc.str("step").context("encoding ctx.step key")?;
77 enc.u64(step).context("encoding ctx.step value")?;
78 enc.str("trace_id").context("encoding ctx.trace_id key")?;
79 enc.str(trace_id).context("encoding ctx.trace_id value")?;
80 enc.str("trigger").context("encoding ctx.trigger key")?;
81 enc.map(3).context("encoding trigger map")?;
83 enc.str("edge_id").context("encoding trigger.edge_id key")?;
84 enc.str(trigger.edge_id)
85 .context("encoding trigger.edge_id value")?;
86 enc.str("source_node_id")
87 .context("encoding trigger.source_node_id key")?;
88 enc.str(trigger.source_node_id)
89 .context("encoding trigger.source_node_id value")?;
90 enc.str("source_step")
91 .context("encoding trigger.source_step key")?;
92 enc.u64(trigger.source_step)
93 .context("encoding trigger.source_step value")?;
94
95 enc.str("graph_refs").context("encoding 'graph_refs' key")?;
97 enc.map(0).context("encoding empty graph_refs")?;
98
99 enc.str("input").context("encoding 'input' key")?;
101 encode_json_value(&mut enc, input_value).context("encoding input value")?;
102
103 Ok(buf)
104}
105
106fn encode_json_value<W: Write>(
109 enc: &mut Encoder<W>,
110 val: &serde_json::Value,
111) -> Result<(), encode::Error<W::Error>> {
112 match val {
113 serde_json::Value::Null => {
114 enc.null()?;
115 }
116 serde_json::Value::Bool(b) => {
117 enc.bool(*b)?;
118 }
119 serde_json::Value::Number(n) => {
120 if let Some(i) = n.as_i64() {
121 enc.i64(i)?;
122 } else if let Some(u) = n.as_u64() {
123 enc.u64(u)?;
124 } else if let Some(f) = n.as_f64() {
125 enc.f64(f)?;
126 } else {
127 return Err(encode::Error::message("unsupported JSON number"));
128 }
129 }
130 serde_json::Value::String(s) => {
131 enc.str(s)?;
132 }
133 serde_json::Value::Array(arr) => {
134 enc.array(arr.len() as u64)?;
135 for item in arr {
136 encode_json_value(enc, item)?;
137 }
138 }
139 serde_json::Value::Object(map) => {
140 enc.map(map.len() as u64)?;
141 let mut keys: Vec<&String> = map.keys().collect();
142 keys.sort();
143 for k in keys {
144 enc.str(k)?;
145 encode_json_value(enc, &map[k])?;
146 }
147 }
148 }
149 Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn envelope_deterministic_regardless_of_key_order() {
158 let json_a: serde_json::Value =
159 serde_json::from_str(r#"{"alpha": 1, "beta": "two", "gamma": [3, 4]}"#).unwrap();
160 let json_b: serde_json::Value =
161 serde_json::from_str(r#"{"gamma": [3, 4], "alpha": 1, "beta": "two"}"#).unwrap();
162
163 let env_a =
164 build_envelope(&json_a, "g1", "v1", "n1", "t1", "s1", 0, &ROOT_TRIGGER).unwrap();
165 let env_b =
166 build_envelope(&json_b, "g1", "v1", "n1", "t1", "s1", 0, &ROOT_TRIGGER).unwrap();
167
168 assert_eq!(
169 env_a, env_b,
170 "envelopes must be byte-identical regardless of JSON key order"
171 );
172 }
173
174 #[test]
175 fn envelope_auto_wraps_raw_input() {
176 let raw: serde_json::Value = serde_json::from_str(r#"{"text": "hello"}"#).unwrap();
177 let wrapped: serde_json::Value =
178 serde_json::from_str(r#"{"input": {"text": "hello"}}"#).unwrap();
179
180 let env_raw = build_envelope(&raw, "g1", "v1", "n1", "t1", "s1", 0, &ROOT_TRIGGER).unwrap();
181 let env_wrapped =
182 build_envelope(&wrapped, "g1", "v1", "n1", "t1", "s1", 0, &ROOT_TRIGGER).unwrap();
183
184 assert_eq!(
185 env_raw, env_wrapped,
186 "raw and wrapped inputs must produce identical envelopes"
187 );
188 }
189}