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