Skip to main content

scientific_workflow/storage/
json_payload_decoder.rs

1//! Per-payload decoding contracts and key-based decoder registration.
2//!
3//! A payload decoder receives only one field's borrowed raw JSON and returns
4//! one concrete Rust value. It does not receive record time, sibling fields,
5//! chunk metadata, a destination state, or a series. [`JsonPayloadDecoderRegistry`] matches keys
6//! to these independently reusable conversions and privately adapts their
7//! heterogeneous results for insertion into `SystemState`.
8//!
9//! [`JsonPayloadDecoder::decode_json_payload`] accepts `&str` containing exactly one JSON value.
10//! The reader can obtain this slice from `serde_json::value::RawValue` while
11//! retaining the enclosing line buffer. A tensor decoder can therefore build
12//! its final allocation without an intermediate `serde_json::Value` tree. The
13//! returned payload is owned and cannot retain the temporary input.
14//!
15//! Type erasure occurs only inside the registry adapter. Public decoders still
16//! return their real `T`; the adapter moves that value directly into the state
17//! through `SystemState::insert_payload` and never invokes `T::clone`.
18//!
19//! This module performs no JSON parsing, filesystem access, chunk validation,
20//! record-key validation, state construction, or series collection. The reader
21//! owns those operations and dispatches fields in canonical schema order.
22
23use std::collections::{HashMap, HashSet};
24use std::error::Error;
25use std::fmt;
26use std::marker::PhantomData;
27
28use serde::Serialize;
29use serde::de::DeserializeOwned;
30
31use crate::system_state::{StateError, SystemState};
32
33use super::error::StorageError;
34
35#[path = "json_payload_decoder/string.rs"]
36mod string;
37#[path = "json_payload_decoder/vec_f64.rs"]
38mod vec_f64;
39
40pub use string::JsonStringDecoder;
41pub use vec_f64::JsonVecF64Decoder;
42
43/// Object-safe error boundary for application-defined payload conversion.
44type BoxError = Box<dyn Error + Send + Sync + 'static>;
45
46/// Converts one borrowed raw JSON value into one concrete state payload.
47///
48/// `T` stays explicit at this boundary. Any thread-safe
49/// `Fn(&str) -> Result<T, E>` implements the trait automatically; named decoder
50/// types may implement it directly when they own configuration or shared state.
51pub trait JsonPayloadDecoder<T>: Send + Sync + 'static {
52    /// Decoder-specific failure retained by [`StorageError::DecodeField`].
53    type Error: Error + Send + Sync + 'static;
54
55    /// Decodes exactly one complete raw JSON value into an owned payload.
56    fn decode_json_payload(&self, raw_json: &str) -> Result<T, Self::Error>;
57}
58
59impl<T, E, F> JsonPayloadDecoder<T> for F
60where
61    F: Fn(&str) -> Result<T, E> + Send + Sync + 'static,
62    E: Error + Send + Sync + 'static,
63{
64    type Error = E;
65
66    /// Invokes the registered closure without wrapping or copying its output.
67    fn decode_json_payload(&self, raw_json: &str) -> Result<T, Self::Error> {
68        self(raw_json)
69    }
70}
71
72/// Heterogeneous per-key payload decoder registry.
73///
74/// One registry may contain the union of keys used by several output streams.
75/// Coverage therefore requires every selected key but permits additional
76/// registrations. Keys are exact and are not trimmed or case-normalized.
77///
78/// This type is intentionally non-Clone because registered decoders may own
79/// caches, handles, or synchronization state with unknown clone semantics.
80#[derive(Default)]
81pub struct JsonPayloadDecoderRegistry {
82    entries: HashMap<Box<str>, Box<dyn ErasedPayloadDecoder>>,
83}
84
85impl JsonPayloadDecoderRegistry {
86    /// Creates an empty registry.
87    pub fn new() -> Self {
88        Self::default()
89    }
90
91    /// Creates an empty registry with capacity for at least `capacity` keys.
92    pub fn with_capacity(capacity: usize) -> Self {
93        Self {
94            entries: HashMap::with_capacity(capacity),
95        }
96    }
97
98    /// Adds a field decoded directly through Serde JSON into `T`.
99    ///
100    /// This is the concise path for payload types whose JSON representation
101    /// already matches their Rust representation. Specialized decoders remain
102    /// available through [`JsonPayloadDecoderRegistry::register_for_field`]
103    /// when conversion requires configuration, validation, or a different
104    /// wire shape.
105    ///
106    /// # Errors
107    ///
108    /// Returns the same empty-key or duplicate-key errors as
109    /// [`JsonPayloadDecoderRegistry::register_for_field`]. Payload parse
110    /// failures are reported later with their field context by the reader.
111    pub fn with_json_field<T>(mut self, key: impl Into<String>) -> Result<Self, StorageError>
112    where
113        T: DeserializeOwned + Serialize + Clone + Send + 'static,
114    {
115        self.register_for_field::<T, _>(key, |raw_json: &str| serde_json::from_str::<T>(raw_json))?;
116        Ok(self)
117    }
118
119    /// Registers one typed decoder under one exact state field key.
120    ///
121    /// Successful decoded values are moved directly into `SystemState`; the
122    /// adapter never invokes `T::clone`.
123    ///
124    /// # Errors
125    ///
126    /// Returns [`StorageError::InvalidConfiguration`] for an empty key and
127    /// [`StorageError::DuplicateDecoder`] for an existing key. The incoming
128    /// decoder is dropped on either configuration error.
129    pub fn register_for_field<T, D>(
130        &mut self,
131        key: impl Into<String>,
132        decoder: D,
133    ) -> Result<(), StorageError>
134    where
135        T: Serialize + Clone + Send + 'static,
136        D: JsonPayloadDecoder<T>,
137    {
138        let key = key.into();
139        if key.is_empty() {
140            return Err(StorageError::InvalidConfiguration {
141                setting: "decoder.key",
142                reason: "decoder key must not be empty".to_owned(),
143            });
144        }
145        if self.entries.contains_key(key.as_str()) {
146            return Err(StorageError::DuplicateDecoder { field: key });
147        }
148        self.entries.insert(
149            key.into_boxed_str(),
150            Box::new(TypedDecoder {
151                decoder,
152                payload: PhantomData,
153            }),
154        );
155        Ok(())
156    }
157
158    /// Returns the number of registered field keys.
159    pub fn len(&self) -> usize {
160        self.entries.len()
161    }
162
163    /// Reports whether no payload decoder is registered.
164    pub fn is_empty(&self) -> bool {
165        self.entries.is_empty()
166    }
167
168    /// Reports whether an exact field key has a registered decoder.
169    pub fn has_decoder_for_field(&self, key: &str) -> bool {
170        self.entries.contains_key(key)
171    }
172
173    /// Iterates registered keys in unspecified hash-map order.
174    ///
175    /// Reader dispatch never uses this order. Callers needing stable display
176    /// should sort the returned strings.
177    pub fn registered_field_names(&self) -> impl ExactSizeIterator<Item = &str> {
178        self.entries.keys().map(AsRef::as_ref)
179    }
180
181    /// Verifies that every selected stream field has a decoder.
182    ///
183    /// Additional keys remain valid for other streams. Repeated input keys are
184    /// harmless and checked once, though metadata validation rejects them
185    /// before the reader reaches this boundary.
186    pub(crate) fn require<'a>(
187        &self,
188        fields: impl IntoIterator<Item = &'a str>,
189    ) -> Result<(), StorageError> {
190        let mut checked = HashSet::new();
191        for field in fields {
192            if checked.insert(field) && !self.has_decoder_for_field(field) {
193                return Err(StorageError::MissingDecoder {
194                    field: field.to_owned(),
195                });
196            }
197        }
198        Ok(())
199    }
200
201    /// Decodes one matched field and moves it into an empty destination slot.
202    ///
203    /// The reader supplies context after validating record keys. A conversion
204    /// or unexpected insertion failure becomes [`StorageError::DecodeField`]
205    /// with its original source chain preserved.
206    pub(crate) fn decode_into(
207        &self,
208        stream: &str,
209        iteration: u64,
210        field: &str,
211        raw_json: &str,
212        state: &mut SystemState,
213    ) -> Result<(), StorageError> {
214        let decoder = self
215            .entries
216            .get(field)
217            .ok_or_else(|| StorageError::MissingDecoder {
218                field: field.to_owned(),
219            })?;
220        decoder
221            .decode_into(raw_json, field, state)
222            .map_err(|source| StorageError::DecodeField {
223                stream: stream.to_owned(),
224                iteration,
225                field: field.to_owned(),
226                source,
227            })
228    }
229}
230
231impl fmt::Debug for JsonPayloadDecoderRegistry {
232    /// Formats sorted keys without exposing decoder internals or payloads.
233    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
234        let mut keys = self.registered_field_names().collect::<Vec<_>>();
235        keys.sort_unstable();
236        formatter
237            .debug_struct("JsonPayloadDecoderRegistry")
238            .field("keys", &keys)
239            .finish_non_exhaustive()
240    }
241}
242
243/// Object-safe insertion adapter stored by [`JsonPayloadDecoderRegistry`].
244trait ErasedPayloadDecoder: Send + Sync {
245    /// Decodes one field and moves its concrete result into `state`.
246    fn decode_into(
247        &self,
248        raw_json: &str,
249        field: &str,
250        state: &mut SystemState,
251    ) -> Result<(), BoxError>;
252}
253
254/// Concrete typed decoder hidden behind [`ErasedPayloadDecoder`].
255struct TypedDecoder<D, T> {
256    decoder: D,
257    /// Associates the adapter with its concrete output without owning a `T`.
258    payload: PhantomData<fn() -> T>,
259}
260
261impl<T, D> ErasedPayloadDecoder for TypedDecoder<D, T>
262where
263    T: Serialize + Clone + Send + 'static,
264    D: JsonPayloadDecoder<T>,
265{
266    /// Preserves `T` through conversion, then transfers it into the state.
267    fn decode_into(
268        &self,
269        raw_json: &str,
270        field: &str,
271        state: &mut SystemState,
272    ) -> Result<(), BoxError> {
273        let payload = self
274            .decoder
275            .decode_json_payload(raw_json)
276            .map_err(|source| Box::new(source) as BoxError)?;
277
278        match state.insert_payload(field, payload) {
279            Ok(None) => Ok(()),
280            Ok(Some(previous)) => {
281                // Restore the pre-existing payload transactionally. Setting an
282                // identical concrete type must succeed and returns the newly
283                // decoded replacement, which is then dropped.
284                let decoded = state
285                    .insert_payload(field, previous)
286                    .expect("restoring an identical concrete payload type must succeed");
287                drop(decoded);
288                Err(Box::new(DecoderInsertError::Occupied {
289                    field: field.to_owned(),
290                }))
291            }
292            Err(rejection) => {
293                let (source, payload) = rejection.into_parts();
294                drop(payload);
295                Err(Box::new(DecoderInsertError::State(source)))
296            }
297        }
298    }
299}
300
301/// Internal state-insertion failure after payload conversion succeeded.
302#[derive(Debug)]
303enum DecoderInsertError {
304    /// Reader attempted to populate the same state field more than once.
305    Occupied { field: String },
306    /// Destination state did not declare the expected field.
307    State(StateError),
308}
309
310impl fmt::Display for DecoderInsertError {
311    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
312        match self {
313            Self::Occupied { field } => {
314                write!(
315                    formatter,
316                    "decoded state field `{field}` is already populated"
317                )
318            }
319            Self::State(source) => source.fmt(formatter),
320        }
321    }
322}
323
324impl Error for DecoderInsertError {
325    /// Preserves an underlying SystemState insertion failure when present.
326    fn source(&self) -> Option<&(dyn Error + 'static)> {
327        match self {
328            Self::Occupied { .. } => None,
329            Self::State(source) => Some(source),
330        }
331    }
332}