Skip to main content

problemreductions/registry/
schema.rs

1//! Problem schema registration via inventory.
2
3use super::FieldInfo;
4use serde::Serialize;
5use std::fmt;
6use std::str::FromStr;
7
8/// Structural category used to organize problem implementations and catalog output.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
10#[serde(rename_all = "lowercase")]
11pub enum ProblemCategory {
12    Algebraic,
13    Formula,
14    Graph,
15    Misc,
16    Set,
17}
18
19impl ProblemCategory {
20    pub const ALL: [Self; 5] = [
21        Self::Algebraic,
22        Self::Formula,
23        Self::Graph,
24        Self::Misc,
25        Self::Set,
26    ];
27
28    pub const fn as_str(self) -> &'static str {
29        match self {
30            Self::Algebraic => "algebraic",
31            Self::Formula => "formula",
32            Self::Graph => "graph",
33            Self::Misc => "misc",
34            Self::Set => "set",
35        }
36    }
37}
38
39impl fmt::Display for ProblemCategory {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str(self.as_str())
42    }
43}
44
45/// Error returned when a catalog category is not one of the five supported values.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct ParseProblemCategoryError(String);
48
49impl fmt::Display for ParseProblemCategoryError {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        let expected = ProblemCategory::ALL.map(ProblemCategory::as_str).join(", ");
52        write!(
53            formatter,
54            "unknown problem category `{}`; expected one of: {expected}",
55            self.0,
56        )
57    }
58}
59
60impl std::error::Error for ParseProblemCategoryError {}
61
62impl FromStr for ProblemCategory {
63    type Err = ParseProblemCategoryError;
64
65    fn from_str(value: &str) -> Result<Self, Self::Err> {
66        Self::ALL
67            .into_iter()
68            .find(|category| category.as_str() == value)
69            .ok_or_else(|| ParseProblemCategoryError(value.to_string()))
70    }
71}
72
73/// A declared variant dimension for a problem type.
74///
75/// Describes one axis of variation (e.g., graph type, weight type) with
76/// its default value and the set of allowed values.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct VariantDimension {
79    /// Dimension key (e.g., `"graph"`, `"weight"`, `"k"`).
80    pub key: &'static str,
81    /// Default value for this dimension (e.g., `"SimpleGraph"`).
82    pub default_value: &'static str,
83    /// All allowed values for this dimension.
84    pub allowed_values: &'static [&'static str],
85}
86
87impl VariantDimension {
88    /// Create a new variant dimension.
89    pub const fn new(
90        key: &'static str,
91        default_value: &'static str,
92        allowed_values: &'static [&'static str],
93    ) -> Self {
94        Self {
95            key,
96            default_value,
97            allowed_values,
98        }
99    }
100}
101
102/// A registered problem schema entry for static inventory registration.
103///
104/// Category is required rather than inferred from source location:
105///
106/// ```compile_fail
107/// use problemreductions::registry::ProblemSchemaEntry;
108///
109/// let _schema = ProblemSchemaEntry {
110///     name: "Example",
111///     display_name: "Example",
112///     aliases: &[],
113///     dimensions: &[],
114///     module_path: module_path!(),
115///     description: "Example schema",
116///     fields: &[],
117/// };
118/// ```
119pub struct ProblemSchemaEntry {
120    /// Problem name (e.g., "MaximumIndependentSet").
121    pub name: &'static str,
122    /// Human-readable display name (e.g., "Maximum Independent Set").
123    pub display_name: &'static str,
124    /// Short aliases for CLI/MCP lookup (e.g., `&["MIS"]`).
125    pub aliases: &'static [&'static str],
126    /// Declared variant dimensions with defaults and allowed values.
127    pub dimensions: &'static [VariantDimension],
128    /// Explicit structural category shown in catalog output.
129    pub category: ProblemCategory,
130    /// Module path from `module_path!()` (e.g., "problemreductions::models::graph::maximum_independent_set").
131    pub module_path: &'static str,
132    /// Human-readable description.
133    pub description: &'static str,
134    /// Inputs accepted when constructing this problem.
135    pub fields: &'static [FieldInfo],
136}
137
138inventory::collect!(ProblemSchemaEntry);
139
140/// JSON-serializable problem schema.
141#[derive(Debug, Clone, Serialize)]
142pub struct ProblemSchemaJson {
143    /// Problem name.
144    pub name: String,
145    /// Problem description.
146    pub description: String,
147    /// Structural catalog category.
148    pub category: ProblemCategory,
149    /// Inputs accepted when constructing this problem.
150    pub fields: Vec<FieldInfoJson>,
151}
152
153/// JSON-serializable field info.
154#[derive(Debug, Clone, Serialize)]
155pub struct FieldInfoJson {
156    /// Field name.
157    pub name: String,
158    /// Field type.
159    pub type_name: String,
160    /// Field description.
161    pub description: String,
162}
163
164/// Collect all registered problem schemas into JSON-serializable form.
165pub fn collect_schemas() -> Vec<ProblemSchemaJson> {
166    let mut schemas: Vec<ProblemSchemaJson> = inventory::iter::<ProblemSchemaEntry>
167        .into_iter()
168        .map(|entry| ProblemSchemaJson {
169            name: entry.name.to_string(),
170            description: entry.description.to_string(),
171            category: entry.category,
172            fields: entry
173                .fields
174                .iter()
175                .map(|f| FieldInfoJson {
176                    name: f.name.to_string(),
177                    type_name: f.type_name.to_string(),
178                    description: f.description.to_string(),
179                })
180                .collect(),
181        })
182        .collect();
183    schemas.sort_by(|a, b| a.name.cmp(&b.name));
184    schemas
185}
186
187#[cfg(test)]
188#[path = "../unit_tests/registry/schema.rs"]
189mod tests;