Skip to main content

app_kit/workflow/
serialization.rs

1use std::collections::BTreeMap;
2use std::fmt::Display;
3use std::sync::{Arc, OnceLock, RwLock};
4
5use serde_json::{Map, Value};
6
7use crate::AppError;
8use crate::workflow::definition::JsonBoundary;
9use crate::workflow::{StateSerialization, WorkflowState};
10
11#[derive(Clone, Default)]
12pub(super) struct SerializationRegistry {
13    fields: BTreeMap<String, FieldCodec>,
14    tags: BTreeMap<String, Arc<dyn ValueCodec>>,
15}
16
17#[derive(Clone)]
18struct FieldCodec {
19    serialize: Arc<dyn ValueCodec>,
20    deserialize: Arc<dyn ValueCodec>,
21}
22
23trait ValueCodec: Send + Sync {
24    fn transform(&self, value: &Value) -> Result<Value, AppError>;
25}
26
27impl<F> ValueCodec for F
28where
29    F: Fn(&Value) -> Result<Value, AppError> + Send + Sync,
30{
31    fn transform(&self, value: &Value) -> Result<Value, AppError> {
32        self(value)
33    }
34}
35
36impl StateSerialization {
37    /// Returns the one process-global registry, shared across all state owners.
38    ///
39    /// ```
40    /// use app_kit::workflow::{StateSerialization, WorkflowState};
41    /// assert!(std::ptr::eq(StateSerialization::global(), StateSerialization::global()));
42    /// StateSerialization::global().register_tag("global_counter".into(), |value| {
43    ///     value.get("count").cloned().ok_or("missing count")
44    /// })?;
45    /// let encoded = serde_json::from_str(r#"{"count":{"__burr_serde__":"global_counter","count":3}}"#)?;
46    /// assert_eq!(WorkflowState::deserialize(encoded)?.values()["count"], 3);
47    /// # Ok::<(), Box<dyn std::error::Error>>(())
48    /// ```
49    pub fn global() -> &'static Self {
50        static REGISTRY: OnceLock<StateSerialization> = OnceLock::new();
51        REGISTRY.get_or_init(|| Self {
52            registry: RwLock::new(Arc::new(SerializationRegistry::default())),
53        })
54    }
55
56    /// Registers or replaces both transformations for one exact top-level field.
57    ///
58    /// Encoders must return objects; decoders may return any valid JSON value.
59    /// Callback errors retain their Display text plus field and direction context.
60    /// Neither callback invocation nor destruction of replaced captures holds
61    /// the registry lock. An in-flight operation keeps its original callbacks.
62    ///
63    /// ```
64    /// use app_kit::workflow::{StateSerialization, WorkflowState};
65    /// use serde_json::{json, Map, Value};
66    /// StateSerialization::global().register_field(
67    /// "distance_meters".into(),
68    /// |value| Ok::<_, String>(json!({"meters": value})),
69    /// |value| value.get("meters").cloned().ok_or("missing meters"),
70    /// )?;
71    /// let state = WorkflowState::new(Map::from_iter([
72    /// ("distance_meters".into(), Value::from(12)),
73    /// ]))?;
74    /// let encoded = state.serialize()?;
75    /// assert_eq!(encoded["distance_meters"], json!({"meters": 12}));
76    /// assert_eq!(WorkflowState::deserialize(encoded)?, state);
77    /// # Ok::<(), app_kit::AppError>(())
78    /// ```
79    ///
80    /// # Errors
81    ///
82    /// Returns `Integration` if the registry lock is poisoned. Callback results are
83    /// validated when encoding/decoding, not during registration; exact field names
84    /// replace prior registrations for future operations.
85    pub fn register_field<S, D, SE, DE>(
86        &self,
87        field: String,
88        serialize: S,
89        deserialize: D,
90    ) -> Result<(), AppError>
91    where
92        S: Fn(&Value) -> Result<Value, SE> + Send + Sync + 'static,
93        D: Fn(&Value) -> Result<Value, DE> + Send + Sync + 'static,
94        SE: Display,
95        DE: Display,
96    {
97        let encoding_field = field.clone();
98        let decoding_field = field.clone();
99        let codec = FieldCodec {
100            serialize: Arc::new(move |value: &Value| {
101                serialize(value).map_err(|error| {
102                    AppError::InvalidInput(format!(
103                        "field serialize handler for '{encoding_field}' failed: {error}"
104                    ))
105                })
106            }),
107            deserialize: Arc::new(move |value: &Value| {
108                deserialize(value).map_err(|error| {
109                    AppError::InvalidInput(format!(
110                        "field deserialize handler for '{decoding_field}' failed: {error}"
111                    ))
112                })
113            }),
114        };
115        let replaced = {
116            let mut registry = self.registry.write().map_err(|_| {
117                AppError::Integration("state serialization registry lock was poisoned".to_owned())
118            })?;
119            Arc::make_mut(&mut registry).fields.insert(field, codec)
120        };
121        drop(replaced);
122        Ok(())
123    }
124
125    /// Registers or replaces a decoder for an exact `__burr_serde__` tag string.
126    ///
127    /// Empty strings are valid names. The decoder receives the complete original
128    /// tagged object. Its output is terminal: it is not dispatched or recursively
129    /// decoded again. Ordinary untagged containers are decoded recursively.
130    ///
131    /// ```
132    /// use app_kit::workflow::{StateSerialization, WorkflowState};
133    /// use serde_json::{json, Map};
134    /// StateSerialization::global().register_tag("distance".into(), |value| {
135    /// value.get("meters").cloned().ok_or("missing meters")
136    /// })?;
137    /// let state = WorkflowState::deserialize(Map::from_iter([(
138    /// "route".into(),
139    /// json!([{"__burr_serde__": "distance", "meters": 12}]),
140    /// )]))?;
141    /// assert_eq!(state.values()["route"], json!([12]));
142    /// # Ok::<(), app_kit::AppError>(())
143    /// ```
144    ///
145    /// # Errors
146    ///
147    /// Returns `Integration` if the registry lock is poisoned. Decoder failures occur
148    /// at decode time and preserve caller error text with tag context.
149    pub fn register_tag<D, DE>(&self, tag: String, deserialize: D) -> Result<(), AppError>
150    where
151        D: Fn(&Value) -> Result<Value, DE> + Send + Sync + 'static,
152        DE: Display,
153    {
154        let decoding_tag = tag.clone();
155        let decoder = Arc::new(move |value: &Value| {
156            deserialize(value).map_err(|error| {
157                AppError::InvalidInput(format!(
158                    "serde dispatch handler for '{decoding_tag}' failed: {error}"
159                ))
160            })
161        });
162        let replaced = {
163            let mut registry = self.registry.write().map_err(|_| {
164                AppError::Integration("state serialization registry lock was poisoned".to_owned())
165            })?;
166            Arc::make_mut(&mut registry).tags.insert(tag, decoder)
167        };
168        drop(replaced);
169        Ok(())
170    }
171
172    fn snapshot(&self) -> Result<Arc<SerializationRegistry>, AppError> {
173        self.registry
174            .read()
175            .map(|registry| Arc::clone(&registry))
176            .map_err(|_| {
177                AppError::Integration("state serialization registry lock was poisoned".to_owned())
178            })
179    }
180}
181
182impl WorkflowState {
183    /// Encodes ordered state using one snapshot of the global field registry.
184    ///
185    /// Unregistered fields keep their JSON values, including tagged objects.
186    /// Field encoders must produce objects. The complete encoded state retains
187    /// the ordinary JSON byte/depth bounds; failure leaves this state unchanged.
188    ///
189    /// ```
190    /// use app_kit::workflow::WorkflowState;
191    /// use serde_json::{Map, Value};
192    /// let values = Map::from_iter([("count".into(), Value::from(2))]);
193    /// let state = WorkflowState::new(values.clone())?;
194    /// assert_eq!(state.serialize()?, values);
195    /// # Ok::<(), app_kit::AppError>(())
196    /// ```
197    ///
198    /// # Errors
199    ///
200    /// Returns `InvalidInput` for a codec error, a field encoder returning a nonobject,
201    /// or exceeded JSON bounds; `Integration` reports registry lock poisoning.
202    ///
203    /// # Panics
204    ///
205    /// Panics if a registered field encoder panics. The registry lock is released
206    /// before callbacks run.
207    pub fn serialize(&self) -> Result<Map<String, Value>, AppError> {
208        StateSerialization::global()
209            .snapshot()?
210            .encode(self.values())
211    }
212
213    /// Decodes state with one snapshot of global field and tag registrations.
214    ///
215    /// Top-level field decoders take precedence over recursive tagged decoding.
216    /// Missing and malformed tags fail; handler outputs are not redispatched.
217    /// Returned state preserves key order and passes the JSON boundary checks.
218    ///
219    /// ```
220    /// use app_kit::workflow::WorkflowState;
221    /// let values = serde_json::from_str(r#"{"count":2,"ready":true}"#)?;
222    /// let state = WorkflowState::deserialize(values)?;
223    /// assert_eq!(state.values()["count"], 2);
224    /// assert_eq!(state.values()["ready"], true);
225    /// # Ok::<(), Box<dyn std::error::Error>>(())
226    /// ```
227    ///
228    /// # Errors
229    ///
230    /// Returns `InvalidInput` for malformed state, a nonstring or unregistered tag,
231    /// a decoder error, or invalid transformed values. Registry lock poisoning is
232    /// `Integration`; field/tag decoder outputs are not recursively redispatched.
233    ///
234    /// # Panics
235    ///
236    /// Panics if a registered field or tag decoder panics. The registry lock is
237    /// released before callbacks run.
238    pub fn deserialize(values: Map<String, Value>) -> Result<Self, AppError> {
239        JsonBoundary::validate_map(&values)?;
240        let values = StateSerialization::global()
241            .snapshot()?
242            .decode_state(values)?;
243        Self::new(values)
244    }
245}
246
247impl SerializationRegistry {
248    fn encode(&self, state: &Map<String, Value>) -> Result<Map<String, Value>, AppError> {
249        let mut output = Map::with_capacity(state.len());
250        for (field, value) in state {
251            let value = match self.fields.get(field) {
252                Some(codec) => {
253                    let value = codec.serialize.transform(value)?;
254                    if !value.is_object() {
255                        return Err(AppError::InvalidInput(format!(
256                            "field serialize handler for '{field}' must return an object, got {}",
257                            Self::value_kind(&value)
258                        )));
259                    }
260                    value
261                }
262                None => value.clone(),
263            };
264            output.insert(field.clone(), value);
265        }
266        JsonBoundary::validate_map(&output)?;
267        Ok(output)
268    }
269
270    fn decode_state(&self, state: Map<String, Value>) -> Result<Map<String, Value>, AppError> {
271        let mut output = Map::with_capacity(state.len());
272        for (field, value) in state {
273            let value = match self.fields.get(&field) {
274                Some(codec) => codec.deserialize.transform(&value)?,
275                None => self.decode_value(value)?,
276            };
277            output.insert(field, value);
278        }
279        Ok(output)
280    }
281
282    fn decode_value(&self, value: Value) -> Result<Value, AppError> {
283        match value {
284            Value::Array(values) => values
285                .into_iter()
286                .map(|value| self.decode_value(value))
287                .collect::<Result<Vec<_>, _>>()
288                .map(Value::Array),
289            Value::Object(values) => {
290                if let Some(tag) = values.get("__burr_serde__") {
291                    let name = tag.as_str().ok_or_else(|| {
292                        AppError::InvalidInput(format!(
293                            "dispatch key '__burr_serde__' must be a string, got {}",
294                            Self::value_kind(tag)
295                        ))
296                    })?;
297                    let decoder = self.tags.get(name).ok_or_else(|| {
298                        AppError::InvalidInput(format!("no function registered for key: {name}"))
299                    })?;
300                    return decoder.transform(&Value::Object(values));
301                }
302                let mut output = Map::with_capacity(values.len());
303                for (key, value) in values {
304                    output.insert(key, self.decode_value(value)?);
305                }
306                Ok(Value::Object(output))
307            }
308            value => Ok(value),
309        }
310    }
311
312    fn value_kind(value: &Value) -> &'static str {
313        match value {
314            Value::Null => "null",
315            Value::Bool(_) => "bool",
316            Value::Number(number) if number.is_i64() => "i64",
317            Value::Number(number) if number.is_u64() => "u64",
318            Value::Number(_) => "f64",
319            Value::String(_) => "string",
320            Value::Array(_) => "array",
321            Value::Object(_) => "object",
322        }
323    }
324}