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::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 contains(&self, key: &str) -> bool {
287 self.inner.fixed_by_name.contains_key(key) || self.inner.sweep_by_name.contains_key(key)
288 || split_nested_key(key).is_some_and(|(root, path)| {
289 self.inner.fixed_by_name.get(root).is_some_and(|&position| {
290 lookup_json_path(&self.inner.fixed[position].value, path).is_some()
291 }) || self.inner.sweep_by_name.get(root).is_some_and(|&position| {
292 lookup_json_path(self.inner.sweep.value(self.ordinal, position), path).is_some()
293 })
294 })
295 }
296
297 pub fn len(&self) -> usize {
299 self.inner.fixed.len() + self.inner.sweep.key_count()
300 }
301
302 pub fn is_empty(&self) -> bool {
304 self.len() == 0
305 }
306
307 pub fn keys(&self) -> impl Iterator<Item = &str> {
310 self.inner
311 .fixed
312 .iter()
313 .map(|entry| entry.name.as_ref())
314 .chain(self.inner.sweep.keys())
315 }
316
317 pub fn iter(&self) -> impl Iterator<Item = (&str, &Value)> {
320 self.keys().map(|key| {
321 (
322 key,
323 self.value(key)
324 .expect("a key yielded by a validated parameter space must resolve"),
325 )
326 })
327 }
328
329 pub fn to_json(&self) -> Result<String, ConfigurationError> {
335 serde_json::to_string(&ResolvedTaskRef { task: self }).map_err(|source| {
336 ConfigurationError::SerializeTaskParameters {
337 task_ordinal: self.ordinal,
338 source,
339 }
340 })
341 }
342}
343
344fn split_nested_key(key: &str) -> Option<(&str, &str)> {
346 let (root, path) = key.split_once('.')?;
347 if root.is_empty() || path.is_empty() {
348 return None;
349 }
350 Some((root, path))
351}
352
353fn lookup_json_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
355 let mut current = value;
356 for segment in path.split('.') {
357 match current {
358 Value::Object(values) => current = values.get(segment)?,
359 _ => return None,
360 }
361 }
362 Some(current)
363}
364
365impl fmt::Debug for TaskParameters {
366 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
368 formatter
369 .debug_struct("TaskParameters")
370 .field("task_ordinal", &self.ordinal)
371 .field("parameters", &self.len())
372 .finish_non_exhaustive()
373 }
374}
375
376#[derive(Clone)]
381pub struct TaskParametersIter {
382 inner: Arc<ParameterSpaceInner>,
383 next: u64,
384 end: u64,
385}
386
387impl Iterator for TaskParametersIter {
388 type Item = TaskParameters;
389
390 fn next(&mut self) -> Option<Self::Item> {
393 if self.next == self.end {
394 return None;
395 }
396 let ordinal = self.next;
397 self.next += 1;
398 Some(TaskParameters {
399 inner: Arc::clone(&self.inner),
400 ordinal,
401 })
402 }
403
404 fn size_hint(&self) -> (usize, Option<usize>) {
407 let remaining = self.end - self.next;
408 match usize::try_from(remaining) {
409 Ok(remaining) => (remaining, Some(remaining)),
410 Err(_) => (usize::MAX, None),
411 }
412 }
413}
414
415impl FusedIterator for TaskParametersIter {}
416
417impl fmt::Debug for TaskParametersIter {
418 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
420 formatter
421 .debug_struct("TaskParametersIter")
422 .field("next", &self.next)
423 .field("end", &self.end)
424 .finish_non_exhaustive()
425 }
426}
427
428struct ParameterSpaceInner {
430 configuration_directory: PathBuf,
431 fixed_source: Box<[u8]>,
432 sweep_source: Box<[u8]>,
433 fixed: Vec<NamedValue>,
434 fixed_by_name: HashMap<Box<str>, usize>,
435 sweep: SweepPlan,
436 sweep_by_name: HashMap<Box<str>, usize>,
437 task_count: u64,
438}
439
440struct NamedValue {
442 name: Box<str>,
443 value: Value,
444}
445
446enum SweepPlan {
448 Cartesian {
449 axes: Vec<SweepAxis>,
450 task_count: u64,
451 },
452 Cases {
453 keys: Vec<Box<str>>,
454 cases: Vec<Vec<Value>>,
455 },
456}
457
458impl SweepPlan {
459 fn keys(&self) -> impl ExactSizeIterator<Item = &str> {
461 let keys: &[Box<str>] = match self {
462 Self::Cartesian { axes, .. } => {
463 return SweepKeys::Axes(axes.iter()).map(SweepKey::into_str);
464 }
465 Self::Cases { keys, .. } => keys,
466 };
467 SweepKeys::Cases(keys.iter()).map(SweepKey::into_str)
468 }
469
470 fn key_count(&self) -> usize {
472 match self {
473 Self::Cartesian { axes, .. } => axes.len(),
474 Self::Cases { keys, .. } => keys.len(),
475 }
476 }
477
478 fn task_count(&self) -> u64 {
480 match self {
481 Self::Cartesian { task_count, .. } => *task_count,
482 Self::Cases { cases, .. } => {
483 u64::try_from(cases.len()).expect("validated explicit case count must fit in u64")
484 }
485 }
486 }
487
488 fn value(&self, task_ordinal: u64, key_position: usize) -> &Value {
491 match self {
492 Self::Cartesian { axes, .. } => {
493 let axis = &axes[key_position];
494 let axis_length = u64::try_from(axis.values.len())
495 .expect("validated Cartesian axis length must fit in u64");
496 let selected = (task_ordinal / axis.stride) % axis_length;
497 &axis.values[usize::try_from(selected)
498 .expect("selected Cartesian index originated from a usize length")]
499 }
500 Self::Cases { cases, .. } => {
501 &cases[usize::try_from(task_ordinal)
502 .expect("validated explicit task ordinal originated from a usize count")]
503 [key_position]
504 }
505 }
506 }
507}
508
509struct SweepAxis {
511 name: Box<str>,
512 values: Vec<Value>,
513 stride: u64,
514}
515
516enum SweepKey<'a> {
519 Axis(&'a SweepAxis),
520 Case(&'a str),
521}
522
523impl<'a> SweepKey<'a> {
524 fn into_str(self) -> &'a str {
525 match self {
526 Self::Axis(axis) => &axis.name,
527 Self::Case(key) => key,
528 }
529 }
530}
531
532enum SweepKeys<'a> {
534 Axes(std::slice::Iter<'a, SweepAxis>),
535 Cases(std::slice::Iter<'a, Box<str>>),
536}
537
538impl<'a> Iterator for SweepKeys<'a> {
539 type Item = SweepKey<'a>;
540
541 fn next(&mut self) -> Option<Self::Item> {
542 match self {
543 Self::Axes(iter) => iter.next().map(SweepKey::Axis),
544 Self::Cases(iter) => iter.next().map(|key| SweepKey::Case(key)),
545 }
546 }
547
548 fn size_hint(&self) -> (usize, Option<usize>) {
549 match self {
550 Self::Axes(iter) => iter.size_hint(),
551 Self::Cases(iter) => iter.size_hint(),
552 }
553 }
554}
555
556impl ExactSizeIterator for SweepKeys<'_> {}
557impl FusedIterator for SweepKeys<'_> {}
558
559struct ResolvedTaskRef<'a> {
561 task: &'a TaskParameters,
562}
563
564impl Serialize for ResolvedTaskRef<'_> {
565 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
566 where
567 S: Serializer,
568 {
569 let mut map = serializer.serialize_map(Some(self.task.len()))?;
570 for (key, value) in self.task.iter() {
571 map.serialize_entry(key, value)?;
572 }
573 map.end()
574 }
575}
576
577pub(super) enum StrictValue {
584 Null,
585 Bool(bool),
586 Number(Number),
587 String(String),
588 Array(Vec<Self>),
589 Object(Vec<(String, Self)>),
590}
591
592impl StrictValue {
593 fn into_json(self) -> Value {
595 match self {
596 Self::Null => Value::Null,
597 Self::Bool(value) => Value::Bool(value),
598 Self::Number(value) => Value::Number(value),
599 Self::String(value) => Value::String(value),
600 Self::Array(values) => {
601 Value::Array(values.into_iter().map(StrictValue::into_json).collect())
602 }
603 Self::Object(entries) => Value::Object(
604 entries
605 .into_iter()
606 .map(|(key, value)| (key, value.into_json()))
607 .collect(),
608 ),
609 }
610 }
611
612 fn duplicate_key(&self) -> Option<&str> {
614 match self {
615 Self::Array(values) => values.iter().find_map(StrictValue::duplicate_key),
616 Self::Object(entries) => {
617 let mut seen = HashSet::with_capacity(entries.len());
618 for (key, value) in entries {
619 if !seen.insert(key.as_str()) {
620 return Some(key);
621 }
622 if let Some(duplicate) = value.duplicate_key() {
623 return Some(duplicate);
624 }
625 }
626 None
627 }
628 Self::Null | Self::Bool(_) | Self::Number(_) | Self::String(_) => None,
629 }
630 }
631}
632
633impl<'de> Deserialize<'de> for StrictValue {
634 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
635 where
636 D: Deserializer<'de>,
637 {
638 deserializer.deserialize_any(StrictValueVisitor)
639 }
640}
641
642struct StrictValueVisitor;
644
645impl<'de> Visitor<'de> for StrictValueVisitor {
646 type Value = StrictValue;
647
648 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
649 formatter.write_str("any valid JSON value")
650 }
651
652 fn visit_unit<E>(self) -> Result<Self::Value, E> {
653 Ok(StrictValue::Null)
654 }
655
656 fn visit_none<E>(self) -> Result<Self::Value, E> {
657 Ok(StrictValue::Null)
658 }
659
660 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
661 where
662 D: Deserializer<'de>,
663 {
664 StrictValue::deserialize(deserializer)
665 }
666
667 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
668 Ok(StrictValue::Bool(value))
669 }
670
671 fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
672 Ok(StrictValue::Number(Number::from(value)))
673 }
674
675 fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
676 Ok(StrictValue::Number(Number::from(value)))
677 }
678
679 fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
680 where
681 E: serde::de::Error,
682 {
683 Number::from_f64(value)
684 .map(StrictValue::Number)
685 .ok_or_else(|| E::custom("JSON numbers must be finite"))
686 }
687
688 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
689 where
690 E: serde::de::Error,
691 {
692 Ok(StrictValue::String(value.to_owned()))
693 }
694
695 fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
696 Ok(StrictValue::String(value))
697 }
698
699 fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
700 where
701 A: SeqAccess<'de>,
702 {
703 let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
704 while let Some(value) = sequence.next_element()? {
705 values.push(value);
706 }
707 Ok(StrictValue::Array(values))
708 }
709
710 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
711 where
712 A: MapAccess<'de>,
713 {
714 let mut entries = Vec::with_capacity(map.size_hint().unwrap_or(0));
715 while let Some((key, value)) = map.next_entry()? {
716 entries.push((key, value));
717 }
718 Ok(StrictValue::Object(entries))
719 }
720}
721
722pub(super) fn read_source(path: &Path) -> Result<Vec<u8>, ConfigurationError> {
724 fs::read(path).map_err(|source| ConfigurationError::ReadConfigurationFile {
725 path: path.to_path_buf(),
726 source,
727 })
728}
729
730pub(super) fn parse_strict_json(
732 path: &Path,
733 source: &[u8],
734) -> Result<StrictValue, ConfigurationError> {
735 let value: StrictValue = serde_json::from_slice(source).map_err(|source| {
736 ConfigurationError::ParseConfigurationFile {
737 path: path.to_path_buf(),
738 source,
739 }
740 })?;
741 if let Some(key) = value.duplicate_key() {
742 return Err(ConfigurationError::DuplicateConfigurationKey {
743 path: path.to_path_buf(),
744 key: key.to_owned(),
745 });
746 }
747 Ok(value)
748}
749
750fn parse_fixed(path: &Path, document: StrictValue) -> Result<Vec<NamedValue>, ConfigurationError> {
752 let entries = require_object(path, document, "fixed.json root must be an object")?;
753 entries
754 .into_iter()
755 .map(|(name, value)| {
756 validate_name(path, &name, "fixed parameter")?;
757 Ok(NamedValue {
758 name: name.into_boxed_str(),
759 value: value.into_json(),
760 })
761 })
762 .collect()
763}
764
765fn parse_sweep(path: &Path, document: StrictValue) -> Result<SweepPlan, ConfigurationError> {
767 let mut root = require_object(path, document, "sweep.json root must be an object")?;
768 let mode = take_required(path, &mut root, "mode")?;
769 let StrictValue::String(mode) = mode else {
770 return invalid(path, "sweep field `mode` must be a string");
771 };
772 match mode.as_str() {
773 "cartesian" => {
774 let axes = take_required(path, &mut root, "axes")?;
775 reject_remaining(path, &root, "cartesian sweep")?;
776 parse_cartesian(path, axes)
777 }
778 "cases" => {
779 let cases = take_required(path, &mut root, "cases")?;
780 reject_remaining(path, &root, "explicit-case sweep")?;
781 parse_cases(path, cases)
782 }
783 _ => invalid(
784 path,
785 format!("unsupported sweep mode `{mode}`; expected `cartesian` or `cases`"),
786 ),
787 }
788}
789
790fn parse_cartesian(path: &Path, axes: StrictValue) -> Result<SweepPlan, ConfigurationError> {
792 let StrictValue::Array(axis_documents) = axes else {
793 return invalid(path, "cartesian sweep field `axes` must be an array");
794 };
795 let mut parsed = Vec::with_capacity(axis_documents.len());
796 let mut names = HashSet::with_capacity(axis_documents.len());
797 for (position, document) in axis_documents.into_iter().enumerate() {
798 let mut axis = require_object(
799 path,
800 document,
801 format!("cartesian axis at position {position} must be an object"),
802 )?;
803 let name = take_required(path, &mut axis, "name")?;
804 let StrictValue::String(name) = name else {
805 return invalid(
806 path,
807 format!("cartesian axis at position {position} has a non-string `name`"),
808 );
809 };
810 validate_name(path, &name, "sweep axis")?;
811 if !names.insert(name.clone()) {
812 return invalid(
813 path,
814 format!("sweep axis name `{name}` is declared more than once"),
815 );
816 }
817 let values = take_required(path, &mut axis, "values")?;
818 let StrictValue::Array(values) = values else {
819 return invalid(
820 path,
821 format!("cartesian axis `{name}` field `values` must be an array"),
822 );
823 };
824 if values.is_empty() {
825 return invalid(path, format!("cartesian axis `{name}` has no candidates"));
826 }
827 reject_remaining(path, &axis, &format!("cartesian axis `{name}`"))?;
828 parsed.push(SweepAxis {
829 name: name.into_boxed_str(),
830 values: values.into_iter().map(StrictValue::into_json).collect(),
831 stride: 0,
832 });
833 }
834
835 let mut task_count = 1_u64;
836 for axis in parsed.iter_mut().rev() {
837 axis.stride = task_count;
838 let length = u64::try_from(axis.values.len()).map_err(|_| {
839 ConfigurationError::TaskCountOverflow {
840 axis: axis.name.to_string(),
841 }
842 })?;
843 task_count = task_count.checked_mul(length).ok_or_else(|| {
844 ConfigurationError::TaskCountOverflow {
845 axis: axis.name.to_string(),
846 }
847 })?;
848 }
849 Ok(SweepPlan::Cartesian {
850 axes: parsed,
851 task_count,
852 })
853}
854
855fn parse_cases(path: &Path, cases: StrictValue) -> Result<SweepPlan, ConfigurationError> {
857 let StrictValue::Array(case_documents) = cases else {
858 return invalid(path, "explicit sweep field `cases` must be an array");
859 };
860 if case_documents.is_empty() {
861 return invalid(path, "explicit sweep must contain at least one case");
862 }
863
864 let mut case_iter = case_documents.into_iter().enumerate();
865 let (_, first) = case_iter
866 .next()
867 .expect("an explicitly non-empty case array has a first item");
868 let first = require_object(path, first, "explicit case at position 0 must be an object")?;
869 if first.is_empty() {
870 return invalid(
871 path,
872 "explicit sweep cases must contain at least one parameter",
873 );
874 }
875 let mut keys = Vec::with_capacity(first.len());
876 let mut first_values = Vec::with_capacity(first.len());
877 for (name, value) in first {
878 validate_name(path, &name, "explicit-case parameter")?;
879 keys.push(name.into_boxed_str());
880 first_values.push(value.into_json());
881 }
882
883 let expected = keys.iter().map(AsRef::as_ref).collect::<HashSet<&str>>();
884 let mut parsed_cases = Vec::with_capacity(case_iter.size_hint().0 + 1);
885 parsed_cases.push(first_values);
886 for (position, document) in case_iter {
887 let entries = require_object(
888 path,
889 document,
890 format!("explicit case at position {position} must be an object"),
891 )?;
892 let actual = entries
893 .iter()
894 .map(|(name, _)| name.as_str())
895 .collect::<HashSet<_>>();
896 if actual != expected {
897 return invalid(
898 path,
899 format!(
900 "explicit case at position {position} does not contain the same key set as case 0"
901 ),
902 );
903 }
904 let mut by_name = entries.into_iter().collect::<HashMap<_, _>>();
905 parsed_cases.push(
906 keys.iter()
907 .map(|key| {
908 by_name
909 .remove(key.as_ref())
910 .expect("validated explicit case contains every first-case key")
911 .into_json()
912 })
913 .collect(),
914 );
915 }
916 let _ =
917 u64::try_from(parsed_cases.len()).map_err(|_| ConfigurationError::TaskCountOverflow {
918 axis: "explicit cases".to_owned(),
919 })?;
920 Ok(SweepPlan::Cases {
921 keys,
922 cases: parsed_cases,
923 })
924}
925
926fn validate_disjoint(
928 fixed_path: &Path,
929 sweep_path: &Path,
930 fixed: &[NamedValue],
931 sweep: &SweepPlan,
932) -> Result<(), ConfigurationError> {
933 let fixed_names = fixed
934 .iter()
935 .map(|entry| entry.name.as_ref())
936 .collect::<HashSet<&str>>();
937 if let Some(key) = sweep.keys().find(|key| fixed_names.contains(key)) {
938 return Err(ConfigurationError::FixedSweepKeyConflict {
939 key: key.to_owned(),
940 fixed_path: fixed_path.to_path_buf(),
941 sweep_path: sweep_path.to_path_buf(),
942 });
943 }
944 Ok(())
945}
946
947pub(super) fn require_object(
949 path: &Path,
950 value: StrictValue,
951 reason: impl Into<String>,
952) -> Result<Vec<(String, StrictValue)>, ConfigurationError> {
953 match value {
954 StrictValue::Object(entries) => Ok(entries),
955 _ => invalid(path, reason),
956 }
957}
958
959fn take_required(
961 path: &Path,
962 entries: &mut Vec<(String, StrictValue)>,
963 name: &str,
964) -> Result<StrictValue, ConfigurationError> {
965 let Some(position) = entries.iter().position(|(key, _)| key == name) else {
966 return invalid(path, format!("required field `{name}` is missing"));
967 };
968 Ok(entries.remove(position).1)
969}
970
971fn reject_remaining(
973 path: &Path,
974 entries: &[(String, StrictValue)],
975 context: &str,
976) -> Result<(), ConfigurationError> {
977 if let Some((name, _)) = entries.first() {
978 return invalid(path, format!("{context} contains unknown field `{name}`"));
979 }
980 Ok(())
981}
982
983pub(super) fn validate_name(path: &Path, name: &str, kind: &str) -> Result<(), ConfigurationError> {
985 if name.trim().is_empty() {
986 return invalid(
987 path,
988 format!("{kind} name must not be empty or whitespace-only"),
989 );
990 }
991 Ok(())
992}
993
994pub(super) fn invalid<T>(path: &Path, reason: impl Into<String>) -> Result<T, ConfigurationError> {
996 Err(ConfigurationError::InvalidConfigurationDocument {
997 path: path.to_path_buf(),
998 reason: reason.into(),
999 })
1000}