Skip to main content

scientific_workflow/system_state/
state.rs

1//! Fixed-layout, heterogeneous state values at one scientific time point.
2//!
3//! [`SystemState`] is the public typed access boundary over the private
4//! [`StateValue`](super::value::StateValue) erasure layer. Every state owns its
5//! payloads, while all states derived from one [`StateSpec`] share immutable
6//! field metadata and name lookup tables.
7//!
8//! # Layout invariant
9//!
10//! The payload vector always has exactly one slot per field declared by the
11//! specification. A slot may be empty, but fields cannot be added, removed, or
12//! reordered after template loading. Consequently, name lookup resolves once
13//! to a stable integer index and all states with the same specification have
14//! identical structural shape.
15//!
16//! # Ownership and cloning
17//!
18//! `set` consumes a payload, and `take` returns that same owned payload without
19//! calling `Clone`. Moving a complete state into an SSTS or writer similarly
20//! moves ownership. Explicitly cloning a `SystemState` is different: it shares
21//! the immutable specification but deep-clones every populated payload.
22//!
23//! # Type safety
24//!
25//! Typed access uses Rust's exact runtime [`TypeId`](std::any::TypeId). A type
26//! mismatch reports both type names. A failed consuming `take` restores the
27//! original erased payload to its slot before returning the error, so an
28//! incorrect type request cannot discard scientific data.
29
30use std::any::{Any, type_name};
31use std::fmt;
32
33use super::error::StateError;
34use super::spec::{FieldSpec, StateSpec};
35use super::value::StateValue;
36
37/// The temporal coordinate associated with one [`SystemState`].
38///
39/// `index` is always present and provides deterministic ordering, chunk
40/// boundaries, and checkpoint identity. `physical` optionally records a
41/// finite domain time such as seconds or model time. Time-axis units belong to
42/// SSTS metadata so they are not repeated in every state.
43#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct TimePoint {
45    index: u64,
46    physical: Option<f64>,
47}
48
49impl TimePoint {
50    /// Creates an index-only time point.
51    pub const fn new(index: u64) -> Self {
52        Self {
53            index,
54            physical: None,
55        }
56    }
57
58    /// Creates a time point with an optional finite physical coordinate.
59    ///
60    /// Returns `None` for `NaN` or either infinity. Negative finite values are
61    /// accepted because some scientific coordinate systems use an origin after
62    /// the beginning of a simulation or observation.
63    pub fn from_physical(index: u64, physical: f64) -> Option<Self> {
64        physical.is_finite().then_some(Self {
65            index,
66            physical: Some(physical),
67        })
68    }
69
70    /// Returns the deterministic integer index.
71    pub const fn index(self) -> u64 {
72        self.index
73    }
74
75    /// Returns the optional physical coordinate.
76    pub const fn physical(self) -> Option<f64> {
77        self.physical
78    }
79}
80
81/// A heterogeneous collection of payloads describing one system time point.
82///
83/// Fields are declared by a JSON-derived [`StateSpec`]. Values are addressed
84/// by those field names but stored in compact optional slots. The type-erased
85/// representation remains private; callers always insert, borrow, mutate, and
86/// extract concrete Rust types.
87pub struct SystemState {
88    spec: StateSpec,
89    time: TimePoint,
90    values: Vec<Option<StateValue>>,
91}
92
93impl SystemState {
94    /// Creates an empty state from a validated specification.
95    ///
96    /// This constructor is crate-private so an external caller cannot create a
97    /// state without first loading a template. [`StateSpec::empty`] is the
98    /// public initial construction path.
99    pub(crate) fn new(spec: StateSpec, time: TimePoint) -> Self {
100        let values = (0..spec.len()).map(|_| None).collect();
101        Self { spec, time, values }
102    }
103
104    /// Creates another empty state with the same shared specification.
105    ///
106    /// No payload is cloned. Only the immutable specification handle is
107    /// cloned, which increments an internal `Arc` reference count.
108    pub fn empty(&self, time: TimePoint) -> Self {
109        Self::new(self.spec.clone(), time)
110    }
111
112    /// Returns this state's temporal coordinate.
113    pub const fn time(&self) -> TimePoint {
114        self.time
115    }
116
117    /// Returns the shared immutable field specification.
118    pub const fn spec(&self) -> &StateSpec {
119        &self.spec
120    }
121
122    /// Returns the number of fields declared by the state specification.
123    ///
124    /// This count is structural and includes empty payload slots.
125    pub fn len(&self) -> usize {
126        self.values.len()
127    }
128
129    /// Reports whether the state specification declares no fields.
130    ///
131    /// This is consistent with [`SystemState::len`]. To test whether a
132    /// non-empty layout currently carries no payloads, use
133    /// [`SystemState::is_blank`].
134    pub fn is_empty(&self) -> bool {
135        self.values.is_empty()
136    }
137
138    /// Returns the number of slots that currently contain payloads.
139    pub fn loaded(&self) -> usize {
140        self.values.iter().filter(|value| value.is_some()).count()
141    }
142
143    /// Reports whether every declared payload slot is empty.
144    pub fn is_blank(&self) -> bool {
145        self.values.iter().all(Option::is_none)
146    }
147
148    /// Returns field specifications in deterministic template order.
149    pub fn fields(&self) -> &[FieldSpec] {
150        self.spec.fields()
151    }
152
153    /// Reports whether a declared field currently contains a payload.
154    ///
155    /// # Errors
156    ///
157    /// Returns [`StateError::UnknownField`] when `key` was not declared by the
158    /// JSON template.
159    pub fn has(&self, key: &str) -> Result<bool, StateError> {
160        let index = self.spec.index_of(key)?;
161        Ok(self.values[index].is_some())
162    }
163
164    /// Reports whether a populated field contains the exact Rust type `T`.
165    ///
166    /// An empty declared field returns `false`.
167    ///
168    /// # Errors
169    ///
170    /// Returns [`StateError::UnknownField`] when `key` was not declared by the
171    /// JSON template.
172    pub fn is<T>(&self, key: &str) -> Result<bool, StateError>
173    where
174        T: Any,
175    {
176        let index = self.spec.index_of(key)?;
177        Ok(self.values[index].as_ref().is_some_and(StateValue::is::<T>))
178    }
179
180    /// Sets or replaces the payload in a declared field.
181    ///
182    /// `payload` moves into the state and is never cloned. If the slot was
183    /// already populated, its previous value is dropped. Call [`take`](Self::take)
184    /// first when the previous payload must be retained.
185    ///
186    /// # Errors
187    ///
188    /// Returns [`StateError::UnknownField`] when `key` was not declared by the
189    /// JSON template. The payload is dropped with the returned error because
190    /// this minimal API does not expose the internal erased-value wrapper.
191    pub fn set<T>(&mut self, key: &str, payload: T) -> Result<(), StateError>
192    where
193        T: Any + Clone + Send,
194    {
195        let index = self.spec.index_of(key)?;
196        self.values[index] = Some(StateValue::new(payload));
197        Ok(())
198    }
199
200    /// Borrows a populated field as the exact Rust type `T`.
201    ///
202    /// # Errors
203    ///
204    /// Returns [`StateError::UnknownField`] for an undeclared key,
205    /// [`StateError::MissingValue`] for an empty slot, or
206    /// [`StateError::TypeMismatch`] when the stored concrete type differs from
207    /// `T`.
208    pub fn get<T>(&self, key: &str) -> Result<&T, StateError>
209    where
210        T: Any,
211    {
212        let value = self.value(key)?;
213        let actual = value.type_name();
214
215        value
216            .downcast_ref::<T>()
217            .ok_or_else(|| StateError::TypeMismatch {
218                field: key.to_owned(),
219                expected: type_name::<T>(),
220                actual,
221            })
222    }
223
224    /// Mutably borrows a populated field as the exact Rust type `T`.
225    ///
226    /// Mutation occurs in place and does not clone the payload.
227    ///
228    /// # Errors
229    ///
230    /// Returns [`StateError::UnknownField`] for an undeclared key,
231    /// [`StateError::MissingValue`] for an empty slot, or
232    /// [`StateError::TypeMismatch`] when the stored concrete type differs from
233    /// `T`.
234    pub fn get_mut<T>(&mut self, key: &str) -> Result<&mut T, StateError>
235    where
236        T: Any,
237    {
238        let value = self.value_mut(key)?;
239        let actual = value.type_name();
240
241        value
242            .downcast_mut::<T>()
243            .ok_or_else(|| StateError::TypeMismatch {
244                field: key.to_owned(),
245                expected: type_name::<T>(),
246                actual,
247            })
248    }
249
250    /// Removes and returns the payload from a declared field.
251    ///
252    /// A successful call moves the original concrete `T` out of its internal
253    /// box and leaves the field slot empty. It does not invoke `Clone`. If `T`
254    /// does not match, the original erased value is restored before the error
255    /// is returned.
256    ///
257    /// # Errors
258    ///
259    /// Returns [`StateError::UnknownField`] for an undeclared key,
260    /// [`StateError::MissingValue`] for an empty slot, or
261    /// [`StateError::TypeMismatch`] when the stored concrete type differs from
262    /// `T`.
263    pub fn take<T>(&mut self, key: &str) -> Result<T, StateError>
264    where
265        T: Any + Send,
266    {
267        let index = self.spec.index_of(key)?;
268        let value = self.values[index]
269            .take()
270            .ok_or_else(|| StateError::MissingValue {
271                field: key.to_owned(),
272            })?;
273        let actual = value.type_name();
274
275        match value.downcast::<T>() {
276            Ok(payload) => Ok(payload),
277            Err(value) => {
278                self.values[index] = Some(value);
279                Err(StateError::TypeMismatch {
280                    field: key.to_owned(),
281                    expected: type_name::<T>(),
282                    actual,
283                })
284            }
285        }
286    }
287
288    /// Drops the payload stored in one declared field.
289    ///
290    /// Returns `true` when a payload was present and dropped, or `false` when
291    /// the declared slot was already empty.
292    ///
293    /// # Errors
294    ///
295    /// Returns [`StateError::UnknownField`] when `key` was not declared by the
296    /// JSON template.
297    pub fn clear(&mut self, key: &str) -> Result<bool, StateError> {
298        let index = self.spec.index_of(key)?;
299        Ok(self.values[index].take().is_some())
300    }
301
302    /// Drops every payload while retaining the shared layout and time point.
303    pub fn clear_all(&mut self) {
304        self.values.iter_mut().for_each(|value| *value = None);
305    }
306
307    /// Returns a populated erased value for a typed immutable accessor.
308    fn value(&self, key: &str) -> Result<&StateValue, StateError> {
309        let index = self.spec.index_of(key)?;
310        self.values[index]
311            .as_ref()
312            .ok_or_else(|| StateError::MissingValue {
313                field: key.to_owned(),
314            })
315    }
316
317    /// Returns a populated erased value for a typed mutable accessor.
318    fn value_mut(&mut self, key: &str) -> Result<&mut StateValue, StateError> {
319        let index = self.spec.index_of(key)?;
320        self.values[index]
321            .as_mut()
322            .ok_or_else(|| StateError::MissingValue {
323                field: key.to_owned(),
324            })
325    }
326}
327
328impl Clone for SystemState {
329    /// Shares the immutable specification and deep-clones populated payloads.
330    fn clone(&self) -> Self {
331        Self {
332            spec: self.spec.clone(),
333            time: self.time,
334            values: self.values.clone(),
335        }
336    }
337}
338
339impl fmt::Debug for SystemState {
340    /// Formats structural metadata without formatting scientific payloads.
341    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
342        formatter
343            .debug_struct("SystemState")
344            .field("time", &self.time)
345            .field("source", &self.spec.source())
346            .field("fields", &self.len())
347            .field("loaded", &self.loaded())
348            .finish_non_exhaustive()
349    }
350}