1use std::collections::{HashMap, HashSet};
51use std::fmt;
52use std::fs;
53use std::iter::FusedIterator;
54use std::path::{Path, PathBuf};
55use std::sync::Arc;
56
57use serde::de::{DeserializeOwned, MapAccess, SeqAccess, Visitor};
58use serde::ser::SerializeMap;
59use serde::{Deserialize, Deserializer, Serialize, Serializer};
60use serde_json::{Number, Value};
61
62use super::{ParameterKeyTuple, error::ConfigurationError};
63
64const FIXED_FILE: &str = "fixed.json";
65const SWEEP_FILE: &str = "sweep.json";
66
67#[derive(Clone)]
73pub struct ParameterSpace {
74 inner: Arc<ParameterSpaceInner>,
75}
76
77impl ParameterSpace {
78 pub fn load(configuration_directory: impl Into<PathBuf>) -> Result<Self, ConfigurationError> {
90 let configuration_directory = configuration_directory.into();
91 let fixed_path = configuration_directory.join(FIXED_FILE);
92 let sweep_path = configuration_directory.join(SWEEP_FILE);
93 let fixed_source = read_source(&fixed_path)?;
94 let sweep_source = read_source(&sweep_path)?;
95 let fixed_document = parse_strict_json(&fixed_path, &fixed_source)?;
96 let sweep_document = parse_strict_json(&sweep_path, &sweep_source)?;
97 let fixed = parse_fixed(&fixed_path, fixed_document)?;
98 let sweep = parse_sweep(&sweep_path, sweep_document)?;
99 validate_disjoint(&fixed_path, &sweep_path, &fixed, &sweep)?;
100
101 let fixed_by_name = fixed
102 .iter()
103 .enumerate()
104 .map(|(index, entry)| (entry.name.clone(), index))
105 .collect();
106 let sweep_by_name = sweep
107 .keys()
108 .enumerate()
109 .map(|(index, key)| (Box::<str>::from(key), index))
110 .collect();
111 let task_count = sweep.task_count();
112
113 Ok(Self {
114 inner: Arc::new(ParameterSpaceInner {
115 configuration_directory,
116 fixed_source: fixed_source.into_boxed_slice(),
117 sweep_source: sweep_source.into_boxed_slice(),
118 fixed,
119 fixed_by_name,
120 sweep,
121 sweep_by_name,
122 task_count,
123 }),
124 })
125 }
126
127 pub fn configuration_directory(&self) -> &Path {
129 &self.inner.configuration_directory
130 }
131
132 pub fn fixed_source_json(&self) -> &[u8] {
137 &self.inner.fixed_source
138 }
139
140 pub fn sweep_source_json(&self) -> &[u8] {
142 &self.inner.sweep_source
143 }
144
145 pub fn fixed_parameter_count(&self) -> usize {
147 self.inner.fixed.len()
148 }
149
150 pub fn sweep_parameter_count(&self) -> usize {
152 self.inner.sweep.key_count()
153 }
154
155 pub fn parameter_count(&self) -> usize {
157 self.fixed_parameter_count() + self.sweep_parameter_count()
158 }
159
160 pub fn task_count(&self) -> u64 {
162 self.inner.task_count
163 }
164
165 pub fn contains_parameter(&self, key: &str) -> bool {
168 self.inner.fixed_by_name.contains_key(key) || self.inner.sweep_by_name.contains_key(key)
169 }
170
171 pub fn fixed_keys(&self) -> impl ExactSizeIterator<Item = &str> {
173 self.inner.fixed.iter().map(|entry| entry.name.as_ref())
174 }
175
176 pub fn sweep_keys(&self) -> impl ExactSizeIterator<Item = &str> {
178 self.inner.sweep.keys()
179 }
180
181 pub fn task(&self, ordinal: u64) -> Result<TaskParameters, ConfigurationError> {
186 if ordinal >= self.task_count() {
187 return Err(ConfigurationError::TaskOrdinalOutOfBounds {
188 ordinal,
189 task_count: self.task_count(),
190 });
191 }
192 Ok(TaskParameters {
193 inner: Arc::clone(&self.inner),
194 ordinal,
195 })
196 }
197
198 pub fn tasks(&self) -> TaskParametersIter {
203 TaskParametersIter {
204 inner: Arc::clone(&self.inner),
205 next: 0,
206 end: self.task_count(),
207 }
208 }
209}
210
211impl fmt::Debug for ParameterSpace {
212 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 formatter
215 .debug_struct("ParameterSpace")
216 .field("configuration_directory", &self.configuration_directory())
217 .field("fixed_parameters", &self.fixed_parameter_count())
218 .field("sweep_parameters", &self.sweep_parameter_count())
219 .field("task_count", &self.task_count())
220 .finish_non_exhaustive()
221 }
222}
223
224#[derive(Clone)]
229pub struct TaskParameters {
230 inner: Arc<ParameterSpaceInner>,
231 ordinal: u64,
232}
233
234impl TaskParameters {
235 pub fn task_ordinal(&self) -> u64 {
237 self.ordinal
238 }
239
240 pub fn value(&self, key: &str) -> Option<&Value> {
244 if let Some((root, path)) = split_nested_key(key) {
245 if let Some(&position) = self.inner.fixed_by_name.get(root) {
246 return lookup_json_path(&self.inner.fixed[position].value, path);
247 }
248 if let Some(&position) = self.inner.sweep_by_name.get(root) {
249 return lookup_json_path(self.inner.sweep.value(self.ordinal, position), path);
250 }
251 }
252 if let Some(&position) = self.inner.fixed_by_name.get(key) {
253 return Some(&self.inner.fixed[position].value);
254 }
255 let &position = self.inner.sweep_by_name.get(key)?;
256 Some(self.inner.sweep.value(self.ordinal, position))
257 }
258
259 pub fn require_value(&self, key: &str) -> Result<&Value, ConfigurationError> {
261 self.value(key)
262 .ok_or_else(|| ConfigurationError::UnknownTaskParameter {
263 task_ordinal: self.ordinal,
264 key: key.to_owned(),
265 })
266 }
267
268 pub fn decode_value<T>(&self, key: &str) -> Result<T, ConfigurationError>
274 where
275 T: DeserializeOwned,
276 {
277 let value = self.require_value(key)?;
278 T::deserialize(value).map_err(|source| ConfigurationError::DecodeTaskParameter {
279 task_ordinal: self.ordinal,
280 key: key.to_owned(),
281 source,
282 })
283 }
284
285 pub fn decode_values<Values, Keys>(&self, keys: Keys) -> Result<Values, ConfigurationError>
291 where
292 Keys: ParameterKeyTuple<Values>,
293 {
294 keys.decode(self)
295 }
296
297 pub fn contains(&self, key: &str) -> bool {
299 self.inner.fixed_by_name.contains_key(key)
300 || self.inner.sweep_by_name.contains_key(key)
301 || split_nested_key(key).is_some_and(|(root, path)| {
302 self.inner.fixed_by_name.get(root).is_some_and(|&position| {
303 lookup_json_path(&self.inner.fixed[position].value, path).is_some()
304 }) || self.inner.sweep_by_name.get(root).is_some_and(|&position| {
305 lookup_json_path(self.inner.sweep.value(self.ordinal, position), path).is_some()
306 })
307 })
308 }
309
310 pub fn len(&self) -> usize {
312 self.inner.fixed.len() + self.inner.sweep.key_count()
313 }
314
315 pub fn is_empty(&self) -> bool {
317 self.len() == 0
318 }
319
320 pub fn keys(&self) -> impl Iterator<Item = &str> {
323 self.inner
324 .fixed
325 .iter()
326 .map(|entry| entry.name.as_ref())
327 .chain(self.inner.sweep.keys())
328 }
329
330 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
333 self.keys().map(|key| {
334 (
335 key,
336 self.value(key)
337 .expect("a key yielded by a validated parameter space must resolve"),
338 )
339 })
340 }
341
342 pub fn to_json(&self) -> Result<String, ConfigurationError> {
348 serde_json::to_string(&ResolvedTaskRef { task: self }).map_err(|source| {
349 ConfigurationError::SerializeTaskParameters {
350 task_ordinal: self.ordinal,
351 source,
352 }
353 })
354 }
355}
356
357fn split_nested_key(key: &str) -> Option<(&str, &str)> {
359 let (root, path) = key.split_once('.')?;
360 if root.is_empty() || path.is_empty() {
361 return None;
362 }
363 Some((root, path))
364}
365
366fn lookup_json_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
368 let mut current = value;
369 for segment in path.split('.') {
370 match current {
371 Value::Object(values) => current = values.get(segment)?,
372 _ => return None,
373 }
374 }
375 Some(current)
376}
377
378impl fmt::Debug for TaskParameters {
379 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381 formatter
382 .debug_struct("TaskParameters")
383 .field("task_ordinal", &self.ordinal)
384 .field("parameters", &self.len())
385 .finish_non_exhaustive()
386 }
387}
388
389#[derive(Clone)]
394pub struct TaskParametersIter {
395 inner: Arc<ParameterSpaceInner>,
396 next: u64,
397 end: u64,
398}
399
400impl Iterator for TaskParametersIter {
401 type Item = TaskParameters;
402
403 fn next(&mut self) -> Option<Self::Item> {
406 if self.next == self.end {
407 return None;
408 }
409 let ordinal = self.next;
410 self.next += 1;
411 Some(TaskParameters {
412 inner: Arc::clone(&self.inner),
413 ordinal,
414 })
415 }
416
417 fn size_hint(&self) -> (usize, Option<usize>) {
420 let remaining = self.end - self.next;
421 match usize::try_from(remaining) {
422 Ok(remaining) => (remaining, Some(remaining)),
423 Err(_) => (usize::MAX, None),
424 }
425 }
426}
427
428impl FusedIterator for TaskParametersIter {}
429
430impl fmt::Debug for TaskParametersIter {
431 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
433 formatter
434 .debug_struct("TaskParametersIter")
435 .field("next", &self.next)
436 .field("end", &self.end)
437 .finish_non_exhaustive()
438 }
439}
440
441struct ParameterSpaceInner {
443 configuration_directory: PathBuf,
444 fixed_source: Box<[u8]>,
445 sweep_source: Box<[u8]>,
446 fixed: Vec<NamedValue>,
447 fixed_by_name: HashMap<Box<str>, usize>,
448 sweep: SweepPlan,
449 sweep_by_name: HashMap<Box<str>, usize>,
450 task_count: u64,
451}
452
453struct NamedValue {
455 name: Box<str>,
456 value: Value,
457}
458
459enum SweepPlan {
461 Cartesian {
462 axes: Vec<SweepAxis>,
463 task_count: u64,
464 },
465 Cases {
466 keys: Vec<Box<str>>,
467 cases: Vec<Vec<Value>>,
468 },
469}
470
471impl SweepPlan {
472 fn keys(&self) -> impl ExactSizeIterator<Item = &str> {
474 let keys: &[Box<str>] = match self {
475 Self::Cartesian { axes, .. } => {
476 return SweepKeys::Axes(axes.iter()).map(SweepKey::into_str);
477 }
478 Self::Cases { keys, .. } => keys,
479 };
480 SweepKeys::Cases(keys.iter()).map(SweepKey::into_str)
481 }
482
483 fn key_count(&self) -> usize {
485 match self {
486 Self::Cartesian { axes, .. } => axes.len(),
487 Self::Cases { keys, .. } => keys.len(),
488 }
489 }
490
491 fn task_count(&self) -> u64 {
493 match self {
494 Self::Cartesian { task_count, .. } => *task_count,
495 Self::Cases { cases, .. } => {
496 u64::try_from(cases.len()).expect("validated explicit case count must fit in u64")
497 }
498 }
499 }
500
501 fn value(&self, task_ordinal: u64, key_position: usize) -> &Value {
504 match self {
505 Self::Cartesian { axes, .. } => {
506 let axis = &axes[key_position];
507 let axis_length = u64::try_from(axis.values.len())
508 .expect("validated Cartesian axis length must fit in u64");
509 let selected = (task_ordinal / axis.stride) % axis_length;
510 &axis.values[usize::try_from(selected)
511 .expect("selected Cartesian index originated from a usize length")]
512 }
513 Self::Cases { cases, .. } => {
514 &cases[usize::try_from(task_ordinal)
515 .expect("validated explicit task ordinal originated from a usize count")]
516 [key_position]
517 }
518 }
519 }
520}
521
522struct SweepAxis {
524 name: Box<str>,
525 values: Vec<Value>,
526 stride: u64,
527}
528
529enum SweepKey<'a> {
532 Axis(&'a SweepAxis),
533 Case(&'a str),
534}
535
536impl<'a> SweepKey<'a> {
537 fn into_str(self) -> &'a str {
538 match self {
539 Self::Axis(axis) => &axis.name,
540 Self::Case(key) => key,
541 }
542 }
543}
544
545enum SweepKeys<'a> {
547 Axes(std::slice::Iter<'a, SweepAxis>),
548 Cases(std::slice::Iter<'a, Box<str>>),
549}
550
551impl<'a> Iterator for SweepKeys<'a> {
552 type Item = SweepKey<'a>;
553
554 fn next(&mut self) -> Option<Self::Item> {
555 match self {
556 Self::Axes(iter) => iter.next().map(SweepKey::Axis),
557 Self::Cases(iter) => iter.next().map(|key| SweepKey::Case(key)),
558 }
559 }
560
561 fn size_hint(&self) -> (usize, Option<usize>) {
562 match self {
563 Self::Axes(iter) => iter.size_hint(),
564 Self::Cases(iter) => iter.size_hint(),
565 }
566 }
567}
568
569impl ExactSizeIterator for SweepKeys<'_> {}
570impl FusedIterator for SweepKeys<'_> {}
571
572struct ResolvedTaskRef<'a> {
574 task: &'a TaskParameters,
575}
576
577impl Serialize for ResolvedTaskRef<'_> {
578 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
579 where
580 S: Serializer,
581 {
582 let mut map = serializer.serialize_map(Some(self.task.len()))?;
583 for (key, value) in self.task.iter() {
584 map.serialize_entry(key, value)?;
585 }
586 map.end()
587 }
588}
589
590pub(super) enum StrictValue {
597 Null,
598 Bool(bool),
599 Number(Number),
600 String(String),
601 Array(Vec<Self>),
602 Object(Vec<(String, Self)>),
603}
604
605impl StrictValue {
606 fn into_json(self) -> Value {
608 match self {
609 Self::Null => Value::Null,
610 Self::Bool(value) => Value::Bool(value),
611 Self::Number(value) => Value::Number(value),
612 Self::String(value) => Value::String(value),
613 Self::Array(values) => {
614 Value::Array(values.into_iter().map(StrictValue::into_json).collect())
615 }
616 Self::Object(entries) => Value::Object(
617 entries
618 .into_iter()
619 .map(|(key, value)| (key, value.into_json()))
620 .collect(),
621 ),
622 }
623 }
624
625 fn duplicate_key(&self) -> Option<&str> {
627 match self {
628 Self::Array(values) => values.iter().find_map(StrictValue::duplicate_key),
629 Self::Object(entries) => {
630 let mut seen = HashSet::with_capacity(entries.len());
631 for (key, value) in entries {
632 if !seen.insert(key.as_str()) {
633 return Some(key);
634 }
635 if let Some(duplicate) = value.duplicate_key() {
636 return Some(duplicate);
637 }
638 }
639 None
640 }
641 Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_) => None,
642 }
643 }
644}
645
646impl<'de> Deserialize<'de> for StrictValue {
647 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
648 where
649 D: Deserializer<'de>,
650 {
651 deserializer.deserialize_any(StrictValueVisitor)
652 }
653}
654
655struct StrictValueVisitor;
657
658impl<'de> Visitor<'de> for StrictValueVisitor {
659 type Value = StrictValue;
660
661 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
662 formatter.write_str("any valid JSON value")
663 }
664
665 fn visit_unit<E>(self) -> Result<Self::Value, E> {
666 Ok(StrictValue::Null)
667 }
668
669 fn visit_none<E>(self) -> Result<Self::Value, E> {
670 Ok(StrictValue::Null)
671 }
672
673 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
674 where
675 D: Deserializer<'de>,
676 {
677 StrictValue::deserialize(deserializer)
678 }
679
680 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
681 Ok(StrictValue::Bool(value))
682 }
683
684 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
685 Ok(StrictValue::Number(Number::from(value)))
686 }
687
688 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
689 Ok(StrictValue::Number(Number::from(value)))
690 }
691
692 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
693 where
694 E: serde::de::Error,
695 {
696 Number::from_f64(value)
697 .map(StrictValue::Number)
698 .ok_or_else(|| E::custom("JSON numbers must be finite"))
699 }
700
701 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
702 where
703 E: serde::de::Error,
704 {
705 Ok(StrictValue::String(value.to_owned()))
706 }
707
708 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
709 Ok(StrictValue::String(value))
710 }
711
712 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
713 where
714 A: SeqAccess<'de>,
715 {
716 let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
717 while let Some(value) = sequence.next_element()? {
718 values.push(value);
719 }
720 Ok(StrictValue::Array(values))
721 }
722
723 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
724 where
725 A: MapAccess<'de>,
726 {
727 let mut entries = Vec::with_capacity(map.size_hint().unwrap_or(0));
728 while let Some((key, value)) = map.next_entry()? {
729 entries.push((key, value));
730 }
731 Ok(StrictValue::Object(entries))
732 }
733}
734
735pub(super) fn read_source(path: &Path) -> Result<Vec<u8>, ConfigurationError> {
737 fs::read(path).map_err(|source| ConfigurationError::ReadConfigurationFile {
738 path: path.to_path_buf(),
739 source,
740 })
741}
742
743pub(super) fn parse_strict_json(
745 path: &Path,
746 source: &[u8],
747) -> Result<StrictValue, ConfigurationError> {
748 let value: StrictValue = serde_json::from_slice(source).map_err(|source| {
749 ConfigurationError::ParseConfigurationFile {
750 path: path.to_path_buf(),
751 source,
752 }
753 })?;
754 if let Some(key) = value.duplicate_key() {
755 return Err(ConfigurationError::DuplicateConfigurationKey {
756 path: path.to_path_buf(),
757 key: key.to_owned(),
758 });
759 }
760 Ok(value)
761}
762
763fn parse_fixed(path: &Path, document: StrictValue) -> Result<Vec<NamedValue>, ConfigurationError> {
765 let entries = require_object(path, document, "fixed.json root must be an object")?;
766 entries
767 .into_iter()
768 .map(|(name, value)| {
769 validate_name(path, &name, "fixed parameter")?;
770 Ok(NamedValue {
771 name: name.into_boxed_str(),
772 value: value.into_json(),
773 })
774 })
775 .collect()
776}
777
778fn parse_sweep(path: &Path, document: StrictValue) -> Result<SweepPlan, ConfigurationError> {
780 let mut root = require_object(path, document, "sweep.json root must be an object")?;
781 let mode = take_required(path, &mut root, "mode")?;
782 let StrictValue::String(mode) = mode else {
783 return invalid(path, "sweep field `mode` must be a string");
784 };
785 match mode.as_str() {
786 "cartesian" => {
787 let axes = take_required(path, &mut root, "axes")?;
788 reject_remaining(path, &root, "cartesian sweep")?;
789 parse_cartesian(path, axes)
790 }
791 "cases" => {
792 let cases = take_required(path, &mut root, "cases")?;
793 reject_remaining(path, &root, "explicit-case sweep")?;
794 parse_cases(path, cases)
795 }
796 _ => invalid(
797 path,
798 format!("unsupported sweep mode `{mode}`; expected `cartesian` or `cases`"),
799 ),
800 }
801}
802
803fn parse_cartesian(path: &Path, axes: StrictValue) -> Result<SweepPlan, ConfigurationError> {
805 let StrictValue::Array(axis_documents) = axes else {
806 return invalid(path, "cartesian sweep field `axes` must be an array");
807 };
808 let mut parsed = Vec::with_capacity(axis_documents.len());
809 let mut names = HashSet::with_capacity(axis_documents.len());
810 for (position, document) in axis_documents.into_iter().enumerate() {
811 let mut axis = require_object(
812 path,
813 document,
814 format!("cartesian axis at position {position} must be an object"),
815 )?;
816 let name = take_required(path, &mut axis, "name")?;
817 let StrictValue::String(name) = name else {
818 return invalid(
819 path,
820 format!("cartesian axis at position {position} has a non-string `name`"),
821 );
822 };
823 validate_name(path, &name, "sweep axis")?;
824 if !names.insert(name.clone()) {
825 return invalid(
826 path,
827 format!("sweep axis name `{name}` is declared more than once"),
828 );
829 }
830 let values = take_required(path, &mut axis, "values")?;
831 let StrictValue::Array(values) = values else {
832 return invalid(
833 path,
834 format!("cartesian axis `{name}` field `values` must be an array"),
835 );
836 };
837 if values.is_empty() {
838 return invalid(path, format!("cartesian axis `{name}` has no candidates"));
839 }
840 reject_remaining(path, &axis, &format!("cartesian axis `{name}`"))?;
841 parsed.push(SweepAxis {
842 name: name.into_boxed_str(),
843 values: values.into_iter().map(StrictValue::into_json).collect(),
844 stride: 0,
845 });
846 }
847
848 let mut task_count = 1_u64;
849 for axis in parsed.iter_mut().rev() {
850 axis.stride = task_count;
851 let length = u64::try_from(axis.values.len()).map_err(|_| {
852 ConfigurationError::TaskCountOverflow {
853 axis: axis.name.to_string(),
854 }
855 })?;
856 task_count = task_count.checked_mul(length).ok_or_else(|| {
857 ConfigurationError::TaskCountOverflow {
858 axis: axis.name.to_string(),
859 }
860 })?;
861 }
862 Ok(SweepPlan::Cartesian {
863 axes: parsed,
864 task_count,
865 })
866}
867
868fn parse_cases(path: &Path, cases: StrictValue) -> Result<SweepPlan, ConfigurationError> {
870 let StrictValue::Array(case_documents) = cases else {
871 return invalid(path, "explicit sweep field `cases` must be an array");
872 };
873 if case_documents.is_empty() {
874 return invalid(path, "explicit sweep must contain at least one case");
875 }
876
877 let mut case_iter = case_documents.into_iter().enumerate();
878 let (_, first) = case_iter
879 .next()
880 .expect("an explicitly non-empty case array has a first item");
881 let first = require_object(path, first, "explicit case at position 0 must be an object")?;
882 if first.is_empty() {
883 return invalid(
884 path,
885 "explicit sweep cases must contain at least one parameter",
886 );
887 }
888 let mut keys = Vec::with_capacity(first.len());
889 let mut first_values = Vec::with_capacity(first.len());
890 for (name, value) in first {
891 validate_name(path, &name, "explicit-case parameter")?;
892 keys.push(name.into_boxed_str());
893 first_values.push(value.into_json());
894 }
895
896 let expected = keys.iter().map(AsRef::as_ref).collect::<HashSet<&str>>();
897 let mut parsed_cases = Vec::with_capacity(case_iter.size_hint().0 + 1);
898 parsed_cases.push(first_values);
899 for (position, document) in case_iter {
900 let entries = require_object(
901 path,
902 document,
903 format!("explicit case at position {position} must be an object"),
904 )?;
905 let actual = entries
906 .iter()
907 .map(|(name, _)| name.as_str())
908 .collect::<HashSet<_>>();
909 if actual != expected {
910 return invalid(
911 path,
912 format!(
913 "explicit case at position {position} does not contain the same key set as case 0"
914 ),
915 );
916 }
917 let mut by_name = entries.into_iter().collect::<HashMap<_, _>>();
918 parsed_cases.push(
919 keys.iter()
920 .map(|key| {
921 by_name
922 .remove(key.as_ref())
923 .expect("validated explicit case contains every first-case key")
924 .into_json()
925 })
926 .collect(),
927 );
928 }
929 let _ =
930 u64::try_from(parsed_cases.len()).map_err(|_| ConfigurationError::TaskCountOverflow {
931 axis: "explicit cases".to_owned(),
932 })?;
933 Ok(SweepPlan::Cases {
934 keys,
935 cases: parsed_cases,
936 })
937}
938
939fn validate_disjoint(
941 fixed_path: &Path,
942 sweep_path: &Path,
943 fixed: &[NamedValue],
944 sweep: &SweepPlan,
945) -> Result<(), ConfigurationError> {
946 let fixed_names = fixed
947 .iter()
948 .map(|entry| entry.name.as_ref())
949 .collect::<HashSet<&str>>();
950 if let Some(key) = sweep.keys().find(|key| fixed_names.contains(key)) {
951 return Err(ConfigurationError::FixedSweepKeyConflict {
952 key: key.to_owned(),
953 fixed_path: fixed_path.to_path_buf(),
954 sweep_path: sweep_path.to_path_buf(),
955 });
956 }
957 Ok(())
958}
959
960pub(super) fn require_object(
962 path: &Path,
963 value: StrictValue,
964 reason: impl Into<String>,
965) -> Result<Vec<(String, StrictValue)>, ConfigurationError> {
966 match value {
967 StrictValue::Object(entries) => Ok(entries),
968 _ => invalid(path, reason),
969 }
970}
971
972fn take_required(
974 path: &Path,
975 entries: &mut Vec<(String, StrictValue)>,
976 name: &str,
977) -> Result<StrictValue, ConfigurationError> {
978 let Some(position) = entries.iter().position(|(key, _)| key == name) else {
979 return invalid(path, format!("required field `{name}` is missing"));
980 };
981 Ok(entries.remove(position).1)
982}
983
984fn reject_remaining(
986 path: &Path,
987 entries: &[(String, StrictValue)],
988 context: &str,
989) -> Result<(), ConfigurationError> {
990 if let Some((name, _)) = entries.first() {
991 return invalid(path, format!("{context} contains unknown field `{name}`"));
992 }
993 Ok(())
994}
995
996pub(super) fn validate_name(path: &Path, name: &str, kind: &str) -> Result<(), ConfigurationError> {
998 if name.trim().is_empty() {
999 return invalid(
1000 path,
1001 format!("{kind} name must not be empty or whitespace-only"),
1002 );
1003 }
1004 Ok(())
1005}
1006
1007pub(super) fn invalid<T>(path: &Path, reason: impl Into<String>) -> Result<T, ConfigurationError> {
1009 Err(ConfigurationError::InvalidConfigurationDocument {
1010 path: path.to_path_buf(),
1011 reason: reason.into(),
1012 })
1013}