Skip to main content

problemreductions/registry/
problem_ref.rs

1//! Typed internal problem references with catalog-validated variants.
2
3use super::problem_type::ProblemType;
4use std::collections::BTreeMap;
5
6/// A typed internal reference to a specific problem variant.
7///
8/// Unlike `export::ProblemRef` (a plain DTO), this type validates its
9/// variant dimensions against the catalog at construction time.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ProblemRef {
12    /// Canonical problem name.
13    name: String,
14    /// Validated variant dimensions.
15    variant: BTreeMap<String, String>,
16}
17
18impl ProblemRef {
19    /// Create a `ProblemRef` from positional values, matching them against
20    /// the problem type's declared dimensions.
21    ///
22    /// Values are matched by checking which dimension's allowed_values contains
23    /// each positional value. Unmatched dimensions are filled with defaults.
24    ///
25    /// # Errors
26    ///
27    /// Returns an error if any value doesn't match a dimension's allowed values.
28    pub fn from_values<I, S>(problem_type: &ProblemType, values: I) -> Result<Self, String>
29    where
30        I: IntoIterator<Item = S>,
31        S: AsRef<str>,
32    {
33        // Start with all defaults
34        let mut variant: BTreeMap<String, String> = problem_type.default_variant();
35        let mut matched_dims: Vec<bool> = vec![false; problem_type.dimensions.len()];
36
37        for value in values {
38            let val = value.as_ref();
39            // Find which dimension this value belongs to
40            let dim_idx = problem_type
41                .dimensions
42                .iter()
43                .enumerate()
44                .find(|(i, dim)| !matched_dims[*i] && dim.allowed_values.contains(&val))
45                .map(|(i, _)| i);
46
47            match dim_idx {
48                Some(idx) => {
49                    matched_dims[idx] = true;
50                    let dim = &problem_type.dimensions[idx];
51                    variant.insert(dim.key.to_string(), val.to_string());
52                }
53                None => {
54                    let known: Vec<&str> = problem_type
55                        .dimensions
56                        .iter()
57                        .flat_map(|d| d.allowed_values.iter().copied())
58                        .collect();
59                    return Err(format!(
60                        "Unknown variant value \"{val}\" for {}. Known variants: {known:?}",
61                        problem_type.canonical_name,
62                    ));
63                }
64            }
65        }
66
67        Ok(Self {
68            name: problem_type.canonical_name.to_string(),
69            variant,
70        })
71    }
72
73    /// Create a `ProblemRef` from an explicit variant map, validating against the catalog.
74    pub fn from_map(
75        problem_type: &ProblemType,
76        variant: BTreeMap<String, String>,
77    ) -> Result<Self, String> {
78        // Validate all keys and values
79        for (key, value) in &variant {
80            let dim = problem_type
81                .dimensions
82                .iter()
83                .find(|d| d.key == key.as_str())
84                .ok_or_else(|| {
85                    format!(
86                        "Unknown dimension \"{key}\" for {}",
87                        problem_type.canonical_name
88                    )
89                })?;
90            if !dim.allowed_values.contains(&value.as_str()) {
91                return Err(format!(
92                    "Unknown value \"{value}\" for dimension \"{key}\" of {}. Known variants: {:?}",
93                    problem_type.canonical_name, dim.allowed_values
94                ));
95            }
96        }
97
98        // Fill in defaults for missing dimensions
99        let mut full_variant = problem_type.default_variant();
100        full_variant.extend(variant);
101
102        Ok(Self {
103            name: problem_type.canonical_name.to_string(),
104            variant: full_variant,
105        })
106    }
107
108    /// Get the canonical problem name.
109    pub fn name(&self) -> &str {
110        &self.name
111    }
112
113    /// Get the validated variant map.
114    pub fn variant(&self) -> &BTreeMap<String, String> {
115        &self.variant
116    }
117
118    /// Convert to an `export::ProblemRef` DTO.
119    pub fn to_export_ref(&self) -> crate::export::ProblemRef {
120        crate::export::ProblemRef {
121            name: self.name.clone(),
122            variant: self.variant.clone(),
123        }
124    }
125}
126
127/// Parse a slash-separated problem spec string against the catalog.
128///
129/// Only validates against catalog schema (names, aliases, dimensions).
130/// Does NOT check reduction graph reachability.
131pub fn parse_catalog_problem_ref(input: &str) -> Result<ProblemRef, String> {
132    let parts: Vec<&str> = input.split('/').collect();
133    let raw_name = parts[0];
134    let values: Vec<&str> = parts[1..].to_vec();
135
136    // Resolve name through catalog
137    let problem_type = super::problem_type::find_problem_type_by_alias(raw_name)
138        .ok_or_else(|| format!("Unknown problem type: \"{raw_name}\""))?;
139
140    let effective_values: Vec<String> = values.iter().map(|s| s.to_string()).collect();
141
142    ProblemRef::from_values(&problem_type, &effective_values)
143}
144
145/// Check whether a catalog-validated `ProblemRef` exists in the reduction graph.
146///
147/// Returns the export DTO if the variant is reachable, or an error describing
148/// which graph variants exist for the problem.
149pub fn require_graph_variant(
150    graph: &crate::rules::ReductionGraph,
151    problem_ref: &ProblemRef,
152) -> Result<crate::export::ProblemRef, String> {
153    let known_variants = graph.variants_for(problem_ref.name());
154    if known_variants.iter().any(|v| v == problem_ref.variant()) {
155        return Ok(problem_ref.to_export_ref());
156    }
157
158    Err(format!(
159        "Variant {:?} of {} is schema-valid but not reachable in the reduction graph. \
160         Known graph variants: {:?}",
161        problem_ref.variant(),
162        problem_ref.name(),
163        known_variants
164    ))
165}