1use std::collections::BTreeSet;
22
23use indexmap::IndexSet;
24use rand::Rng;
25use rand::SeedableRng;
26use rand::rngs::StdRng;
27use serde::{Deserialize, Serialize};
28
29use crate::attributes::{AttributeError, Attributed, LabelKey, LabelValue, Labels, TagKey, TagValue, Tags, Tier};
30use crate::constraint::{
31 BoolConstraint, Constraint, DoubleConstraint, IntConstraint, SelectionConstraint,
32 StringConstraint,
33};
34use crate::domain::{
35 Domain, DoubleDomain, IntegerDomain, ResolverId, SelectionDomain, StringDomain,
36};
37use crate::expression::{DerivationError, EvalValue, Expression, ValueBindings};
38use crate::names::ParameterName;
39use crate::validation::ValidationResult;
40use crate::value::{GeneratorInfo, SelectionItem, Value, ValueKind};
41
42#[derive(Debug, thiserror::Error)]
48pub enum ParameterError {
49 #[error("default value is not in the parameter's domain")]
51 DefaultNotInDomain,
52
53 #[error("default value violates a registered constraint")]
55 DefaultViolatesConstraint,
56
57 #[error("constraint kind does not match parameter kind ({parameter_kind:?})")]
59 ConstraintKindMismatch {
60 parameter_kind: ValueKind,
62 },
63
64 #[error("derived parameters cannot produce Selection values")]
67 DerivedSelectionUnsupported,
68
69 #[error(transparent)]
72 Attribute(#[from] AttributeError),
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct IntegerParameter {
82 pub name: ParameterName,
84 pub domain: IntegerDomain,
86 #[serde(default)]
88 pub constraints: Vec<IntConstraint>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub default: Option<i64>,
92 #[serde(default)]
94 pub labels: Labels,
95 #[serde(default)]
97 pub tags: Tags,
98}
99
100impl IntegerParameter {
101 pub fn range(name: ParameterName, min: i64, max: i64) -> crate::Result<Self> {
103 Ok(Self {
104 name,
105 domain: IntegerDomain::range(min, max)?,
106 constraints: Vec::new(),
107 default: None,
108 labels: Labels::new(),
109 tags: Tags::new(),
110 })
111 }
112
113 pub fn of(name: ParameterName, values: BTreeSet<i64>) -> crate::Result<Self> {
115 Ok(Self {
116 name,
117 domain: IntegerDomain::discrete(values)?,
118 constraints: Vec::new(),
119 default: None,
120 labels: Labels::new(),
121 tags: Tags::new(),
122 })
123 }
124
125 pub fn with_default(mut self, default: i64) -> crate::Result<Self> {
128 if !self.domain.contains_native(default) {
129 return Err(ParameterError::DefaultNotInDomain.into());
130 }
131 for c in &self.constraints {
132 if !c.test(default) {
133 return Err(ParameterError::DefaultViolatesConstraint.into());
134 }
135 }
136 self.default = Some(default);
137 Ok(self)
138 }
139
140 #[must_use]
142 pub fn with_constraint(mut self, c: IntConstraint) -> Self {
143 self.constraints.push(c);
144 self
145 }
146
147 pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
149 check_no_tag_conflict(&self.tags, key.as_str())?;
150 self.labels.insert(key, value);
151 Ok(self)
152 }
153
154 pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
156 check_no_label_conflict(&self.labels, key.as_str())?;
157 self.tags.insert(key, value);
158 Ok(self)
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168pub struct DoubleParameter {
169 pub name: ParameterName,
171 pub domain: DoubleDomain,
173 #[serde(default)]
175 pub constraints: Vec<DoubleConstraint>,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub default: Option<f64>,
179 #[serde(default)]
181 pub labels: Labels,
182 #[serde(default)]
184 pub tags: Tags,
185}
186
187impl DoubleParameter {
188 pub fn range(name: ParameterName, min: f64, max: f64) -> crate::Result<Self> {
190 Ok(Self {
191 name,
192 domain: DoubleDomain::range(min, max)?,
193 constraints: Vec::new(),
194 default: None,
195 labels: Labels::new(),
196 tags: Tags::new(),
197 })
198 }
199
200 pub fn with_default(mut self, default: f64) -> crate::Result<Self> {
203 if !self.domain.contains_native(default) {
204 return Err(ParameterError::DefaultNotInDomain.into());
205 }
206 for c in &self.constraints {
207 if !c.test(default) {
208 return Err(ParameterError::DefaultViolatesConstraint.into());
209 }
210 }
211 self.default = Some(default);
212 Ok(self)
213 }
214
215 #[must_use]
217 pub fn with_constraint(mut self, c: DoubleConstraint) -> Self {
218 self.constraints.push(c);
219 self
220 }
221
222 pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
224 check_no_tag_conflict(&self.tags, key.as_str())?;
225 self.labels.insert(key, value);
226 Ok(self)
227 }
228
229 pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
231 check_no_label_conflict(&self.labels, key.as_str())?;
232 self.tags.insert(key, value);
233 Ok(self)
234 }
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct BooleanParameter {
244 pub name: ParameterName,
246 #[serde(default)]
248 pub constraints: Vec<BoolConstraint>,
249 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub default: Option<bool>,
252 #[serde(default)]
254 pub labels: Labels,
255 #[serde(default)]
257 pub tags: Tags,
258}
259
260impl BooleanParameter {
261 #[must_use]
263 pub fn of(name: ParameterName) -> Self {
264 Self {
265 name,
266 constraints: Vec::new(),
267 default: None,
268 labels: Labels::new(),
269 tags: Tags::new(),
270 }
271 }
272
273 pub fn with_default(mut self, default: bool) -> crate::Result<Self> {
275 for c in &self.constraints {
276 if !c.test(default) {
277 return Err(ParameterError::DefaultViolatesConstraint.into());
278 }
279 }
280 self.default = Some(default);
281 Ok(self)
282 }
283
284 #[must_use]
286 pub fn with_constraint(mut self, c: BoolConstraint) -> Self {
287 self.constraints.push(c);
288 self
289 }
290
291 pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
293 check_no_tag_conflict(&self.tags, key.as_str())?;
294 self.labels.insert(key, value);
295 Ok(self)
296 }
297
298 pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
300 check_no_label_conflict(&self.labels, key.as_str())?;
301 self.tags.insert(key, value);
302 Ok(self)
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub struct StringParameter {
313 pub name: ParameterName,
315 pub domain: StringDomain,
317 #[serde(default)]
319 pub constraints: Vec<StringConstraint>,
320 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub default: Option<String>,
323 #[serde(default)]
325 pub labels: Labels,
326 #[serde(default)]
328 pub tags: Tags,
329}
330
331impl StringParameter {
332 #[must_use]
334 pub fn of(name: ParameterName) -> Self {
335 Self {
336 name,
337 domain: StringDomain::any(),
338 constraints: Vec::new(),
339 default: None,
340 labels: Labels::new(),
341 tags: Tags::new(),
342 }
343 }
344
345 pub fn regex(name: ParameterName, pattern: impl Into<String>) -> crate::Result<Self> {
347 Ok(Self {
348 name,
349 domain: StringDomain::regex(pattern)?,
350 constraints: Vec::new(),
351 default: None,
352 labels: Labels::new(),
353 tags: Tags::new(),
354 })
355 }
356
357 pub fn with_default(mut self, default: impl Into<String>) -> crate::Result<Self> {
360 let default = default.into();
361 if !self.domain.contains_native(&default) {
362 return Err(ParameterError::DefaultNotInDomain.into());
363 }
364 for c in &self.constraints {
365 if !c.test(&default) {
366 return Err(ParameterError::DefaultViolatesConstraint.into());
367 }
368 }
369 self.default = Some(default);
370 Ok(self)
371 }
372
373 #[must_use]
375 pub fn with_constraint(mut self, c: StringConstraint) -> Self {
376 self.constraints.push(c);
377 self
378 }
379
380 pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
382 check_no_tag_conflict(&self.tags, key.as_str())?;
383 self.labels.insert(key, value);
384 Ok(self)
385 }
386
387 pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
389 check_no_label_conflict(&self.labels, key.as_str())?;
390 self.tags.insert(key, value);
391 Ok(self)
392 }
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct SelectionParameter {
402 pub name: ParameterName,
404 pub domain: SelectionDomain,
406 #[serde(default)]
408 pub constraints: Vec<SelectionConstraint>,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub default: Option<IndexSet<SelectionItem>>,
412 #[serde(default)]
414 pub labels: Labels,
415 #[serde(default)]
417 pub tags: Tags,
418}
419
420impl SelectionParameter {
421 pub fn of(
423 name: ParameterName,
424 values: IndexSet<SelectionItem>,
425 max_selections: u32,
426 ) -> crate::Result<Self> {
427 Ok(Self {
428 name,
429 domain: SelectionDomain::fixed(values, max_selections)?,
430 constraints: Vec::new(),
431 default: None,
432 labels: Labels::new(),
433 tags: Tags::new(),
434 })
435 }
436
437 pub fn external(
439 name: ParameterName,
440 resolver: ResolverId,
441 max_selections: u32,
442 ) -> crate::Result<Self> {
443 Ok(Self {
444 name,
445 domain: SelectionDomain::external(resolver, max_selections)?,
446 constraints: Vec::new(),
447 default: None,
448 labels: Labels::new(),
449 tags: Tags::new(),
450 })
451 }
452
453 pub fn with_default(
455 mut self,
456 default: IndexSet<SelectionItem>,
457 ) -> crate::Result<Self> {
458 if matches!(self.domain, SelectionDomain::Fixed { .. })
462 && !self.domain.contains_items_fixed(&default)
463 {
464 return Err(ParameterError::DefaultNotInDomain.into());
465 }
466 for c in &self.constraints {
467 if !c.test(&default) {
468 return Err(ParameterError::DefaultViolatesConstraint.into());
469 }
470 }
471 self.default = Some(default);
472 Ok(self)
473 }
474
475 #[must_use]
477 pub fn with_constraint(mut self, c: SelectionConstraint) -> Self {
478 self.constraints.push(c);
479 self
480 }
481
482 pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
484 check_no_tag_conflict(&self.tags, key.as_str())?;
485 self.labels.insert(key, value);
486 Ok(self)
487 }
488
489 pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
491 check_no_label_conflict(&self.labels, key.as_str())?;
492 self.tags.insert(key, value);
493 Ok(self)
494 }
495}
496
497#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
507pub struct DerivedParameter {
508 pub name: ParameterName,
510 pub kind: ValueKind,
512 pub expression: Expression,
514 #[serde(default)]
516 pub labels: Labels,
517 #[serde(default)]
519 pub tags: Tags,
520}
521
522impl DerivedParameter {
523 pub fn new(
525 name: ParameterName,
526 kind: ValueKind,
527 expression: Expression,
528 ) -> crate::Result<Self> {
529 if matches!(kind, ValueKind::Selection) {
530 return Err(ParameterError::DerivedSelectionUnsupported.into());
531 }
532 Ok(Self {
533 name,
534 kind,
535 expression,
536 labels: Labels::new(),
537 tags: Tags::new(),
538 })
539 }
540
541 pub fn compute(&self, bindings: &ValueBindings) -> Result<Value, DerivationError> {
544 let raw = self.expression.eval(bindings)?;
545 if raw.kind() != self.kind {
546 return Err(DerivationError::TypeMismatch {
547 op: format!("derived({})", self.name),
548 expected: format!("{:?}", self.kind),
549 actual: format!("{:?}", raw.kind()),
550 });
551 }
552 let generator = Some(GeneratorInfo::Derived {
553 expression: format!("{:?}", self.expression),
554 });
555 Ok(match raw {
556 EvalValue::Integer(n) => Value::integer(self.name.clone(), n, generator),
557 EvalValue::Double(n) => Value::double(self.name.clone(), n, generator),
558 EvalValue::Boolean(b) => Value::boolean(self.name.clone(), b, generator),
559 EvalValue::String(s) => Value::string(self.name.clone(), s, generator),
560 })
561 }
562
563 pub fn with_label(mut self, key: LabelKey, value: LabelValue) -> crate::Result<Self> {
565 check_no_tag_conflict(&self.tags, key.as_str())?;
566 self.labels.insert(key, value);
567 Ok(self)
568 }
569
570 pub fn with_tag(mut self, key: TagKey, value: TagValue) -> crate::Result<Self> {
572 check_no_label_conflict(&self.labels, key.as_str())?;
573 self.tags.insert(key, value);
574 Ok(self)
575 }
576}
577
578fn check_no_tag_conflict(tags: &Tags, key: &str) -> Result<(), AttributeError> {
583 if tags.keys().any(|k| k.as_str() == key) {
584 return Err(AttributeError::DuplicateKey {
585 key: key.to_owned(),
586 tiers: vec![Tier::Label, Tier::Tag],
587 });
588 }
589 Ok(())
590}
591
592fn check_no_label_conflict(labels: &Labels, key: &str) -> Result<(), AttributeError> {
593 if labels.keys().any(|k| k.as_str() == key) {
594 return Err(AttributeError::DuplicateKey {
595 key: key.to_owned(),
596 tiers: vec![Tier::Label, Tier::Tag],
597 });
598 }
599 Ok(())
600}
601
602#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
612#[serde(tag = "kind", rename_all = "snake_case")]
613pub enum Parameter {
614 Integer(IntegerParameter),
616 Double(DoubleParameter),
618 Boolean(BooleanParameter),
620 String(StringParameter),
622 Selection(SelectionParameter),
624 Derived(DerivedParameter),
626}
627
628impl Parameter {
629 #[must_use]
631 pub const fn name(&self) -> &ParameterName {
632 match self {
633 Self::Integer(p) => &p.name,
634 Self::Double(p) => &p.name,
635 Self::Boolean(p) => &p.name,
636 Self::String(p) => &p.name,
637 Self::Selection(p) => &p.name,
638 Self::Derived(p) => &p.name,
639 }
640 }
641
642 #[must_use]
644 pub const fn kind(&self) -> ValueKind {
645 match self {
646 Self::Integer(_) => ValueKind::Integer,
647 Self::Double(_) => ValueKind::Double,
648 Self::Boolean(_) => ValueKind::Boolean,
649 Self::String(_) => ValueKind::String,
650 Self::Selection(_) => ValueKind::Selection,
651 Self::Derived(p) => p.kind,
652 }
653 }
654
655 #[must_use]
658 pub const fn domain(&self) -> Option<Domain<'_>> {
659 Some(match self {
660 Self::Integer(p) => Domain::Integer {
661 parameter: &p.name,
662 domain: &p.domain,
663 },
664 Self::Double(p) => Domain::Double {
665 parameter: &p.name,
666 domain: &p.domain,
667 },
668 Self::Boolean(p) => Domain::Boolean { parameter: &p.name },
669 Self::String(p) => Domain::String {
670 parameter: &p.name,
671 domain: &p.domain,
672 },
673 Self::Selection(p) => Domain::Selection {
674 parameter: &p.name,
675 domain: &p.domain,
676 },
677 Self::Derived(_) => return None,
678 })
679 }
680
681 #[must_use]
684 pub fn default(&self) -> Option<Value> {
685 let generator = Some(GeneratorInfo::Default);
686 match self {
687 Self::Integer(p) => p
688 .default
689 .map(|d| Value::integer(p.name.clone(), d, generator)),
690 Self::Double(p) => p
691 .default
692 .map(|d| Value::double(p.name.clone(), d, generator)),
693 Self::Boolean(p) => p
694 .default
695 .map(|d| Value::boolean(p.name.clone(), d, generator)),
696 Self::String(p) => p
697 .default
698 .clone()
699 .map(|d| Value::string(p.name.clone(), d, generator)),
700 Self::Selection(p) => p
701 .default
702 .clone()
703 .map(|d| Value::selection(p.name.clone(), d, generator)),
704 Self::Derived(_) => None,
705 }
706 }
707
708 pub fn generate<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
717 if let Some(d) = self.default() {
718 return d;
719 }
720 self.generate_random(rng)
721 }
722
723 pub fn generate_random<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
730 if let Self::Derived(_) = self {
731 unimplemented!(
732 "derived parameters do not support direct sampling; use DerivedParameter::compute"
733 );
734 }
735 let domain = self.domain().expect("non-derived has a domain");
736 domain.sample(rng)
737 }
738
739 pub fn generate_boundary<R: Rng + ?Sized>(&self, rng: &mut R) -> Value {
745 let domain = self
746 .domain()
747 .expect("generate_boundary is undefined for derived parameters");
748 let boundaries = domain.boundary_values();
749 if boundaries.is_empty() {
750 return domain.sample(rng);
751 }
752 let idx = rng.gen_range(0..boundaries.len());
753 boundaries.into_iter().nth(idx).expect("idx < len")
754 }
755
756 #[must_use]
759 pub fn validate(&self, value: &Value) -> ValidationResult {
760 if value.kind() != self.kind() {
761 return ValidationResult::failed(
762 "kind mismatch",
763 vec![format!(
764 "expected {:?}, got {:?}",
765 self.kind(),
766 value.kind()
767 )],
768 );
769 }
770
771 let mut violations = Vec::new();
772 match (self, value) {
773 (Self::Integer(p), Value::Integer(v)) => {
774 if !p.domain.contains_native(v.value) {
775 violations.push(format!("value {} not in domain", v.value));
776 }
777 for c in &p.constraints {
778 if !c.test(v.value) {
779 violations.push("constraint not satisfied".to_owned());
780 }
781 }
782 }
783 (Self::Double(p), Value::Double(v)) => {
784 if !p.domain.contains_native(v.value) {
785 violations.push(format!("value {} not in domain", v.value));
786 }
787 for c in &p.constraints {
788 if !c.test(v.value) {
789 violations.push("constraint not satisfied".to_owned());
790 }
791 }
792 }
793 (Self::Boolean(p), Value::Boolean(v)) => {
794 for c in &p.constraints {
795 if !c.test(v.value) {
796 violations.push("constraint not satisfied".to_owned());
797 }
798 }
799 }
800 (Self::String(p), Value::String(v)) => {
801 if !p.domain.contains_native(&v.value) {
802 violations.push("value not in domain".to_owned());
803 }
804 for c in &p.constraints {
805 if !c.test(&v.value) {
806 violations.push("constraint not satisfied".to_owned());
807 }
808 }
809 }
810 (Self::Selection(p), Value::Selection(v)) => {
811 if matches!(p.domain, SelectionDomain::Fixed { .. })
815 && !p.domain.contains_items_fixed(&v.items)
816 {
817 violations.push("selection not in domain".to_owned());
818 }
819 for c in &p.constraints {
820 if !c.test(&v.items) {
821 violations.push("constraint not satisfied".to_owned());
822 }
823 }
824 }
825 (Self::Derived(_), _) => {
826 }
829 _ => unreachable!("kind match enforced above"),
830 }
831
832 if violations.is_empty() {
833 ValidationResult::Passed
834 } else {
835 ValidationResult::failed("validation failed", violations)
836 }
837 }
838
839 #[must_use]
849 pub fn satisfies(&self, c: &Constraint) -> bool {
850 if c.kind() != self.kind() {
851 return false;
852 }
853 match (self, c) {
854 (Self::Integer(p), Constraint::Integer(ic)) => {
855 for b in p.domain.boundaries_native() {
856 if ic.test(b) {
857 return true;
858 }
859 }
860 let mut rng = StdRng::seed_from_u64(SATISFIES_SEED);
861 for _ in 0..SATISFIES_SAMPLES {
862 if ic.test(p.domain.sample_native(&mut rng)) {
863 return true;
864 }
865 }
866 false
867 }
868 (Self::Double(p), Constraint::Double(dc)) => {
869 for b in p.domain.boundaries_native() {
870 if dc.test(b) {
871 return true;
872 }
873 }
874 let mut rng = StdRng::seed_from_u64(SATISFIES_SEED);
875 for _ in 0..SATISFIES_SAMPLES {
876 if dc.test(p.domain.sample_native(&mut rng)) {
877 return true;
878 }
879 }
880 false
881 }
882 (Self::Boolean(_), Constraint::Boolean(bc)) => bc.test(true) || bc.test(false),
883 (Self::String(_), Constraint::String(sc)) => {
884 sc.test("")
887 }
888 (Self::Selection(p), Constraint::Selection(sc)) => {
889 for boundary in p.domain.boundaries_fixed() {
892 let iset: IndexSet<SelectionItem> = boundary.into_iter().collect();
893 if sc.test(&iset) {
894 return true;
895 }
896 }
897 false
898 }
899 _ => false,
900 }
901 }
902}
903
904impl Constraint {
906 const fn kind(&self) -> ValueKind {
907 match self {
908 Self::Integer(_) => ValueKind::Integer,
909 Self::Double(_) => ValueKind::Double,
910 Self::Boolean(_) => ValueKind::Boolean,
911 Self::String(_) => ValueKind::String,
912 Self::Selection(_) => ValueKind::Selection,
913 }
914 }
915}
916
917const SATISFIES_SEED: u64 = 0x5a71_5f1e_55e5_d007;
918const SATISFIES_SAMPLES: u32 = 8;
919
920impl Attributed for Parameter {
925 fn labels(&self) -> &Labels {
926 match self {
927 Self::Integer(p) => &p.labels,
928 Self::Double(p) => &p.labels,
929 Self::Boolean(p) => &p.labels,
930 Self::String(p) => &p.labels,
931 Self::Selection(p) => &p.labels,
932 Self::Derived(p) => &p.labels,
933 }
934 }
935
936 fn tags(&self) -> &Tags {
937 match self {
938 Self::Integer(p) => &p.tags,
939 Self::Double(p) => &p.tags,
940 Self::Boolean(p) => &p.tags,
941 Self::String(p) => &p.tags,
942 Self::Selection(p) => &p.tags,
943 Self::Derived(p) => &p.tags,
944 }
945 }
946}
947
948impl Attributed for IntegerParameter {
949 fn labels(&self) -> &Labels {
950 &self.labels
951 }
952 fn tags(&self) -> &Tags {
953 &self.tags
954 }
955}
956
957impl Attributed for DoubleParameter {
958 fn labels(&self) -> &Labels {
959 &self.labels
960 }
961 fn tags(&self) -> &Tags {
962 &self.tags
963 }
964}
965
966impl Attributed for BooleanParameter {
967 fn labels(&self) -> &Labels {
968 &self.labels
969 }
970 fn tags(&self) -> &Tags {
971 &self.tags
972 }
973}
974
975impl Attributed for StringParameter {
976 fn labels(&self) -> &Labels {
977 &self.labels
978 }
979 fn tags(&self) -> &Tags {
980 &self.tags
981 }
982}
983
984impl Attributed for SelectionParameter {
985 fn labels(&self) -> &Labels {
986 &self.labels
987 }
988 fn tags(&self) -> &Tags {
989 &self.tags
990 }
991}
992
993impl Attributed for DerivedParameter {
994 fn labels(&self) -> &Labels {
995 &self.labels
996 }
997 fn tags(&self) -> &Tags {
998 &self.tags
999 }
1000}
1001
1002#[cfg(test)]
1007mod tests {
1008 use super::*;
1009 use rand::rngs::StdRng;
1010
1011 fn pname(s: &str) -> ParameterName {
1012 ParameterName::new(s).unwrap()
1013 }
1014
1015 fn rng() -> StdRng {
1016 StdRng::seed_from_u64(7)
1017 }
1018
1019 #[test]
1022 fn integer_range_constructor() {
1023 let p = IntegerParameter::range(pname("n"), 1, 10).unwrap();
1024 assert_eq!(p.name.as_str(), "n");
1025 assert_eq!(p.default, None);
1026 }
1027
1028 #[test]
1029 fn integer_with_default_and_constraint() {
1030 let p = IntegerParameter::range(pname("n"), 1, 10)
1031 .unwrap()
1032 .with_constraint(IntConstraint::Min { n: 3 })
1033 .with_default(5)
1034 .unwrap();
1035 assert_eq!(p.default, Some(5));
1036
1037 let err = IntegerParameter::range(pname("n"), 1, 10)
1039 .unwrap()
1040 .with_default(42)
1041 .unwrap_err();
1042 assert!(matches!(
1043 err,
1044 crate::Error::Parameter(ParameterError::DefaultNotInDomain)
1045 ));
1046
1047 let err = IntegerParameter::range(pname("n"), 1, 10)
1049 .unwrap()
1050 .with_constraint(IntConstraint::Min { n: 5 })
1051 .with_default(3)
1052 .unwrap_err();
1053 assert!(matches!(
1054 err,
1055 crate::Error::Parameter(ParameterError::DefaultViolatesConstraint)
1056 ));
1057 }
1058
1059 #[test]
1060 fn integer_label_tag_namespace_enforcement() {
1061 let p = IntegerParameter::range(pname("n"), 1, 10).unwrap();
1062 let p = p
1063 .with_label(LabelKey::new("type").unwrap(), LabelValue::new("threads").unwrap())
1064 .unwrap();
1065 let err = p
1069 .with_tag(TagKey::new("type").unwrap(), TagValue::new("bench").unwrap())
1070 .unwrap_err();
1071 assert!(matches!(
1072 err,
1073 crate::Error::Attribute(AttributeError::DuplicateKey { .. })
1074 ));
1075 }
1076
1077 #[test]
1080 fn double_parameter_roundtrip() {
1081 let p = DoubleParameter::range(pname("r"), 0.0, 1.0)
1082 .unwrap()
1083 .with_default(0.5)
1084 .unwrap();
1085 assert_eq!(p.default, Some(0.5));
1086 }
1087
1088 #[test]
1091 fn boolean_parameter_with_default_and_constraint() {
1092 let p = BooleanParameter::of(pname("flag"))
1093 .with_constraint(BoolConstraint::EqTo { b: true })
1094 .with_default(true)
1095 .unwrap();
1096 assert_eq!(p.default, Some(true));
1097
1098 let err = BooleanParameter::of(pname("flag"))
1099 .with_constraint(BoolConstraint::EqTo { b: true })
1100 .with_default(false)
1101 .unwrap_err();
1102 assert!(matches!(
1103 err,
1104 crate::Error::Parameter(ParameterError::DefaultViolatesConstraint)
1105 ));
1106 }
1107
1108 #[test]
1111 fn string_regex_parameter_rejects_non_matching_default() {
1112 let err = StringParameter::regex(pname("s"), "^foo$")
1113 .unwrap()
1114 .with_default("bar")
1115 .unwrap_err();
1116 assert!(matches!(
1117 err,
1118 crate::Error::Parameter(ParameterError::DefaultNotInDomain)
1119 ));
1120 }
1121
1122 #[test]
1125 fn selection_parameter_default_subset_check() {
1126 let values: IndexSet<SelectionItem> =
1127 ["a", "b", "c"].iter().map(|s| SelectionItem::new(*s).unwrap()).collect();
1128 let p = SelectionParameter::of(pname("s"), values, 2).unwrap();
1129
1130 let good: IndexSet<SelectionItem> =
1131 std::iter::once(SelectionItem::new("a").unwrap()).collect();
1132 assert!(p.clone().with_default(good).is_ok());
1133
1134 let bad: IndexSet<SelectionItem> =
1135 std::iter::once(SelectionItem::new("z").unwrap()).collect();
1136 let err = p.with_default(bad).unwrap_err();
1137 assert!(matches!(
1138 err,
1139 crate::Error::Parameter(ParameterError::DefaultNotInDomain)
1140 ));
1141 }
1142
1143 #[test]
1146 fn parameter_name_kind_and_domain_dispatch() {
1147 let p: Parameter = Parameter::Integer(
1148 IntegerParameter::range(pname("n"), 1, 10).unwrap(),
1149 );
1150 assert_eq!(p.name().as_str(), "n");
1151 assert_eq!(p.kind(), ValueKind::Integer);
1152 assert!(p.domain().is_some());
1153 }
1154
1155 #[test]
1156 fn parameter_generate_prefers_default() {
1157 let p = Parameter::Integer(
1158 IntegerParameter::range(pname("n"), 1, 10)
1159 .unwrap()
1160 .with_default(7)
1161 .unwrap(),
1162 );
1163 let mut r = rng();
1164 let v = p.generate(&mut r);
1165 assert_eq!(v.as_integer(), Some(7));
1166 match v.provenance().generator.as_ref().unwrap() {
1168 GeneratorInfo::Default => {}
1169 other => panic!("expected Default, got {other:?}"),
1170 }
1171 }
1172
1173 #[test]
1174 fn parameter_generate_random_draws_from_domain() {
1175 let p = Parameter::Integer(
1176 IntegerParameter::range(pname("n"), 1, 10).unwrap(),
1177 );
1178 let mut r = rng();
1179 for _ in 0..20 {
1180 let v = p.generate_random(&mut r);
1181 let n = v.as_integer().unwrap();
1182 assert!((1..=10).contains(&n));
1183 }
1184 }
1185
1186 #[test]
1187 fn parameter_generate_boundary_hits_an_endpoint() {
1188 let p = Parameter::Integer(
1189 IntegerParameter::range(pname("n"), 1, 10).unwrap(),
1190 );
1191 let mut r = rng();
1192 let mut seen = BTreeSet::new();
1193 for _ in 0..50 {
1194 let v = p.generate_boundary(&mut r);
1195 seen.insert(v.as_integer().unwrap());
1196 }
1197 assert!(seen.contains(&1) || seen.contains(&10));
1198 }
1199
1200 #[test]
1201 fn parameter_validate_catches_kind_and_domain() {
1202 let p = Parameter::Integer(
1203 IntegerParameter::range(pname("n"), 1, 10).unwrap(),
1204 );
1205 let ok = Value::integer(pname("n"), 5, None);
1206 assert!(p.validate(&ok).is_passed());
1207
1208 let out_of_range = Value::integer(pname("n"), 42, None);
1209 assert!(p.validate(&out_of_range).is_failed());
1210
1211 let wrong_kind = Value::boolean(pname("n"), true, None);
1212 assert!(p.validate(&wrong_kind).is_failed());
1213 }
1214
1215 #[test]
1216 fn parameter_satisfies_hits_constraint_via_boundaries() {
1217 let p = Parameter::Integer(
1218 IntegerParameter::range(pname("n"), 1, 10).unwrap(),
1219 );
1220 assert!(p.satisfies(&Constraint::Integer(IntConstraint::Min { n: 5 })));
1221 assert!(!p.satisfies(&Constraint::Integer(IntConstraint::Min { n: 100 })));
1222 assert!(!p.satisfies(&Constraint::Boolean(BoolConstraint::EqTo { b: true })));
1224 }
1225
1226 #[test]
1227 fn parameter_attributed_trait() {
1228 let p = Parameter::Integer(
1229 IntegerParameter::range(pname("n"), 1, 10).unwrap(),
1230 );
1231 assert!(<Parameter as Attributed>::labels(&p).is_empty());
1232 assert!(<Parameter as Attributed>::tags(&p).is_empty());
1233 }
1234
1235 #[test]
1236 fn parameter_serde_roundtrip() {
1237 let p = Parameter::Integer(
1238 IntegerParameter::range(pname("n"), 1, 10)
1239 .unwrap()
1240 .with_default(5)
1241 .unwrap(),
1242 );
1243 let json = serde_json::to_string(&p).unwrap();
1244 let back: Parameter = serde_json::from_str(&json).unwrap();
1245 assert_eq!(p, back);
1246 }
1247
1248 #[test]
1251 fn derived_parameter_computes_from_bindings() {
1252 use crate::expression::{BinOp, Expression, Literal};
1253 let expr = Expression::binop(
1254 BinOp::Mul,
1255 Expression::reference(pname("threads")),
1256 Expression::literal(Literal::Integer { value: 2 }),
1257 );
1258 let p = DerivedParameter::new(pname("double_threads"), ValueKind::Integer, expr).unwrap();
1259
1260 let mut bindings = ValueBindings::new();
1261 bindings.insert(pname("threads"), Value::integer(pname("threads"), 8, None));
1262 let out = p.compute(&bindings).unwrap();
1263 assert_eq!(out.as_integer(), Some(16));
1264 }
1265
1266 #[test]
1267 fn derived_parameter_rejects_selection_kind() {
1268 use crate::expression::{Expression, Literal};
1269 let err = DerivedParameter::new(
1270 pname("bad"),
1271 ValueKind::Selection,
1272 Expression::literal(Literal::Integer { value: 1 }),
1273 )
1274 .unwrap_err();
1275 assert!(matches!(
1276 err,
1277 crate::Error::Parameter(ParameterError::DerivedSelectionUnsupported)
1278 ));
1279 }
1280
1281 #[test]
1282 fn derived_parameter_kind_mismatch_errors() {
1283 use crate::expression::{Expression, Literal};
1284 let p = DerivedParameter::new(
1286 pname("d"),
1287 ValueKind::Double,
1288 Expression::literal(Literal::Integer { value: 1 }),
1289 )
1290 .unwrap();
1291 let err = p.compute(&ValueBindings::new()).unwrap_err();
1292 assert!(matches!(err, DerivationError::TypeMismatch { .. }));
1293 }
1294
1295 #[test]
1296 fn outer_parameter_with_derived_variant() {
1297 use crate::expression::{Expression, Literal};
1298 let p = Parameter::Derived(
1299 DerivedParameter::new(
1300 pname("c"),
1301 ValueKind::Integer,
1302 Expression::literal(Literal::Integer { value: 3 }),
1303 )
1304 .unwrap(),
1305 );
1306 assert_eq!(p.kind(), ValueKind::Integer);
1307 assert!(p.domain().is_none());
1309 }
1310}