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}