1use std::collections::BTreeSet;
30use std::sync::Arc;
31
32use indexmap::IndexSet;
33use rand::Rng;
34use rand::seq::IteratorRandom;
35use regex::Regex;
36use serde::{Deserialize, Serialize};
37
38use crate::error::{Error, Result};
39use crate::names::{NameError, ParameterName};
40use crate::value::{SelectionItem, Value};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(tag = "kind", rename_all = "snake_case")]
54pub enum Cardinality {
55 Finite {
58 count: u64,
60 },
61 Unbounded,
63}
64
65impl Cardinality {
66 #[must_use]
68 pub const fn finite(count: u64) -> Self {
69 Self::Finite { count }
70 }
71}
72
73#[derive(Debug, thiserror::Error, PartialEq, Eq)]
79pub enum DomainError {
80 #[error("domain is not enumerable")]
83 NotEnumerable,
84
85 #[error("invalid range: min={min}, max={max}")]
88 InvalidRange {
89 min: String,
91 max: String,
93 },
94
95 #[error("discrete integer domain must contain at least one value")]
97 EmptyDiscrete,
98
99 #[error("fixed selection domain must contain at least one value")]
101 EmptySelection,
102
103 #[error("selection domain max_selections must be at least 1")]
105 ZeroMaxSelections,
106}
107
108#[derive(Debug, Clone)]
120pub struct RegexPattern {
121 source: String,
122 compiled: Regex,
123}
124
125impl RegexPattern {
126 pub fn new(source: impl Into<String>) -> Result<Self, regex::Error> {
128 let source = source.into();
129 let compiled = Regex::new(&source)?;
130 Ok(Self { source, compiled })
131 }
132
133 #[must_use]
135 pub fn as_str(&self) -> &str {
136 &self.source
137 }
138
139 #[must_use]
141 pub fn is_match(&self, text: &str) -> bool {
142 self.compiled.is_match(text)
143 }
144}
145
146impl PartialEq for RegexPattern {
147 fn eq(&self, other: &Self) -> bool {
148 self.source == other.source
149 }
150}
151impl Eq for RegexPattern {}
152
153impl std::hash::Hash for RegexPattern {
154 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
155 self.source.hash(state);
156 }
157}
158
159impl Serialize for RegexPattern {
160 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
161 s.serialize_str(&self.source)
162 }
163}
164
165impl<'de> Deserialize<'de> for RegexPattern {
166 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
167 let source = String::deserialize(d)?;
168 Self::new(source).map_err(serde::de::Error::custom)
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
182pub struct ResolverId(String);
183
184const RESOLVER_ID_MAX: usize = 64;
185
186impl ResolverId {
187 pub fn new(candidate: impl Into<String>) -> std::result::Result<Self, NameError> {
189 let s = candidate.into();
190 if s.is_empty() {
191 return Err(NameError::Empty);
192 }
193 if s.len() > RESOLVER_ID_MAX {
194 return Err(NameError::TooLong {
195 length: s.len(),
196 max: RESOLVER_ID_MAX,
197 });
198 }
199 let first = s.chars().next().expect("non-empty");
200 if !(first.is_ascii_alphabetic() || first == '_') {
201 return Err(NameError::BadStart { ch: first });
202 }
203 for (offset, ch) in s.char_indices() {
204 if !(ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')) {
205 return Err(NameError::InvalidChar { ch, offset });
206 }
207 }
208 Ok(Self(s))
209 }
210
211 #[must_use]
213 pub fn as_str(&self) -> &str {
214 &self.0
215 }
216}
217
218impl std::fmt::Display for ResolverId {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 f.write_str(&self.0)
221 }
222}
223
224impl Serialize for ResolverId {
225 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
226 s.serialize_str(&self.0)
227 }
228}
229
230impl<'de> Deserialize<'de> for ResolverId {
231 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> std::result::Result<Self, D::Error> {
232 let s = String::deserialize(d)?;
233 Self::new(s).map_err(serde::de::Error::custom)
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
244#[serde(tag = "shape", rename_all = "snake_case")]
245pub enum IntegerDomain {
246 Range {
248 min: i64,
250 max: i64,
252 },
253 Discrete {
255 values: BTreeSet<i64>,
257 },
258}
259
260impl IntegerDomain {
261 pub fn range(min: i64, max: i64) -> Result<Self> {
263 if min > max {
264 return Err(Error::Domain(DomainError::InvalidRange {
265 min: min.to_string(),
266 max: max.to_string(),
267 }));
268 }
269 Ok(Self::Range { min, max })
270 }
271
272 pub fn discrete(values: BTreeSet<i64>) -> Result<Self> {
274 if values.is_empty() {
275 return Err(Error::Domain(DomainError::EmptyDiscrete));
276 }
277 Ok(Self::Discrete { values })
278 }
279
280 #[must_use]
282 pub fn contains_native(&self, value: i64) -> bool {
283 match self {
284 Self::Range { min, max } => value >= *min && value <= *max,
285 Self::Discrete { values } => values.contains(&value),
286 }
287 }
288
289 #[must_use]
292 pub fn cardinality(&self) -> Cardinality {
293 match self {
294 Self::Range { min, max } => {
295 let min = i128::from(*min);
296 let max = i128::from(*max);
297 let width = max - min + 1;
298 let count = u64::try_from(width).unwrap_or(u64::MAX);
299 Cardinality::finite(count)
300 }
301 Self::Discrete { values } => Cardinality::finite(values.len() as u64),
302 }
303 }
304
305 #[must_use]
307 pub fn boundaries_native(&self) -> Vec<i64> {
308 match self {
309 Self::Range { min, max } => {
310 if min == max {
311 vec![*min]
312 } else {
313 vec![*min, *max]
314 }
315 }
316 Self::Discrete { values } => {
317 let mut out = Vec::with_capacity(2);
318 if let Some(v) = values.iter().next() {
319 out.push(*v);
320 }
321 if let Some(v) = values.iter().next_back()
322 && out.last() != Some(v)
323 {
324 out.push(*v);
325 }
326 out
327 }
328 }
329 }
330
331 pub fn sample_native<R: Rng + ?Sized>(&self, rng: &mut R) -> i64 {
333 match self {
334 Self::Range { min, max } => rng.gen_range(*min..=*max),
335 Self::Discrete { values } => {
336 let idx = rng.gen_range(0..values.len());
337 *values.iter().nth(idx).expect("idx < len")
338 }
339 }
340 }
341
342 #[must_use]
344 pub fn iter_native<'a>(&'a self) -> Box<dyn Iterator<Item = i64> + 'a> {
345 match self {
346 Self::Range { min, max } => Box::new(*min..=*max),
347 Self::Discrete { values } => Box::new(values.iter().copied()),
348 }
349 }
350}
351
352#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
359#[serde(tag = "shape", rename_all = "snake_case")]
360pub enum DoubleDomain {
361 Range {
363 min: f64,
365 max: f64,
367 },
368}
369
370impl DoubleDomain {
371 pub fn range(min: f64, max: f64) -> Result<Self> {
374 if !min.is_finite() || !max.is_finite() || min > max {
375 return Err(Error::Domain(DomainError::InvalidRange {
376 min: format!("{min}"),
377 max: format!("{max}"),
378 }));
379 }
380 Ok(Self::Range { min, max })
381 }
382
383 #[must_use]
385 pub fn contains_native(&self, value: f64) -> bool {
386 if value.is_nan() {
387 return false;
388 }
389 match self {
390 Self::Range { min, max } => value >= *min && value <= *max,
391 }
392 }
393
394 #[must_use]
396 pub const fn cardinality(&self) -> Cardinality {
397 Cardinality::Unbounded
398 }
399
400 #[must_use]
402 #[allow(clippy::float_cmp, reason = "exact equality here detects a single-point range")]
403 pub fn boundaries_native(&self) -> Vec<f64> {
404 match self {
405 Self::Range { min, max } => {
406 if min == max {
407 vec![*min]
408 } else {
409 vec![*min, *max]
410 }
411 }
412 }
413 }
414
415 pub fn sample_native<R: Rng + ?Sized>(&self, rng: &mut R) -> f64 {
417 match self {
418 Self::Range { min, max } => rng.gen_range(*min..=*max),
419 }
420 }
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
429#[serde(tag = "shape", rename_all = "snake_case")]
430pub enum StringDomain {
431 Any,
433 Regex {
435 pattern: RegexPattern,
437 },
438}
439
440impl StringDomain {
441 #[must_use]
443 pub const fn any() -> Self {
444 Self::Any
445 }
446
447 pub fn regex(source: impl Into<String>) -> Result<Self> {
449 Ok(Self::Regex {
450 pattern: RegexPattern::new(source)?,
451 })
452 }
453
454 #[must_use]
456 pub fn contains_native(&self, value: &str) -> bool {
457 match self {
458 Self::Any => true,
459 Self::Regex { pattern } => pattern.is_match(value),
460 }
461 }
462
463 #[must_use]
465 pub const fn cardinality(&self) -> Cardinality {
466 Cardinality::Unbounded
467 }
468
469 #[must_use]
473 pub fn boundaries_native(&self) -> Vec<String> {
474 vec![String::new()]
475 }
476
477 pub fn sample_native<R: Rng + ?Sized>(&self, _rng: &mut R) -> String {
484 match self {
485 Self::Any => String::new(),
486 Self::Regex { .. } => unimplemented!(
487 "sampling StringDomain::Regex requires a regex generator crate \
488 (see SRD-0004 follow-ups); call the authored default instead."
489 ),
490 }
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(tag = "shape", rename_all = "snake_case")]
501pub enum SelectionDomain {
502 Fixed {
504 values: IndexSet<SelectionItem>,
506 max_selections: u32,
508 },
509 External {
512 resolver: ResolverId,
514 max_selections: u32,
516 },
517}
518
519impl SelectionDomain {
520 pub fn fixed(values: IndexSet<SelectionItem>, max_selections: u32) -> Result<Self> {
523 if values.is_empty() {
524 return Err(Error::Domain(DomainError::EmptySelection));
525 }
526 if max_selections == 0 {
527 return Err(Error::Domain(DomainError::ZeroMaxSelections));
528 }
529 Ok(Self::Fixed {
530 values,
531 max_selections,
532 })
533 }
534
535 pub fn external(resolver: ResolverId, max_selections: u32) -> Result<Self> {
537 if max_selections == 0 {
538 return Err(Error::Domain(DomainError::ZeroMaxSelections));
539 }
540 Ok(Self::External {
541 resolver,
542 max_selections,
543 })
544 }
545
546 #[must_use]
548 pub const fn max_selections(&self) -> u32 {
549 match self {
550 Self::Fixed { max_selections, .. } | Self::External { max_selections, .. } => {
551 *max_selections
552 }
553 }
554 }
555
556 #[must_use]
561 pub fn contains_items_fixed(&self, items: &IndexSet<SelectionItem>) -> bool {
562 match self {
563 Self::Fixed {
564 values,
565 max_selections,
566 } => items.len() <= *max_selections as usize && items.iter().all(|i| values.contains(i)),
567 Self::External { .. } => false,
568 }
569 }
570
571 #[must_use]
573 pub fn cardinality(&self) -> Cardinality {
574 match self {
575 Self::Fixed { values, .. } => Cardinality::finite(values.len() as u64),
576 Self::External { .. } => Cardinality::Unbounded,
577 }
578 }
579
580 #[must_use]
582 pub fn boundaries_fixed(&self) -> Vec<IndexSet<SelectionItem>> {
583 match self {
584 Self::Fixed { values, .. } => {
585 let mut out: Vec<IndexSet<SelectionItem>> = Vec::new();
586 if let Some(first) = values.iter().next() {
587 let mut one = IndexSet::new();
588 one.insert(first.clone());
589 out.push(one);
590 }
591 if let Some(last) = values.iter().next_back() {
592 let mut one = IndexSet::new();
593 one.insert(last.clone());
594 if out.first().is_none_or(|f| f.iter().next() != Some(last)) {
595 out.push(one);
596 }
597 }
598 out
599 }
600 Self::External { .. } => Vec::new(),
601 }
602 }
603
604 pub fn sample_fixed<R: Rng + ?Sized>(&self, rng: &mut R) -> IndexSet<SelectionItem> {
611 match self {
612 Self::Fixed {
613 values,
614 max_selections,
615 } => {
616 let cap = (*max_selections as usize).min(values.len()).max(1);
617 let k = rng.gen_range(1..=cap);
618 let picks: Vec<SelectionItem> =
619 values.iter().cloned().choose_multiple(rng, k);
620 picks.into_iter().collect()
621 }
622 Self::External { .. } => unimplemented!(
623 "sampling SelectionDomain::External requires a \
624 SelectionResolverRegistry (see SRD-0004 D15)."
625 ),
626 }
627 }
628}
629
630#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636pub struct LabeledEntry {
637 pub value: SelectionItem,
639 pub label: String,
641}
642
643pub trait SelectionResolver: Send + Sync + std::fmt::Debug + 'static {
651 fn id(&self) -> &ResolverId;
653
654 fn valid_values(&self) -> Result<IndexSet<SelectionItem>>;
657
658 fn is_valid(&self, value: &SelectionItem) -> Result<bool> {
660 Ok(self.valid_values()?.contains(value))
661 }
662
663 fn describe(&self) -> &str;
665}
666
667pub trait LabeledSelectionResolver: SelectionResolver {
670 fn labeled_values(&self) -> Result<Vec<LabeledEntry>>;
672}
673
674pub trait SelectionResolverRegistry: Send + Sync + std::fmt::Debug + 'static {
677 fn get(&self, id: &ResolverId) -> Option<Arc<dyn SelectionResolver>>;
679
680 fn ids(&self) -> Vec<ResolverId>;
682}
683
684#[derive(Debug, Clone, Copy)]
697pub enum Domain<'a> {
698 Integer {
700 parameter: &'a ParameterName,
702 domain: &'a IntegerDomain,
704 },
705 Double {
707 parameter: &'a ParameterName,
709 domain: &'a DoubleDomain,
711 },
712 Boolean {
714 parameter: &'a ParameterName,
716 },
717 String {
719 parameter: &'a ParameterName,
721 domain: &'a StringDomain,
723 },
724 Selection {
726 parameter: &'a ParameterName,
728 domain: &'a SelectionDomain,
730 },
731}
732
733impl<'a> Domain<'a> {
734 #[must_use]
736 pub const fn parameter(&self) -> &'a ParameterName {
737 match self {
738 Self::Integer { parameter, .. }
739 | Self::Double { parameter, .. }
740 | Self::Boolean { parameter }
741 | Self::String { parameter, .. }
742 | Self::Selection { parameter, .. } => parameter,
743 }
744 }
745
746 #[must_use]
750 pub fn contains(&self, value: &Value) -> bool {
751 match (self, value) {
752 (Self::Integer { domain, .. }, Value::Integer(v)) => {
753 domain.contains_native(v.value)
754 }
755 (Self::Double { domain, .. }, Value::Double(v)) => {
756 domain.contains_native(v.value)
757 }
758 (Self::Boolean { .. }, Value::Boolean(_)) => true,
759 (Self::String { domain, .. }, Value::String(v)) => {
760 domain.contains_native(&v.value)
761 }
762 (Self::Selection { domain, .. }, Value::Selection(v)) => match domain {
763 SelectionDomain::Fixed { .. } => domain.contains_items_fixed(&v.items),
764 SelectionDomain::External { max_selections, .. } => {
765 v.items.len() <= *max_selections as usize
766 }
767 },
768 _ => false,
769 }
770 }
771
772 #[must_use]
774 pub fn cardinality(&self) -> Cardinality {
775 match self {
776 Self::Integer { domain, .. } => domain.cardinality(),
777 Self::Double { domain, .. } => domain.cardinality(),
778 Self::Boolean { .. } => Cardinality::finite(2),
779 Self::String { domain, .. } => domain.cardinality(),
780 Self::Selection { domain, .. } => domain.cardinality(),
781 }
782 }
783
784 #[must_use]
786 pub fn boundary_values(&self) -> Vec<Value> {
787 match self {
788 Self::Integer { parameter, domain } => domain
789 .boundaries_native()
790 .into_iter()
791 .map(|v| Value::integer((*parameter).clone(), v, None))
792 .collect(),
793 Self::Double { parameter, domain } => domain
794 .boundaries_native()
795 .into_iter()
796 .map(|v| Value::double((*parameter).clone(), v, None))
797 .collect(),
798 Self::Boolean { parameter } => vec![
799 Value::boolean((*parameter).clone(), false, None),
800 Value::boolean((*parameter).clone(), true, None),
801 ],
802 Self::String { parameter, domain } => domain
803 .boundaries_native()
804 .into_iter()
805 .map(|v| Value::string((*parameter).clone(), v, None))
806 .collect(),
807 Self::Selection { parameter, domain } => domain
808 .boundaries_fixed()
809 .into_iter()
810 .map(|items| Value::selection((*parameter).clone(), items, None))
811 .collect(),
812 }
813 }
814
815 pub fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
822 match self {
823 Self::Integer { parameter, domain } => {
824 Value::integer((*parameter).clone(), domain.sample_native(rng), None)
825 }
826 Self::Double { parameter, domain } => {
827 Value::double((*parameter).clone(), domain.sample_native(rng), None)
828 }
829 Self::Boolean { parameter } => {
830 Value::boolean((*parameter).clone(), rng.gen_bool(0.5), None)
831 }
832 Self::String { parameter, domain } => {
833 Value::string((*parameter).clone(), domain.sample_native(rng), None)
834 }
835 Self::Selection { parameter, domain } => {
836 Value::selection((*parameter).clone(), domain.sample_fixed(rng), None)
837 }
838 }
839 }
840
841 pub fn enumerate(&self) -> Result<Box<dyn Iterator<Item = Value> + 'a>> {
844 match self {
845 Self::Integer { parameter, domain } => {
846 let parameter = (*parameter).clone();
847 match domain {
848 IntegerDomain::Range { min, max } => {
849 let (min, max) = (*min, *max);
850 Ok(Box::new((min..=max).map(move |v| {
851 Value::integer(parameter.clone(), v, None)
852 })))
853 }
854 IntegerDomain::Discrete { values } => {
855 Ok(Box::new(values.iter().copied().map(move |v| {
856 Value::integer(parameter.clone(), v, None)
857 })))
858 }
859 }
860 }
861 Self::Double { .. } | Self::String { .. } => {
862 Err(Error::Domain(DomainError::NotEnumerable))
863 }
864 Self::Boolean { parameter } => {
865 let parameter = (*parameter).clone();
866 Ok(Box::new([false, true].into_iter().map(move |v| {
867 Value::boolean(parameter.clone(), v, None)
868 })))
869 }
870 Self::Selection { parameter, domain } => match domain {
871 SelectionDomain::Fixed { values, .. } => {
872 let parameter = (*parameter).clone();
873 Ok(Box::new(values.iter().cloned().map(move |item| {
874 let mut one = IndexSet::new();
875 one.insert(item);
876 Value::selection(parameter.clone(), one, None)
877 })))
878 }
879 SelectionDomain::External { .. } => {
880 Err(Error::Domain(DomainError::NotEnumerable))
881 }
882 },
883 }
884 }
885}
886
887#[cfg(test)]
892mod tests {
893 use super::*;
894 use rand::SeedableRng;
895 use rand::rngs::StdRng;
896
897 fn pname(s: &str) -> ParameterName {
898 ParameterName::new(s).unwrap()
899 }
900
901 fn selitems(xs: &[&str]) -> IndexSet<SelectionItem> {
902 xs.iter().map(|s| SelectionItem::new(*s).unwrap()).collect()
903 }
904
905 #[test]
908 fn cardinality_equality() {
909 assert_eq!(Cardinality::finite(3), Cardinality::finite(3));
910 assert_ne!(Cardinality::finite(3), Cardinality::Unbounded);
911 }
912
913 #[test]
916 fn regex_pattern_compiles_and_matches() {
917 let p = RegexPattern::new("^[a-z]+$").unwrap();
918 assert!(p.is_match("abc"));
919 assert!(!p.is_match("abc1"));
920 assert_eq!(p.as_str(), "^[a-z]+$");
921 }
922
923 #[test]
924 fn regex_pattern_serde_roundtrip() {
925 let p = RegexPattern::new("^foo$").unwrap();
926 let json = serde_json::to_string(&p).unwrap();
927 assert_eq!(json, "\"^foo$\"");
928 let back: RegexPattern = serde_json::from_str(&json).unwrap();
929 assert_eq!(p, back);
930 }
931
932 #[test]
933 fn regex_pattern_invalid_source_is_deserialise_error() {
934 let bad: std::result::Result<RegexPattern, _> = serde_json::from_str("\"[\"");
935 assert!(bad.is_err());
936 }
937
938 #[test]
941 fn resolver_id_accepts_simple_ids() {
942 ResolverId::new("datasets").unwrap();
943 ResolverId::new("study-templates").unwrap();
944 }
945
946 #[test]
947 fn resolver_id_rejects_bad_start() {
948 assert!(matches!(
949 ResolverId::new("1ds"),
950 Err(NameError::BadStart { .. })
951 ));
952 }
953
954 #[test]
957 fn integer_range_constructor_and_ops() {
958 let d = IntegerDomain::range(1, 5).unwrap();
959 assert!(d.contains_native(1));
960 assert!(d.contains_native(5));
961 assert!(!d.contains_native(0));
962 assert!(!d.contains_native(6));
963 assert_eq!(d.cardinality(), Cardinality::finite(5));
964 assert_eq!(d.boundaries_native(), vec![1, 5]);
965 }
966
967 #[test]
968 fn integer_range_single_point_has_one_boundary() {
969 let d = IntegerDomain::range(7, 7).unwrap();
970 assert_eq!(d.boundaries_native(), vec![7]);
971 assert_eq!(d.cardinality(), Cardinality::finite(1));
972 }
973
974 #[test]
975 fn integer_range_rejects_reversed_bounds() {
976 let err = IntegerDomain::range(5, 1).unwrap_err();
977 assert!(matches!(err, Error::Domain(DomainError::InvalidRange { .. })));
978 }
979
980 #[test]
981 fn integer_discrete_constructor_and_ops() {
982 let mut set = BTreeSet::new();
983 set.insert(3);
984 set.insert(1);
985 set.insert(5);
986 let d = IntegerDomain::discrete(set).unwrap();
987 assert!(d.contains_native(1));
988 assert!(!d.contains_native(2));
989 assert_eq!(d.cardinality(), Cardinality::finite(3));
990 assert_eq!(d.boundaries_native(), vec![1, 5]);
991 }
992
993 #[test]
994 fn integer_discrete_rejects_empty() {
995 let err = IntegerDomain::discrete(BTreeSet::new()).unwrap_err();
996 assert!(matches!(err, Error::Domain(DomainError::EmptyDiscrete)));
997 }
998
999 #[test]
1000 fn integer_range_cardinality_saturates() {
1001 let d = IntegerDomain::range(i64::MIN, i64::MAX).unwrap();
1002 assert_eq!(d.cardinality(), Cardinality::finite(u64::MAX));
1003 }
1004
1005 #[test]
1006 fn integer_sample_in_range() {
1007 let mut rng = StdRng::seed_from_u64(42);
1008 let d = IntegerDomain::range(10, 20).unwrap();
1009 for _ in 0..50 {
1010 let v = d.sample_native(&mut rng);
1011 assert!((10..=20).contains(&v));
1012 }
1013 }
1014
1015 #[test]
1016 fn integer_iter_covers_range() {
1017 let d = IntegerDomain::range(1, 3).unwrap();
1018 let got: Vec<i64> = d.iter_native().collect();
1019 assert_eq!(got, vec![1, 2, 3]);
1020 }
1021
1022 #[test]
1025 fn double_range_rejects_nan_and_reversed() {
1026 assert!(DoubleDomain::range(f64::NAN, 1.0).is_err());
1027 assert!(DoubleDomain::range(0.0, f64::NAN).is_err());
1028 assert!(DoubleDomain::range(f64::INFINITY, 1.0).is_err());
1029 assert!(DoubleDomain::range(2.0, 1.0).is_err());
1030 }
1031
1032 #[test]
1033 fn double_range_contains() {
1034 let d = DoubleDomain::range(0.0, 1.0).unwrap();
1035 assert!(d.contains_native(0.0));
1036 assert!(d.contains_native(1.0));
1037 assert!(d.contains_native(0.5));
1038 assert!(!d.contains_native(-0.1));
1039 assert!(!d.contains_native(f64::NAN));
1040 }
1041
1042 #[test]
1043 fn double_cardinality_is_unbounded() {
1044 let d = DoubleDomain::range(0.0, 1.0).unwrap();
1045 assert_eq!(d.cardinality(), Cardinality::Unbounded);
1046 }
1047
1048 #[test]
1051 fn string_any_contains_anything() {
1052 let d = StringDomain::any();
1053 assert!(d.contains_native(""));
1054 assert!(d.contains_native("hello"));
1055 assert_eq!(d.cardinality(), Cardinality::Unbounded);
1056 }
1057
1058 #[test]
1059 fn string_regex_contains_matches_only() {
1060 let d = StringDomain::regex("^[a-z]+$").unwrap();
1061 assert!(d.contains_native("abc"));
1062 assert!(!d.contains_native("abc1"));
1063 }
1064
1065 #[test]
1066 fn string_regex_rejects_malformed_source() {
1067 let err = StringDomain::regex("[").unwrap_err();
1068 assert!(matches!(err, Error::Regex(_)));
1069 }
1070
1071 #[test]
1074 fn selection_fixed_constructor_and_ops() {
1075 let d = SelectionDomain::fixed(selitems(&["a", "b", "c"]), 2).unwrap();
1076 assert_eq!(d.max_selections(), 2);
1077 assert_eq!(d.cardinality(), Cardinality::finite(3));
1078 assert!(d.contains_items_fixed(&selitems(&["a"])));
1079 assert!(d.contains_items_fixed(&selitems(&["a", "b"])));
1080 assert!(!d.contains_items_fixed(&selitems(&["a", "b", "c"])));
1081 assert!(!d.contains_items_fixed(&selitems(&["x"])));
1082 }
1083
1084 #[test]
1085 fn selection_fixed_rejects_empty_and_zero_max() {
1086 assert!(matches!(
1087 SelectionDomain::fixed(IndexSet::new(), 1),
1088 Err(Error::Domain(DomainError::EmptySelection))
1089 ));
1090 assert!(matches!(
1091 SelectionDomain::fixed(selitems(&["a"]), 0),
1092 Err(Error::Domain(DomainError::ZeroMaxSelections))
1093 ));
1094 }
1095
1096 #[test]
1097 fn selection_external_constructor() {
1098 let id = ResolverId::new("datasets").unwrap();
1099 let d = SelectionDomain::external(id, 1).unwrap();
1100 assert_eq!(d.cardinality(), Cardinality::Unbounded);
1101 assert!(d.boundaries_fixed().is_empty());
1102 }
1103
1104 #[test]
1105 fn selection_sample_respects_max() {
1106 let mut rng = StdRng::seed_from_u64(7);
1107 let d = SelectionDomain::fixed(selitems(&["a", "b", "c", "d"]), 2).unwrap();
1108 for _ in 0..50 {
1109 let pick = d.sample_fixed(&mut rng);
1110 assert!(!pick.is_empty());
1111 assert!(pick.len() <= 2);
1112 for item in &pick {
1113 assert!(["a", "b", "c", "d"].contains(&item.as_str()));
1114 }
1115 }
1116 }
1117
1118 #[test]
1121 fn domain_view_contains_dispatches_by_kind() {
1122 let name = pname("threads");
1123 let id = IntegerDomain::range(1, 10).unwrap();
1124 let view = Domain::Integer {
1125 parameter: &name,
1126 domain: &id,
1127 };
1128 let in_range = Value::integer(name.clone(), 5, None);
1129 let out_of_range = Value::integer(name.clone(), 42, None);
1130 let wrong_kind = Value::boolean(name.clone(), true, None);
1131 assert!(view.contains(&in_range));
1132 assert!(!view.contains(&out_of_range));
1133 assert!(!view.contains(&wrong_kind));
1134 }
1135
1136 #[test]
1137 fn domain_view_boundaries_return_values() {
1138 let name = pname("threads");
1139 let id = IntegerDomain::range(1, 10).unwrap();
1140 let view = Domain::Integer {
1141 parameter: &name,
1142 domain: &id,
1143 };
1144 let bs = view.boundary_values();
1145 assert_eq!(bs.len(), 2);
1146 assert_eq!(bs[0].as_integer(), Some(1));
1147 assert_eq!(bs[1].as_integer(), Some(10));
1148 assert_eq!(bs[0].parameter().as_str(), "threads");
1149 }
1150
1151 #[test]
1152 fn domain_view_enumerate_integer_range() {
1153 let name = pname("n");
1154 let id = IntegerDomain::range(1, 3).unwrap();
1155 let view = Domain::Integer {
1156 parameter: &name,
1157 domain: &id,
1158 };
1159 let values: Vec<i64> = view
1160 .enumerate()
1161 .unwrap()
1162 .map(|v| v.as_integer().unwrap())
1163 .collect();
1164 assert_eq!(values, vec![1, 2, 3]);
1165 }
1166
1167 #[test]
1168 fn domain_view_enumerate_double_range_is_not_enumerable() {
1169 let name = pname("r");
1170 let dd = DoubleDomain::range(0.0, 1.0).unwrap();
1171 let view = Domain::Double {
1172 parameter: &name,
1173 domain: &dd,
1174 };
1175 match view.enumerate() {
1176 Ok(_) => panic!("expected NotEnumerable"),
1177 Err(Error::Domain(DomainError::NotEnumerable)) => {}
1178 Err(other) => panic!("wrong error: {other:?}"),
1179 }
1180 }
1181
1182 #[test]
1183 fn domain_view_enumerate_boolean_yields_both() {
1184 let name = pname("b");
1185 let view = Domain::Boolean { parameter: &name };
1186 let got: Vec<bool> = view
1187 .enumerate()
1188 .unwrap()
1189 .map(|v| v.as_boolean().unwrap())
1190 .collect();
1191 assert_eq!(got, vec![false, true]);
1192 }
1193
1194 #[test]
1195 fn domain_view_sample_produces_valid_integer_value() {
1196 let mut rng = StdRng::seed_from_u64(9);
1197 let name = pname("n");
1198 let id = IntegerDomain::range(1, 100).unwrap();
1199 let view = Domain::Integer {
1200 parameter: &name,
1201 domain: &id,
1202 };
1203 let v = view.sample(&mut rng);
1204 assert!(view.contains(&v));
1205 assert!(v.verify_fingerprint());
1206 }
1207
1208 #[test]
1209 fn domain_view_selection_contains_checks_items_fixed() {
1210 let name = pname("s");
1211 let sd = SelectionDomain::fixed(selitems(&["a", "b", "c"]), 2).unwrap();
1212 let view = Domain::Selection {
1213 parameter: &name,
1214 domain: &sd,
1215 };
1216 let good = Value::selection(name.clone(), selitems(&["a"]), None);
1217 let too_many = Value::selection(name.clone(), selitems(&["a", "b", "c"]), None);
1218 let bad_item = Value::selection(name.clone(), selitems(&["z"]), None);
1219 assert!(view.contains(&good));
1220 assert!(!view.contains(&too_many));
1221 assert!(!view.contains(&bad_item));
1222 }
1223
1224 #[test]
1225 fn domain_view_enumerate_selection_gives_single_item_values() {
1226 let name = pname("s");
1227 let sd = SelectionDomain::fixed(selitems(&["a", "b"]), 2).unwrap();
1228 let view = Domain::Selection {
1229 parameter: &name,
1230 domain: &sd,
1231 };
1232 let picks: Vec<String> = view
1233 .enumerate()
1234 .unwrap()
1235 .map(|v| v.as_selection().unwrap().iter().next().unwrap().as_str().to_owned())
1236 .collect();
1237 assert_eq!(picks, vec!["a".to_owned(), "b".to_owned()]);
1238 }
1239
1240 #[test]
1243 fn integer_domain_serde_roundtrip_range() {
1244 let d = IntegerDomain::range(1, 10).unwrap();
1245 let json = serde_json::to_string(&d).unwrap();
1246 let back: IntegerDomain = serde_json::from_str(&json).unwrap();
1247 assert_eq!(d, back);
1248 }
1249
1250 #[test]
1251 fn selection_domain_serde_roundtrip_fixed() {
1252 let d = SelectionDomain::fixed(selitems(&["a", "b"]), 2).unwrap();
1253 let json = serde_json::to_string(&d).unwrap();
1254 let back: SelectionDomain = serde_json::from_str(&json).unwrap();
1255 assert_eq!(d, back);
1256 }
1257
1258 #[test]
1259 fn string_domain_serde_roundtrip_regex() {
1260 let d = StringDomain::regex("^foo$").unwrap();
1261 let json = serde_json::to_string(&d).unwrap();
1262 let back: StringDomain = serde_json::from_str(&json).unwrap();
1263 assert_eq!(d, back);
1264 }
1265}