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