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