Skip to main content

paramodel_elements/
domain.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parameter domains.
5//!
6//! A domain is the set of values a parameter may legally take. Per
7//! SRD-0004 D2 each parameter kind owns a sub-enum of its admissible
8//! shapes (`IntegerDomain`, `DoubleDomain`, `StringDomain`,
9//! `SelectionDomain`; `Boolean` has no payload), and a view enum
10//! [`Domain`] borrows into whichever sub-enum a [`Parameter`] carries.
11//! This module defines the owned sub-enums, their native-type
12//! operations (membership, cardinality, boundaries, sampling,
13//! enumeration), the [`Domain`] view wrapper that lifts those ops into
14//! [`Value`] form, and the [`SelectionResolver`] trait the Selection
15//! `External` variant defers to at plan-compilation time.
16//!
17//! Gaps flagged for later SRD revisions:
18//!
19//! - `StringDomain::Regex` sampling. The `regex` crate does not provide
20//!   regex-backed generation; a future slice can plug in a generator
21//!   crate or defer sampling to the caller. Until then, [`Domain::sample`]
22//!   panics for the `Regex` variant.
23//! - `SelectionDomain::External` sampling. Registry-aware sampling is
24//!   specified in SRD-0004 D15 but not yet wired up; [`Domain::sample`]
25//!   panics for the `External` variant.
26//!
27//! [`Parameter`]: crate::value::Value
28
29use 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// ---------------------------------------------------------------------------
43// Cardinality.
44// ---------------------------------------------------------------------------
45
46/// How many distinct values a domain contains.
47///
48/// Cardinality is reported at domain granularity, not value-kind
49/// granularity. It is `Unbounded` for continuous (`DoubleDomain::Range`)
50/// and open-ended (`StringDomain::Any` / `Regex`,
51/// `SelectionDomain::External`) domains; otherwise it is `Finite(n)`.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53#[serde(tag = "kind", rename_all = "snake_case")]
54pub enum Cardinality {
55    /// Finite count of distinct values, saturating at `u64::MAX` if the
56    /// true count would exceed it.
57    Finite {
58        /// The count.
59        count: u64,
60    },
61    /// Too many values to count (continuous or open-ended).
62    Unbounded,
63}
64
65impl Cardinality {
66    /// Construct a finite cardinality.
67    #[must_use]
68    pub const fn finite(count: u64) -> Self {
69        Self::Finite { count }
70    }
71}
72
73// ---------------------------------------------------------------------------
74// DomainError.
75// ---------------------------------------------------------------------------
76
77/// Errors produced by domain constructors and operations.
78#[derive(Debug, thiserror::Error, PartialEq, Eq)]
79pub enum DomainError {
80    /// The domain is not enumerable (continuous or open-ended). Returned
81    /// by [`Domain::enumerate`].
82    #[error("domain is not enumerable")]
83    NotEnumerable,
84
85    /// A numeric range was rejected because `min > max`, a bound was
86    /// `NaN`, or a bound was non-finite.
87    #[error("invalid range: min={min}, max={max}")]
88    InvalidRange {
89        /// The rejected minimum bound as a display string.
90        min: String,
91        /// The rejected maximum bound as a display string.
92        max: String,
93    },
94
95    /// A discrete integer domain was constructed with zero values.
96    #[error("discrete integer domain must contain at least one value")]
97    EmptyDiscrete,
98
99    /// A fixed selection domain was constructed with zero values.
100    #[error("fixed selection domain must contain at least one value")]
101    EmptySelection,
102
103    /// A selection domain was constructed with `max_selections = 0`.
104    #[error("selection domain max_selections must be at least 1")]
105    ZeroMaxSelections,
106}
107
108// ---------------------------------------------------------------------------
109// RegexPattern.
110// ---------------------------------------------------------------------------
111
112/// A validated regex, carrying both the source pattern and the compiled
113/// automaton.
114///
115/// Equality and hashing use the source string (the compiled automaton
116/// has no stable representation). Serde emits the source string and
117/// recompiles on deserialisation — the only place in paramodel where
118/// deserialisation can fail due to payload validity.
119#[derive(Debug, Clone)]
120pub struct RegexPattern {
121    source:   String,
122    compiled: Regex,
123}
124
125impl RegexPattern {
126    /// Compile a source pattern.
127    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    /// Borrow the source pattern.
134    #[must_use]
135    pub fn as_str(&self) -> &str {
136        &self.source
137    }
138
139    /// Test whether the given text matches.
140    #[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// ---------------------------------------------------------------------------
173// ResolverId.
174// ---------------------------------------------------------------------------
175
176/// Opaque identifier naming an external selection resolver.
177///
178/// Resolver ids are short ASCII identifiers — they follow the same
179/// validation rules as a [`ParameterName`] so they show up cleanly in
180/// logs and generated code.
181#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
182pub struct ResolverId(String);
183
184const RESOLVER_ID_MAX: usize = 64;
185
186impl ResolverId {
187    /// Construct a new id, validating the candidate.
188    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    /// Borrow the inner string.
212    #[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// ---------------------------------------------------------------------------
238// IntegerDomain.
239// ---------------------------------------------------------------------------
240
241/// Integer domain: either an inclusive numeric range or a finite discrete
242/// set.
243#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
244#[serde(tag = "shape", rename_all = "snake_case")]
245pub enum IntegerDomain {
246    /// Inclusive `[min, max]`.
247    Range {
248        /// Lower bound (inclusive).
249        min: i64,
250        /// Upper bound (inclusive).
251        max: i64,
252    },
253    /// Explicit value set; at least one element.
254    Discrete {
255        /// The permitted values.
256        values: BTreeSet<i64>,
257    },
258}
259
260impl IntegerDomain {
261    /// Construct an inclusive range. Rejects `min > max`.
262    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    /// Construct a discrete domain. Rejects empty sets.
273    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    /// Membership test on the native payload.
281    #[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    /// Count of distinct values. Saturates at `u64::MAX` if a range's
290    /// width exceeds `u64`.
291    #[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    /// Boundary values (at most two).
306    #[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    /// Uniformly sample a value using the given RNG.
332    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    /// Iterator over every value in the domain.
343    #[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// ---------------------------------------------------------------------------
353// DoubleDomain.
354// ---------------------------------------------------------------------------
355
356/// Double-precision float domain: inclusive range with finite, non-`NaN`
357/// bounds.
358#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
359#[serde(tag = "shape", rename_all = "snake_case")]
360pub enum DoubleDomain {
361    /// Inclusive `[min, max]`.
362    Range {
363        /// Lower bound (inclusive).
364        min: f64,
365        /// Upper bound (inclusive).
366        max: f64,
367    },
368}
369
370impl DoubleDomain {
371    /// Construct an inclusive range. Rejects `NaN`, non-finite bounds,
372    /// and reversed bounds.
373    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    /// Membership test. `NaN` is never contained.
384    #[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    /// Continuous ranges are `Unbounded`.
395    #[must_use]
396    pub const fn cardinality(&self) -> Cardinality {
397        Cardinality::Unbounded
398    }
399
400    /// Range endpoints, deduplicated if `min == max`.
401    #[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    /// Uniformly sample from the range.
416    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// ---------------------------------------------------------------------------
424// StringDomain.
425// ---------------------------------------------------------------------------
426
427/// String domain: accept-all or regex-matched.
428#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
429#[serde(tag = "shape", rename_all = "snake_case")]
430pub enum StringDomain {
431    /// Any UTF-8 string is a member.
432    Any,
433    /// Members are the strings matching this regex.
434    Regex {
435        /// The compiled pattern.
436        pattern: RegexPattern,
437    },
438}
439
440impl StringDomain {
441    /// Construct an accept-all domain.
442    #[must_use]
443    pub const fn any() -> Self {
444        Self::Any
445    }
446
447    /// Construct a regex domain from a source pattern.
448    pub fn regex(source: impl Into<String>) -> Result<Self> {
449        Ok(Self::Regex {
450            pattern: RegexPattern::new(source)?,
451        })
452    }
453
454    /// Membership test on the native payload.
455    #[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    /// String domains are `Unbounded` regardless of shape.
464    #[must_use]
465    pub const fn cardinality(&self) -> Cardinality {
466        Cardinality::Unbounded
467    }
468
469    /// Conservative boundary set. `Any` returns `[""]`; `Regex` also
470    /// returns `[""]` as a placeholder until regex-sampling lands —
471    /// see module docs.
472    #[must_use]
473    pub fn boundaries_native(&self) -> Vec<String> {
474        vec![String::new()]
475    }
476
477    /// Conservative sample. Returns `""` for `Any`. For `Regex`,
478    /// sampling is unimplemented; see module docs.
479    ///
480    /// # Panics
481    ///
482    /// Panics on `StringDomain::Regex`.
483    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// ---------------------------------------------------------------------------
495// SelectionDomain.
496// ---------------------------------------------------------------------------
497
498/// Selection domain: a set the user can pick multiple items from.
499#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
500#[serde(tag = "shape", rename_all = "snake_case")]
501pub enum SelectionDomain {
502    /// The available items are enumerated inline.
503    Fixed {
504        /// The available items, in authored order.
505        values:         IndexSet<SelectionItem>,
506        /// Maximum number of items that may be selected together.
507        max_selections: u32,
508    },
509    /// The available items come from an external resolver looked up by
510    /// id at plan-compilation time.
511    External {
512        /// Opaque resolver id for the registry.
513        resolver:       ResolverId,
514        /// Maximum number of items that may be selected together.
515        max_selections: u32,
516    },
517}
518
519impl SelectionDomain {
520    /// Construct a fixed selection domain. Rejects empty value sets or
521    /// zero `max_selections`.
522    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    /// Construct an external selection domain.
536    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    /// Maximum number of items selectable at once.
547    #[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    /// Membership test: every item in `items` must be a legal
557    /// member. For `External` variants, legality can only be confirmed
558    /// through the resolver — [`Domain::contains`] handles that case by
559    /// degrading to shape-only checks.
560    #[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    /// Available-item count for the `Fixed` variant.
572    #[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    /// Single-item selections at the first and last boundary items.
581    #[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    /// Sample a random subset of size `1..=max_selections`.
605    ///
606    /// # Panics
607    ///
608    /// Panics on `SelectionDomain::External` — registry-aware sampling
609    /// is a future slice; see module docs.
610    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// ---------------------------------------------------------------------------
631// SelectionResolver traits.
632// ---------------------------------------------------------------------------
633
634/// Label attached to an external selection value for display purposes.
635#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636pub struct LabeledEntry {
637    /// Canonical value id.
638    pub value: SelectionItem,
639    /// Human-readable label.
640    pub label: String,
641}
642
643/// Resolves the set of valid values for one external selection.
644///
645/// Embedding systems implement this trait to plug their domain-specific
646/// catalogs (datasets, templates, …) into paramodel. The trait object
647/// lives outside the parameter algebra (see SRD-0004 D15): parameters
648/// hold only the opaque [`ResolverId`], and the registry is consulted at
649/// plan-compilation / binding time.
650pub trait SelectionResolver: Send + Sync + std::fmt::Debug + 'static {
651    /// Resolver identity.
652    fn id(&self) -> &ResolverId;
653
654    /// Current snapshot of valid values. Cheap to call; implementations
655    /// may cache internally.
656    fn valid_values(&self) -> Result<IndexSet<SelectionItem>>;
657
658    /// Membership test. The default delegates to [`Self::valid_values`].
659    fn is_valid(&self, value: &SelectionItem) -> Result<bool> {
660        Ok(self.valid_values()?.contains(value))
661    }
662
663    /// Human-readable description for UI / logs.
664    fn describe(&self) -> &str;
665}
666
667/// Extension trait: values carry UI labels in addition to their
668/// canonical ids.
669pub trait LabeledSelectionResolver: SelectionResolver {
670    /// Labelled version of [`SelectionResolver::valid_values`].
671    fn labeled_values(&self) -> Result<Vec<LabeledEntry>>;
672}
673
674/// Host registry. The host (typically the embedding system) provides an
675/// implementation that knows how to look up resolvers by id.
676pub trait SelectionResolverRegistry: Send + Sync + std::fmt::Debug + 'static {
677    /// Resolver lookup. Returns `None` if the id is unknown.
678    fn get(&self, id: &ResolverId) -> Option<Arc<dyn SelectionResolver>>;
679
680    /// Ids of every registered resolver.
681    fn ids(&self) -> Vec<ResolverId>;
682}
683
684// ---------------------------------------------------------------------------
685// Domain view.
686// ---------------------------------------------------------------------------
687
688/// Borrowed view over a parameter's domain, pinned to the parameter's
689/// name so operations can produce properly-provenanced [`Value`]s.
690///
691/// This is a small deviation from SRD-0004 §Domain, which shows the
692/// view enum without the parameter reference. Threading the name
693/// through is the simplest way to let `sample`, `boundary_values`, and
694/// `enumerate` hand back owned [`Value`]s without asking the caller to
695/// pass the name at every call site.
696#[derive(Debug, Clone, Copy)]
697pub enum Domain<'a> {
698    /// Integer domain view.
699    Integer {
700        /// Owning parameter's name.
701        parameter: &'a ParameterName,
702        /// Borrowed per-kind domain.
703        domain:    &'a IntegerDomain,
704    },
705    /// Double domain view.
706    Double {
707        /// Owning parameter's name.
708        parameter: &'a ParameterName,
709        /// Borrowed per-kind domain.
710        domain:    &'a DoubleDomain,
711    },
712    /// Boolean domain view — no payload.
713    Boolean {
714        /// Owning parameter's name.
715        parameter: &'a ParameterName,
716    },
717    /// String domain view.
718    String {
719        /// Owning parameter's name.
720        parameter: &'a ParameterName,
721        /// Borrowed per-kind domain.
722        domain:    &'a StringDomain,
723    },
724    /// Selection domain view.
725    Selection {
726        /// Owning parameter's name.
727        parameter: &'a ParameterName,
728        /// Borrowed per-kind domain.
729        domain:    &'a SelectionDomain,
730    },
731}
732
733impl<'a> Domain<'a> {
734    /// The owning parameter's name.
735    #[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    /// Membership test. Kind-mismatched values always return `false`.
747    /// For `SelectionDomain::External`, only the shape is checked —
748    /// full validation requires a resolver and is deferred.
749    #[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    /// Count of distinct values.
773    #[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    /// Boundary values, lifted to `Value`.
785    #[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    /// Uniformly sample a value from the domain.
816    ///
817    /// # Panics
818    ///
819    /// Panics for `StringDomain::Regex` and `SelectionDomain::External`
820    /// — see module docs.
821    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    /// Enumerate every value in the domain as `Value`s. Fails for
842    /// continuous and open-ended domains.
843    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// ---------------------------------------------------------------------------
888// Tests.
889// ---------------------------------------------------------------------------
890
891#[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    // ---------- Cardinality / DomainError ----------
906
907    #[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    // ---------- RegexPattern ----------
914
915    #[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    // ---------- ResolverId ----------
939
940    #[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    // ---------- IntegerDomain ----------
955
956    #[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    // ---------- DoubleDomain ----------
1023
1024    #[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    // ---------- StringDomain ----------
1049
1050    #[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    // ---------- SelectionDomain ----------
1072
1073    #[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    // ---------- Domain view ----------
1119
1120    #[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    // ---------- serde ----------
1241
1242    #[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}