1use std::collections::HashMap;
4use std::fmt;
5use std::iter::FusedIterator;
6use std::path::{Path, PathBuf};
7use std::slice;
8use std::sync::Arc;
9
10use super::super::error::ConfigurationError;
11use super::super::parameter_path::ParameterPath;
12use super::super::parameter_tree::{ParameterLeaf, paths_conflict};
13use super::super::source::{
14 StrictValue, parse_strict_json, read_source, require_object, validate_name,
15};
16use super::super::sweep::{ParsedScope, SweepPlan, parse_scope};
17use super::resolved_configuration::{ConfigurationIter, ResolvedConfiguration};
18
19const PARAMETERS_FILE: &str = "parameters.json";
20const CONFIGURATION_DIRECTORY: &str = "config";
21
22#[derive(Clone)]
27pub struct StudyConfiguration {
28 inner: Arc<StudyConfigurationInner>,
29}
30
31#[derive(Clone)]
33pub struct WorkloadConfiguration {
34 pub(super) inner: Arc<WorkloadConfigurationInner>,
35}
36
37impl StudyConfiguration {
38 pub fn load(study_root: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
40 let study_root = study_root.into();
41 let configuration_directory = Arc::new(study_root.join(CONFIGURATION_DIRECTORY));
42 let source_path = configuration_directory.join(PARAMETERS_FILE);
43 let source = read_source(&source_path)?;
44 let document = parse_strict_json(&source_path, &source)?;
45 let mut root = require_object(
46 &source_path,
47 document,
48 "parameters.json root must be an object",
49 )?;
50 let global = take_required(&source_path, &mut root, "global")?;
51 let components = take_required(&source_path, &mut root, "components")?;
52 reject_remaining(&source_path, &root, "parameters.json")?;
53
54 let global = Arc::new(ScopeConfiguration::new(parse_scope(
55 &source_path,
56 require_object(&source_path, global, "`global` must be an object")?,
57 "global scope",
58 )?)?);
59 let components =
60 require_object(&source_path, components, "`components` must be an object")?;
61 if components.is_empty() {
62 return super::super::source::invalid(
63 &source_path,
64 "`components` must contain at least one component",
65 );
66 }
67
68 let mut workloads = HashMap::with_capacity(components.len());
69 for (component_key, component) in components {
70 validate_name(&source_path, &component_key, "component key")?;
71 let mut component = require_object(
72 &source_path,
73 component,
74 format!("component `{component_key}` must be an object"),
75 )?;
76 let shared = take_required(&source_path, &mut component, "shared")?;
77 let workload = take_required(&source_path, &mut component, "workloads")?;
78 reject_remaining(
79 &source_path,
80 &component,
81 &format!("component `{component_key}`"),
82 )?;
83 let shared = Arc::new(ScopeConfiguration::new(parse_scope(
84 &source_path,
85 require_object(
86 &source_path,
87 shared,
88 format!("component `{component_key}` field `shared` must be an object"),
89 )?,
90 &format!("component `{component_key}` shared scope"),
91 )?)?);
92 let workload = require_object(
93 &source_path,
94 workload,
95 format!("component `{component_key}` field `workloads` must be an object"),
96 )?;
97 if workload.is_empty() {
98 return super::super::source::invalid(
99 &source_path,
100 format!("component `{component_key}` must contain at least one workload"),
101 );
102 }
103 let mut component_workloads = HashMap::with_capacity(workload.len());
104 for (workload_key, values) in workload {
105 validate_name(&source_path, &workload_key, "workload key")?;
106 let local = Arc::new(ScopeConfiguration::new(parse_scope(
107 &source_path,
108 require_object(
109 &source_path,
110 values,
111 format!("workload `{component_key}/{workload_key}` must be an object"),
112 )?,
113 &format!("workload `{component_key}/{workload_key}`"),
114 )?)?);
115 let space = compose_space(
116 Arc::clone(&configuration_directory),
117 &source_path,
118 &component_key,
119 &workload_key,
120 [Arc::clone(&global), Arc::clone(&shared), local],
121 )?;
122 component_workloads.insert(workload_key.into_boxed_str(), Arc::new(space));
123 }
124 workloads.insert(component_key.into_boxed_str(), component_workloads);
125 }
126 Ok(Self {
127 inner: Arc::new(StudyConfigurationInner {
128 study_root,
129 configuration_directory,
130 source_path,
131 source: source.into_boxed_slice(),
132 workloads,
133 }),
134 })
135 }
136
137 pub fn study_root(&self) -> &Path {
139 &self.inner.study_root
140 }
141
142 pub fn configuration_directory(&self) -> &Path {
144 self.inner.configuration_directory.as_path()
145 }
146
147 pub fn source_path(&self) -> &Path {
149 &self.inner.source_path
150 }
151
152 pub fn source_json(&self) -> &[u8] {
154 &self.inner.source
155 }
156
157 pub fn workload(
159 &self,
160 component: &str,
161 workload: &str,
162 ) -> Result<WorkloadConfiguration, ConfigurationError> {
163 let inner = self
164 .inner
165 .workloads
166 .get(component)
167 .and_then(|workloads| workloads.get(workload))
168 .map(Arc::clone)
169 .ok_or_else(|| ConfigurationError::UnknownWorkloadConfiguration {
170 component: component.to_owned(),
171 workload: workload.to_owned(),
172 })?;
173 Ok(WorkloadConfiguration { inner })
174 }
175}
176
177impl WorkloadConfiguration {
178 pub fn configuration_directory(&self) -> &Path {
180 self.inner.configuration_directory.as_path()
181 }
182
183 pub fn component(&self) -> &str {
185 &self.inner.component
186 }
187
188 pub fn workload(&self) -> &str {
190 &self.inner.workload
191 }
192
193 pub fn combination_count(&self) -> u64 {
195 self.inner.combination_count
196 }
197
198 pub fn fixed_value_count(&self) -> usize {
200 self.inner.fixed_leaves().count()
201 }
202
203 pub fn swept_value_count(&self) -> usize {
205 self.inner
206 .scopes
207 .iter()
208 .map(|scope| scope.sweep.selectable_paths().len())
209 .sum()
210 }
211
212 pub fn contains(&self, key: &str) -> bool {
214 let Some(path) = ParameterPath::parse(key) else {
215 return false;
216 };
217 self.inner
218 .fixed_leaves()
219 .any(|leaf| leaf.path == path || path.is_ancestor_of(&leaf.path))
220 || self
221 .inner
222 .selectable_paths()
223 .any(|candidate| candidate == &path || path.is_ancestor_of(candidate))
224 }
225
226 pub fn fixed_keys(&self) -> impl ExactSizeIterator<Item = &str> {
228 self.inner.fixed_leaves().map(|leaf| leaf.path.identifier())
229 }
230
231 pub fn sweep_keys(&self) -> impl ExactSizeIterator<Item = &str> {
233 self.inner.selectable_paths().map(ParameterPath::identifier)
234 }
235
236 pub fn combination(&self, ordinal: u64) -> Result<ResolvedConfiguration, ConfigurationError> {
238 if ordinal >= self.combination_count() {
239 return Err(ConfigurationError::CombinationOrdinalOutOfBounds {
240 ordinal,
241 combination_count: self.combination_count(),
242 });
243 }
244 Ok(ResolvedConfiguration::new(Arc::clone(&self.inner), ordinal))
245 }
246
247 pub fn combinations(&self) -> ConfigurationIter {
249 ConfigurationIter::new(Arc::clone(&self.inner), self.combination_count())
250 }
251}
252
253impl fmt::Debug for StudyConfiguration {
254 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
255 formatter
256 .debug_struct("StudyConfiguration")
257 .field("study_root", &self.study_root())
258 .field(
259 "workloads",
260 &self
261 .inner
262 .workloads
263 .values()
264 .map(HashMap::len)
265 .sum::<usize>(),
266 )
267 .finish_non_exhaustive()
268 }
269}
270
271impl fmt::Debug for WorkloadConfiguration {
272 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
273 formatter
274 .debug_struct("WorkloadConfiguration")
275 .field("component", &self.component())
276 .field("workload", &self.workload())
277 .field("combinations", &self.combination_count())
278 .finish_non_exhaustive()
279 }
280}
281
282struct StudyConfigurationInner {
283 study_root: PathBuf,
284 configuration_directory: Arc<PathBuf>,
285 source_path: PathBuf,
286 source: Box<[u8]>,
287 workloads: HashMap<Box<str>, HashMap<Box<str>, Arc<WorkloadConfigurationInner>>>,
288}
289
290pub(crate) struct WorkloadConfigurationInner {
291 pub(super) configuration_directory: Arc<PathBuf>,
292 pub(super) component: Box<str>,
293 pub(super) workload: Box<str>,
294 scopes: [Arc<ScopeConfiguration>; 3],
295 pub(super) combination_count: u64,
296}
297
298fn compose_space(
299 configuration_directory: Arc<PathBuf>,
300 source_path: &Path,
301 component: &str,
302 workload: &str,
303 scopes: [Arc<ScopeConfiguration>; 3],
304) -> Result<WorkloadConfigurationInner, ConfigurationError> {
305 validate_scopes_disjoint(source_path, &scopes)?;
306 let combination_count = scopes.iter().try_fold(1_u64, |count, scope| {
307 count.checked_mul(scope.combination_count()).ok_or_else(|| {
308 ConfigurationError::CombinationCountOverflow {
309 axis: format!("workload `{component}/{workload}`"),
310 }
311 })
312 })?;
313 Ok(WorkloadConfigurationInner {
314 configuration_directory,
315 component: component.to_owned().into_boxed_str(),
316 workload: workload.to_owned().into_boxed_str(),
317 scopes,
318 combination_count,
319 })
320}
321
322struct ScopeConfiguration {
323 fixed: Vec<ParameterLeaf>,
324 fixed_by_path: HashMap<ParameterPath, usize>,
325 sweep: SweepPlan,
326}
327
328impl ScopeConfiguration {
329 fn new(scope: ParsedScope) -> Result<Self, ConfigurationError> {
330 let fixed_by_path = scope
331 .fixed
332 .iter()
333 .enumerate()
334 .map(|(index, leaf)| (leaf.path.clone(), index))
335 .collect();
336 Ok(Self {
337 fixed: scope.fixed,
338 fixed_by_path,
339 sweep: SweepPlan::new(scope.dimensions)?,
340 })
341 }
342
343 const fn combination_count(&self) -> u64 {
344 self.sweep.combination_count()
345 }
346}
347
348impl WorkloadConfigurationInner {
349 pub(super) fn fixed_leaves(&self) -> ScopeSliceIter<'_, ParameterLeaf> {
350 ScopeSliceIter::new([
351 &self.scopes[0].fixed,
352 &self.scopes[1].fixed,
353 &self.scopes[2].fixed,
354 ])
355 }
356
357 pub(super) fn selected_leaves(&self, ordinal: u64) -> impl Iterator<Item = &ParameterLeaf> {
358 self.scopes[0]
359 .sweep
360 .selected_leaves(self.scope_ordinal(ordinal, 0))
361 .chain(
362 self.scopes[1]
363 .sweep
364 .selected_leaves(self.scope_ordinal(ordinal, 1)),
365 )
366 .chain(
367 self.scopes[2]
368 .sweep
369 .selected_leaves(self.scope_ordinal(ordinal, 2)),
370 )
371 }
372
373 pub(super) fn selectable_paths(&self) -> ScopeSliceIter<'_, ParameterPath> {
374 ScopeSliceIter::new([
375 self.scopes[0].sweep.selectable_paths(),
376 self.scopes[1].sweep.selectable_paths(),
377 self.scopes[2].sweep.selectable_paths(),
378 ])
379 }
380
381 pub(super) fn fixed_leaf(&self, path: &ParameterPath) -> Option<&ParameterLeaf> {
382 self.scopes.iter().find_map(|scope| {
383 scope
384 .fixed_by_path
385 .get(path)
386 .map(|&position| &scope.fixed[position])
387 })
388 }
389
390 pub(super) fn scope_ordinal(&self, ordinal: u64, scope: usize) -> u64 {
391 let following = self.scopes[(scope + 1)..]
392 .iter()
393 .map(|scope| scope.combination_count())
394 .product::<u64>();
395 (ordinal / following) % self.scopes[scope].combination_count()
396 }
397}
398
399fn validate_scopes_disjoint(
400 source_path: &Path,
401 scopes: &[Arc<ScopeConfiguration>; 3],
402) -> Result<(), ConfigurationError> {
403 let mut paths: Vec<&ParameterPath> = Vec::new();
404 for path in scopes.iter().flat_map(|scope| {
405 scope
406 .fixed
407 .iter()
408 .map(|leaf| &leaf.path)
409 .chain(scope.sweep.selectable_paths())
410 }) {
411 if let Some(previous) = paths.iter().find(|previous| paths_conflict(previous, path)) {
412 return super::super::source::invalid(
413 source_path,
414 format!("parameter paths `{previous}` and `{path}` overlap"),
415 );
416 }
417 paths.push(path);
418 }
419 Ok(())
420}
421
422pub(super) struct ScopeSliceIter<'a, T> {
423 scopes: [slice::Iter<'a, T>; 3],
424 current: usize,
425 remaining: usize,
426}
427
428impl<'a, T> ScopeSliceIter<'a, T> {
429 fn new(scopes: [&'a [T]; 3]) -> Self {
430 Self {
431 remaining: scopes.iter().map(|scope| scope.len()).sum(),
432 scopes: scopes.map(<[T]>::iter),
433 current: 0,
434 }
435 }
436}
437
438impl<'a, T> Iterator for ScopeSliceIter<'a, T> {
439 type Item = &'a T;
440
441 fn next(&mut self) -> Option<Self::Item> {
442 while self.current < self.scopes.len() {
443 if let Some(value) = self.scopes[self.current].next() {
444 self.remaining -= 1;
445 return Some(value);
446 }
447 self.current += 1;
448 }
449 None
450 }
451
452 fn size_hint(&self) -> (usize, Option<usize>) {
453 (self.remaining, Some(self.remaining))
454 }
455}
456
457impl<T> ExactSizeIterator for ScopeSliceIter<'_, T> {}
458impl<T> FusedIterator for ScopeSliceIter<'_, T> {}
459
460fn take_required(
461 path: &Path,
462 entries: &mut Vec<(String, StrictValue)>,
463 name: &str,
464) -> Result<StrictValue, ConfigurationError> {
465 let Some(position) = entries.iter().position(|(key, _)| key == name) else {
466 return super::super::source::invalid(path, format!("required field `{name}` is missing"));
467 };
468 Ok(entries.remove(position).1)
469}
470
471fn reject_remaining(
472 path: &Path,
473 entries: &[(String, StrictValue)],
474 context: &str,
475) -> Result<(), ConfigurationError> {
476 if let Some((name, _)) = entries.first() {
477 return super::super::source::invalid(
478 path,
479 format!("{context} contains unknown field `{name}`"),
480 );
481 }
482 Ok(())
483}