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