Skip to main content

paramodel_elements/
parameter.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parameters.
5//!
6//! The five concrete parameter kinds from SRD-0004 — `IntegerParameter`,
7//! `DoubleParameter`, `BooleanParameter`, `StringParameter`,
8//! `SelectionParameter` — plus `DerivedParameter` (values computed from
9//! an [`Expression`]). The outer [`Parameter`] enum dispatches name,
10//! kind, labels, tags, default, generation, and validation across
11//! variants.
12//!
13//! Constructors validate the inputs (range order, domain non-emptiness,
14//! max-selections ≥ 1, …). Builder-style setters (`with_default`,
15//! `with_constraint`, `with_label`, `with_tag`) return
16//! `Result<Self>` when they can fail — `with_default` rejects a default
17//! outside the domain or violating a registered constraint;
18//! `with_label`/`with_tag` reject keys that already live on the other
19//! tier (namespace uniqueness per SRD-0005 D5).
20
21use 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// ---------------------------------------------------------------------------
43// ParameterError.
44// ---------------------------------------------------------------------------
45
46/// Errors from parameter construction and builder-style setters.
47#[derive(Debug, thiserror::Error)]
48pub enum ParameterError {
49    /// `with_default` was given a value outside the domain.
50    #[error("default value is not in the parameter's domain")]
51    DefaultNotInDomain,
52
53    /// `with_default` was given a value that fails a registered constraint.
54    #[error("default value violates a registered constraint")]
55    DefaultViolatesConstraint,
56
57    /// A constraint's kind differs from the parameter's.
58    #[error("constraint kind does not match parameter kind ({parameter_kind:?})")]
59    ConstraintKindMismatch {
60        /// The parameter's kind.
61        parameter_kind: ValueKind,
62    },
63
64    /// A `DerivedParameter` was constructed with a Selection kind
65    /// (expressions only produce integer/double/boolean/string).
66    #[error("derived parameters cannot produce Selection values")]
67    DerivedSelectionUnsupported,
68
69    /// `with_label` or `with_tag` introduced a key already present in
70    /// another tier on this parameter.
71    #[error(transparent)]
72    Attribute(#[from] AttributeError),
73}
74
75// ---------------------------------------------------------------------------
76// IntegerParameter.
77// ---------------------------------------------------------------------------
78
79/// A parameter that observes an `i64`.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct IntegerParameter {
82    /// Parameter name.
83    pub name:        ParameterName,
84    /// Value domain.
85    pub domain:      IntegerDomain,
86    /// Registered constraints; all must hold.
87    #[serde(default)]
88    pub constraints: Vec<IntConstraint>,
89    /// Optional default value.
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub default:     Option<i64>,
92    /// Intrinsic-fact map.
93    #[serde(default)]
94    pub labels:      Labels,
95    /// Organisational tag map.
96    #[serde(default)]
97    pub tags:        Tags,
98}
99
100impl IntegerParameter {
101    /// Construct a parameter backed by an inclusive numeric range.
102    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    /// Construct a parameter backed by a discrete value set.
114    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    /// Set the default value. Rejects out-of-domain defaults and
126    /// defaults that violate registered constraints.
127    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    /// Append a constraint.
141    #[must_use]
142    pub fn with_constraint(mut self, c: IntConstraint) -> Self {
143        self.constraints.push(c);
144        self
145    }
146
147    /// Add a label. Rejects keys already used as a tag on this parameter.
148    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    /// Add a tag. Rejects keys already used as a label on this parameter.
155    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// ---------------------------------------------------------------------------
163// DoubleParameter.
164// ---------------------------------------------------------------------------
165
166/// A parameter that observes an `f64`.
167#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
168pub struct DoubleParameter {
169    /// Parameter name.
170    pub name:        ParameterName,
171    /// Value domain.
172    pub domain:      DoubleDomain,
173    /// Registered constraints.
174    #[serde(default)]
175    pub constraints: Vec<DoubleConstraint>,
176    /// Optional default value.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub default:     Option<f64>,
179    /// Intrinsic facts.
180    #[serde(default)]
181    pub labels:      Labels,
182    /// Organisational tags.
183    #[serde(default)]
184    pub tags:        Tags,
185}
186
187impl DoubleParameter {
188    /// Construct a parameter backed by an inclusive range.
189    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    /// Set the default value. Rejects out-of-domain defaults and
201    /// defaults that violate registered constraints.
202    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    /// Append a constraint.
216    #[must_use]
217    pub fn with_constraint(mut self, c: DoubleConstraint) -> Self {
218        self.constraints.push(c);
219        self
220    }
221
222    /// Add a label. Rejects keys already used as a tag.
223    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    /// Add a tag. Rejects keys already used as a label.
230    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// ---------------------------------------------------------------------------
238// BooleanParameter.
239// ---------------------------------------------------------------------------
240
241/// A parameter that observes a `bool`.
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243pub struct BooleanParameter {
244    /// Parameter name.
245    pub name:        ParameterName,
246    /// Registered constraints.
247    #[serde(default)]
248    pub constraints: Vec<BoolConstraint>,
249    /// Optional default value.
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub default:     Option<bool>,
252    /// Intrinsic facts.
253    #[serde(default)]
254    pub labels:      Labels,
255    /// Organisational tags.
256    #[serde(default)]
257    pub tags:        Tags,
258}
259
260impl BooleanParameter {
261    /// Construct a new boolean parameter.
262    #[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    /// Set the default. Rejects if a registered constraint forbids it.
274    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    /// Append a constraint.
285    #[must_use]
286    pub fn with_constraint(mut self, c: BoolConstraint) -> Self {
287        self.constraints.push(c);
288        self
289    }
290
291    /// Add a label.
292    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    /// Add a tag.
299    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// ---------------------------------------------------------------------------
307// StringParameter.
308// ---------------------------------------------------------------------------
309
310/// A parameter that observes a `String`.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
312pub struct StringParameter {
313    /// Parameter name.
314    pub name:        ParameterName,
315    /// Value domain (any string or regex-matched).
316    pub domain:      StringDomain,
317    /// Registered constraints.
318    #[serde(default)]
319    pub constraints: Vec<StringConstraint>,
320    /// Optional default value.
321    #[serde(default, skip_serializing_if = "Option::is_none")]
322    pub default:     Option<String>,
323    /// Intrinsic facts.
324    #[serde(default)]
325    pub labels:      Labels,
326    /// Organisational tags.
327    #[serde(default)]
328    pub tags:        Tags,
329}
330
331impl StringParameter {
332    /// Construct a parameter accepting any string.
333    #[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    /// Construct a parameter restricted to strings matching `pattern`.
346    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    /// Set the default value. Rejects out-of-domain defaults and
358    /// defaults that violate registered constraints.
359    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    /// Append a constraint.
374    #[must_use]
375    pub fn with_constraint(mut self, c: StringConstraint) -> Self {
376        self.constraints.push(c);
377        self
378    }
379
380    /// Add a label.
381    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    /// Add a tag.
388    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// ---------------------------------------------------------------------------
396// SelectionParameter.
397// ---------------------------------------------------------------------------
398
399/// A parameter whose value is an ordered multi-item selection.
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct SelectionParameter {
402    /// Parameter name.
403    pub name:        ParameterName,
404    /// Value domain (inline values or an external resolver).
405    pub domain:      SelectionDomain,
406    /// Registered constraints.
407    #[serde(default)]
408    pub constraints: Vec<SelectionConstraint>,
409    /// Optional default selection (must be a subset of the domain).
410    #[serde(default, skip_serializing_if = "Option::is_none")]
411    pub default:     Option<IndexSet<SelectionItem>>,
412    /// Intrinsic facts.
413    #[serde(default)]
414    pub labels:      Labels,
415    /// Organisational tags.
416    #[serde(default)]
417    pub tags:        Tags,
418}
419
420impl SelectionParameter {
421    /// Construct a parameter backed by an inline value set.
422    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    /// Construct a parameter backed by an external resolver.
438    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    /// Set the default selection.
454    pub fn with_default(
455        mut self,
456        default: IndexSet<SelectionItem>,
457    ) -> crate::Result<Self> {
458        // Fixed domain: every item must be a legal member, and count must
459        // be ≤ max_selections. External domains can't be shape-checked
460        // without a resolver, so we accept any default there.
461        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    /// Append a constraint.
476    #[must_use]
477    pub fn with_constraint(mut self, c: SelectionConstraint) -> Self {
478        self.constraints.push(c);
479        self
480    }
481
482    /// Add a label.
483    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    /// Add a tag.
490    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// ---------------------------------------------------------------------------
498// DerivedParameter.
499// ---------------------------------------------------------------------------
500
501/// A parameter whose value is computed from other already-bound values
502/// by an [`Expression`].
503///
504/// Derived parameters may not be used as axes (SRD-0004 D9) — that's
505/// enforced by the test-plan layer.
506#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
507pub struct DerivedParameter {
508    /// Parameter name.
509    pub name:       ParameterName,
510    /// Declared output kind (Integer/Double/Boolean/String).
511    pub kind:       ValueKind,
512    /// The expression that produces the value.
513    pub expression: Expression,
514    /// Intrinsic facts.
515    #[serde(default)]
516    pub labels:     Labels,
517    /// Organisational tags.
518    #[serde(default)]
519    pub tags:       Tags,
520}
521
522impl DerivedParameter {
523    /// Construct a derived parameter. Rejects `ValueKind::Selection`.
524    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    /// Evaluate the expression against the given bindings and wrap the
542    /// result in a [`Value`] with `Derived` provenance.
543    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    /// Add a label.
564    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    /// Add a tag.
571    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
578// ---------------------------------------------------------------------------
579// Namespace check helpers.
580// ---------------------------------------------------------------------------
581
582fn 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// ---------------------------------------------------------------------------
603// Outer Parameter enum.
604// ---------------------------------------------------------------------------
605
606/// Kind-tagged parameter.
607///
608/// See per-variant structs for constructors and builder-style setters.
609/// Dispatch methods (`name`, `kind`, `default`, `generate`, …) forward
610/// to the inner variant.
611#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
612#[serde(tag = "kind", rename_all = "snake_case")]
613pub enum Parameter {
614    /// Integer parameter.
615    Integer(IntegerParameter),
616    /// Double parameter.
617    Double(DoubleParameter),
618    /// Boolean parameter.
619    Boolean(BooleanParameter),
620    /// String parameter.
621    String(StringParameter),
622    /// Selection parameter.
623    Selection(SelectionParameter),
624    /// Derived parameter.
625    Derived(DerivedParameter),
626}
627
628impl Parameter {
629    /// Parameter name.
630    #[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    /// Parameter kind.
643    #[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    /// Borrowed domain view. Returns `None` for [`Self::Derived`]
656    /// (derived parameters don't own a domain in this SRD tier).
657    #[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    /// The default value, wrapped in [`Value`] with `Default` provenance,
682    /// if one is set.
683    #[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    /// Pick a value. Uses the registered default if present, else
709    /// samples uniformly from the domain.
710    ///
711    /// # Panics
712    ///
713    /// Panics when called on [`Self::Derived`] (derived parameters are
714    /// computed from bindings, not sampled) or on domains whose
715    /// sampling is not implemented (regex, external selection).
716    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    /// Always draws from the domain, ignoring any default.
724    ///
725    /// # Panics
726    ///
727    /// Panics on [`Self::Derived`] and on domains whose sampling is
728    /// unimplemented.
729    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    /// Pick a boundary value from the domain.
740    ///
741    /// # Panics
742    ///
743    /// Panics on [`Self::Derived`] (no domain).
744    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    /// Validate a candidate [`Value`] against this parameter's kind,
757    /// domain, and registered constraints.
758    #[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                // Only fixed domains can be shape-checked without a
812                // resolver; external selections accept anything of the
813                // right shape here.
814                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                // Derived parameters have no domain to check at this
827                // layer — their validity is the expression's validity.
828            }
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    /// Best-effort "is any value in the domain satisfied by this
840    /// constraint?" check. Tests the domain's boundary values plus up
841    /// to eight deterministic random samples (seeded for
842    /// reproducibility). False negatives possible; false positives are
843    /// not.
844    ///
845    /// Returns `false` for derived parameters (no domain to probe),
846    /// for kind mismatches, and for domains that panic on sampling
847    /// (regex, external selection) — those would need a richer check.
848    #[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                // Only probe boundaries — sampling "Any" is empty and
885                // regex domain sampling is a documented gap.
886                sc.test("")
887            }
888            (Self::Selection(p), Constraint::Selection(sc)) => {
889                // Boundaries only; external domains return no boundaries
890                // without a resolver.
891                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
904// Convenience helpers on Constraint for the dispatcher above.
905impl 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
920// ---------------------------------------------------------------------------
921// Attributed impls.
922// ---------------------------------------------------------------------------
923
924impl 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// ---------------------------------------------------------------------------
1003// Tests.
1004// ---------------------------------------------------------------------------
1005
1006#[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    // ---------- IntegerParameter ----------
1020
1021    #[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        // Default outside domain is rejected.
1038        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        // Default violating a constraint is rejected.
1048        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        // Adding a tag with the same key is rejected. `?` converts
1066        // AttributeError straight through crate::Error::Attribute, so
1067        // we match that variant directly.
1068        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    // ---------- DoubleParameter ----------
1078
1079    #[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    // ---------- BooleanParameter ----------
1089
1090    #[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    // ---------- StringParameter ----------
1109
1110    #[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    // ---------- SelectionParameter ----------
1123
1124    #[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    // ---------- Outer Parameter ----------
1144
1145    #[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        // Default provenance.
1167        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        // Kind mismatch yields false.
1223        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    // ---------- DerivedParameter ----------
1249
1250    #[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        // Declared as Double but expression yields Integer.
1285        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        // Derived has no domain.
1308        assert!(p.domain().is_none());
1309    }
1310}