Skip to main content

scientific_workflow/configuration/parameters/
resolved_configuration.rs

1//! Cheap resolved combinations with lazy nested-document materialization.
2
3use std::fmt;
4use std::iter::FusedIterator;
5use std::sync::{Arc, OnceLock};
6
7use serde::de::DeserializeOwned;
8use serde_json::Value;
9
10use super::super::error::ConfigurationError;
11use super::super::parameter_key_tuple::ParameterKeyTuple;
12use super::super::parameter_path::ParameterPath;
13use super::super::parameter_tree::{lookup_path, reconstruct};
14use super::WorkloadConfigurationInner;
15use super::reconstruction;
16
17/// One immutable, lazily materialized workload configuration combination.
18pub struct ResolvedConfiguration {
19    inner: Arc<WorkloadConfigurationInner>,
20    ordinal: u64,
21    resolved: OnceLock<Value>,
22}
23
24impl Clone for ResolvedConfiguration {
25    fn clone(&self) -> Self {
26        Self {
27            inner: Arc::clone(&self.inner),
28            ordinal: self.ordinal,
29            resolved: OnceLock::new(),
30        }
31    }
32}
33
34impl ResolvedConfiguration {
35    pub(super) fn new(inner: Arc<WorkloadConfigurationInner>, ordinal: u64) -> Self {
36        Self {
37            inner,
38            ordinal,
39            resolved: OnceLock::new(),
40        }
41    }
42
43    /// Returns the flattened ordinal within this workload configuration.
44    pub fn ordinal(&self) -> u64 {
45        self.ordinal
46    }
47
48    /// Returns the stable string key of the containing workload component.
49    pub fn component(&self) -> &str {
50        &self.inner.component
51    }
52
53    /// Returns the stable string key of the selected workload.
54    pub fn workload(&self) -> &str {
55        &self.inner.workload
56    }
57
58    /// Returns the selection ordinal contributed by the global scope.
59    pub fn global_ordinal(&self) -> u64 {
60        self.inner.scope_ordinal(self.ordinal, 0)
61    }
62
63    /// Returns the selection ordinal contributed by the component shared scope.
64    pub fn component_ordinal(&self) -> u64 {
65        self.inner.scope_ordinal(self.ordinal, 1)
66    }
67
68    /// Returns the selection ordinal contributed by the workload-local scope.
69    pub fn workload_ordinal(&self) -> u64 {
70        self.inner.scope_ordinal(self.ordinal, 2)
71    }
72
73    /// Borrows an exact leaf or lazily reconstructed nested subtree.
74    pub fn value(&self, key: &str) -> Option<&Value> {
75        let path = ParameterPath::parse(key)?;
76        if let Some(leaf) = self.inner.fixed_leaf(&path) {
77            return Some(&leaf.value);
78        }
79        if let Some(leaf) = self
80            .inner
81            .selected_leaves(self.ordinal)
82            .find(|leaf| leaf.path == path)
83        {
84            return Some(&leaf.value);
85        }
86        lookup_path(self.resolved_document(), &path)
87    }
88
89    /// Borrows a required value or returns an ordinal-qualified error.
90    pub fn require_value(&self, key: &str) -> Result<&Value, ConfigurationError> {
91        self.value(key)
92            .ok_or_else(|| ConfigurationError::UnknownConfigurationValue {
93                ordinal: self.ordinal,
94                key: key.to_owned(),
95            })
96    }
97
98    /// Deserializes one exact leaf or reconstructed subtree into `T`.
99    pub fn decode_value<T>(&self, key: &str) -> Result<T, ConfigurationError>
100    where
101        T: DeserializeOwned,
102    {
103        let Some(path) = ParameterPath::parse(key) else {
104            return Err(ConfigurationError::UnknownConfigurationValue {
105                ordinal: self.ordinal,
106                key: key.to_owned(),
107            });
108        };
109        if let Some(value) = self.exact_leaf(&path) {
110            return T::deserialize(value).map_err(|source| {
111                ConfigurationError::DecodeConfigurationValue {
112                    ordinal: self.ordinal,
113                    key: key.to_owned(),
114                    source,
115                }
116            });
117        }
118        let subtree = reconstruct(
119            self.inner
120                .fixed_leaves()
121                .chain(self.inner.selected_leaves(self.ordinal))
122                .filter(|leaf| path.is_ancestor_of(&leaf.path)),
123        );
124        let Some(value) = lookup_path(&subtree, &path) else {
125            return Err(ConfigurationError::UnknownConfigurationValue {
126                ordinal: self.ordinal,
127                key: key.to_owned(),
128            });
129        };
130        T::deserialize(value).map_err(|source| ConfigurationError::DecodeConfigurationValue {
131            ordinal: self.ordinal,
132            key: key.to_owned(),
133            source,
134        })
135    }
136
137    /// Deserializes a tuple of required paths without an intermediate struct.
138    pub fn decode_values<Values, Keys>(&self, keys: Keys) -> Result<Values, ConfigurationError>
139    where
140        Keys: ParameterKeyTuple<Values>,
141    {
142        keys.decode(self)
143    }
144
145    /// Reports whether this resolved combination contains `key` or descendants.
146    pub fn contains(&self, key: &str) -> bool {
147        let Some(path) = ParameterPath::parse(key) else {
148            return false;
149        };
150        self.inner
151            .fixed_leaves()
152            .any(|leaf| leaf.path == path || path.is_ancestor_of(&leaf.path))
153            || self
154                .inner
155                .selected_leaves(self.ordinal)
156                .any(|leaf| leaf.path == path || path.is_ancestor_of(&leaf.path))
157    }
158
159    /// Returns the number of resolved terminal values.
160    pub fn len(&self) -> usize {
161        self.inner.fixed_leaves().count() + self.inner.selected_leaves(self.ordinal).count()
162    }
163
164    /// Reports whether the resolved document contains no terminal values.
165    pub fn is_empty(&self) -> bool {
166        self.len() == 0
167    }
168
169    /// Iterates canonical terminal parameter identifiers.
170    pub fn keys(&self) -> impl Iterator<Item = &str> {
171        self.inner
172            .fixed_leaves()
173            .map(|leaf| leaf.path.identifier())
174            .chain(
175                self.inner
176                    .selected_leaves(self.ordinal)
177                    .map(|leaf| leaf.path.identifier()),
178            )
179    }
180
181    /// Iterates resolved terminal keys and borrowed values in declaration order.
182    pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
183        self.inner
184            .fixed_leaves()
185            .map(|leaf| (leaf.path.identifier(), &leaf.value))
186            .chain(
187                self.inner
188                    .selected_leaves(self.ordinal)
189                    .map(|leaf| (leaf.path.identifier(), &leaf.value)),
190            )
191    }
192
193    /// Serializes the complete rehydrated nested configuration document.
194    pub fn to_json(&self) -> String {
195        self.resolved_document().to_string()
196    }
197
198    /// Clones the complete rehydrated nested configuration document.
199    ///
200    /// The document is cached after its first reconstruction. This method is
201    /// intended for durable provenance records that must own their JSON value.
202    pub fn to_json_value(&self) -> Value {
203        self.resolved_document().clone()
204    }
205
206    fn resolved_document(&self) -> &Value {
207        self.resolved
208            .get_or_init(|| reconstruction::document(&self.inner, self.ordinal))
209    }
210
211    pub(crate) fn resolved_object(&self) -> &serde_json::Map<String, Value> {
212        self.resolved_document()
213            .as_object()
214            .expect("resolved configurations always form a JSON object")
215    }
216
217    fn exact_leaf(&self, path: &ParameterPath) -> Option<&Value> {
218        if let Some(leaf) = self.inner.fixed_leaf(path) {
219            return Some(&leaf.value);
220        }
221        self.inner
222            .selected_leaves(self.ordinal)
223            .find(|leaf| &leaf.path == path)
224            .map(|leaf| &leaf.value)
225    }
226}
227
228impl fmt::Debug for ResolvedConfiguration {
229    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230        formatter
231            .debug_struct("ResolvedConfiguration")
232            .field("component", &self.component())
233            .field("workload", &self.workload())
234            .field("ordinal", &self.ordinal)
235            .field("values", &self.len())
236            .finish_non_exhaustive()
237    }
238}
239
240/// Lazy deterministic iterator over all combinations of one workload.
241#[derive(Clone)]
242pub struct ConfigurationIter {
243    inner: Arc<WorkloadConfigurationInner>,
244    next: u64,
245    end: u64,
246}
247
248impl ConfigurationIter {
249    pub(super) fn new(inner: Arc<WorkloadConfigurationInner>, end: u64) -> Self {
250        Self {
251            inner,
252            next: 0,
253            end,
254        }
255    }
256}
257
258impl Iterator for ConfigurationIter {
259    type Item = ResolvedConfiguration;
260
261    fn next(&mut self) -> Option<Self::Item> {
262        if self.next == self.end {
263            return None;
264        }
265        let ordinal = self.next;
266        self.next += 1;
267        Some(ResolvedConfiguration::new(Arc::clone(&self.inner), ordinal))
268    }
269
270    fn size_hint(&self) -> (usize, Option<usize>) {
271        let remaining = self.end - self.next;
272        match usize::try_from(remaining) {
273            Ok(remaining) => (remaining, Some(remaining)),
274            Err(_) => (usize::MAX, None),
275        }
276    }
277}
278
279impl FusedIterator for ConfigurationIter {}
280
281impl fmt::Debug for ConfigurationIter {
282    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
283        formatter
284            .debug_struct("ConfigurationIter")
285            .field("next", &self.next)
286            .field("end", &self.end)
287            .finish_non_exhaustive()
288    }
289}