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}