problemreductions/registry/
variant.rs1use std::any::Any;
4use std::collections::BTreeMap;
5
6use crate::registry::dyn_problem::DynProblem;
7use crate::registry::FieldInfo;
8
9#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize)]
14pub enum CreateInputCodec {
15 #[default]
17 Auto,
18 Scalar,
20 Json,
22 CommaSeparated,
24 SemicolonSeparated,
26 EdgeList,
28 ArcList,
30 BipartiteEdgeList,
32 EqualityPairList,
34 FunctionalDependencyList,
36 CharacterRows,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
42pub struct CreateInputInfo {
43 pub name: &'static str,
45 pub type_name: &'static str,
47 pub description: &'static str,
49 pub required: bool,
51 pub codec: CreateInputCodec,
53}
54
55impl CreateInputInfo {
56 pub const fn from_field(field: FieldInfo) -> Self {
58 Self {
59 name: field.name,
60 type_name: field.type_name,
61 description: field.description,
62 required: true,
63 codec: CreateInputCodec::Auto,
64 }
65 }
66}
67
68pub trait CreateSpec {
70 const FIELDS: &'static [FieldInfo];
72 fn inputs() -> Vec<CreateInputInfo>;
74
75 fn deserialize_inputs(data: serde_json::Value) -> Result<Self, serde_json::Error>
77 where
78 Self: Sized + serde::de::DeserializeOwned,
79 {
80 serde_json::from_value(data)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
86pub enum ConstructionError {
87 #[error("no registered variant for `{name}` with variant {variant:?}")]
89 UnregisteredVariant {
90 name: String,
92 variant: BTreeMap<String, String>,
94 },
95 #[error("construction inputs must be a JSON object")]
97 ExpectedObject,
98 #[error("construction input `{0}` is declared more than once")]
100 DuplicateInput(String),
101 #[error("unknown construction input(s): {}", .0.join(", "))]
103 UnknownInputs(Vec<String>),
104 #[error("missing required construction input(s): {}", .0.join(", "))]
106 MissingInputs(Vec<String>),
107 #[error("invalid construction input: {0}")]
109 InvalidInput(String),
110 #[error("problem construction failed: {0}")]
112 Conversion(String),
113 #[error("integer overflow during construction: {0}")]
115 IntegerOverflow(String),
116 #[error("non-finite floating-point construction value: {0}")]
118 NonFiniteFloat(String),
119 #[error("inexact integer-to-float construction value: {0}")]
121 InexactFloatConversion(#[from] crate::types::ExactI64ToF64Error),
122}
123
124impl From<String> for ConstructionError {
125 fn from(message: String) -> Self {
126 Self::Conversion(message)
127 }
128}
129
130impl From<&str> for ConstructionError {
131 fn from(message: &str) -> Self {
132 Self::Conversion(message.to_string())
133 }
134}
135
136impl From<std::convert::Infallible> for ConstructionError {
137 fn from(value: std::convert::Infallible) -> Self {
138 match value {}
139 }
140}
141
142pub type ConstructProblemFn =
144 fn(serde_json::Value) -> Result<Box<dyn DynProblem>, ConstructionError>;
145
146#[derive(Clone, Copy)]
148pub struct RandomRegistration {
149 pub inputs: fn() -> Vec<CreateInputInfo>,
151 pub generate: ConstructProblemFn,
153}
154
155pub trait RandomGenerate: DynProblem + Sized {
157 fn inputs() -> Vec<CreateInputInfo>;
159
160 fn generate(data: serde_json::Value) -> Result<Self, ConstructionError>;
162}
163
164pub fn validate_create_inputs(
166 inputs: &[CreateInputInfo],
167 data: &serde_json::Value,
168) -> Result<(), ConstructionError> {
169 validate_input_contract(
170 inputs.iter().map(|input| (input.name, input.required)),
171 data,
172 )
173}
174
175pub fn validate_direct_create_inputs(
180 fields: &[FieldInfo],
181 data: &serde_json::Value,
182) -> Result<(), ConstructionError> {
183 validate_input_contract(fields.iter().map(|field| (field.name, true)), data)
184}
185
186fn validate_input_contract<'a>(
187 inputs: impl IntoIterator<Item = (&'a str, bool)>,
188 data: &serde_json::Value,
189) -> Result<(), ConstructionError> {
190 let object = data.as_object().ok_or(ConstructionError::ExpectedObject)?;
191 let mut declared = BTreeMap::new();
192 for (name, required) in inputs {
193 if declared.insert(name, required).is_some() {
194 return Err(ConstructionError::DuplicateInput(name.to_string()));
195 }
196 }
197
198 let unknown = object
199 .keys()
200 .filter(|name| !declared.contains_key(name.as_str()))
201 .cloned()
202 .collect::<Vec<_>>();
203 if !unknown.is_empty() {
204 return Err(ConstructionError::UnknownInputs(unknown));
205 }
206
207 let missing = declared
208 .into_iter()
209 .filter(|(name, required)| *required && !object.contains_key(*name))
210 .map(|(name, _)| name.to_string())
211 .collect::<Vec<_>>();
212 if !missing.is_empty() {
213 return Err(ConstructionError::MissingInputs(missing));
214 }
215
216 Ok(())
217}
218
219pub struct VariantEntry {
224 pub name: &'static str,
226 pub variant_fn: fn() -> Vec<(&'static str, &'static str)>,
228 pub complexity: &'static str,
230 pub complexity_eval_fn: fn(&dyn Any) -> f64,
234 pub parameter_names_fn: fn() -> &'static [&'static str],
236 pub parameter_measure_fn: fn(&dyn Any) -> crate::types::ProblemParameters,
238 pub is_default: bool,
240 pub aliases: &'static [&'static str],
246 pub create_inputs: Option<fn() -> Vec<CreateInputInfo>>,
249 pub construct_fn: ConstructProblemFn,
251 pub random: Option<RandomRegistration>,
253 pub factory: fn(serde_json::Value) -> Result<Box<dyn DynProblem>, serde_json::Error>,
255 pub serialize_fn: fn(&dyn Any) -> Option<serde_json::Value>,
257}
258
259impl VariantEntry {
260 pub fn inputs(&self) -> Vec<CreateInputInfo> {
262 match self.create_inputs {
263 Some(inputs) => inputs(),
264 None => super::find_problem_type(self.name)
265 .expect("registered variant must have a problem schema")
266 .fields
267 .iter()
268 .cloned()
269 .map(CreateInputInfo::from_field)
270 .collect(),
271 }
272 }
273
274 pub fn variant(&self) -> Vec<(&'static str, &'static str)> {
276 (self.variant_fn)()
277 }
278
279 pub fn variant_map(&self) -> BTreeMap<String, String> {
281 self.variant()
282 .into_iter()
283 .map(|(k, v)| (k.to_string(), v.to_string()))
284 .collect()
285 }
286
287 pub fn parameter_names(&self) -> &'static [&'static str] {
289 (self.parameter_names_fn)()
290 }
291}
292
293pub fn variant_entries() -> Vec<&'static VariantEntry> {
295 inventory::iter::<VariantEntry>().collect()
296}
297
298pub fn validate_variant_parameter_schemas() -> Result<(), Vec<String>> {
300 let mut errors = Vec::new();
301 let mut schemas = BTreeMap::<&str, Vec<&str>>::new();
302
303 for entry in inventory::iter::<VariantEntry> {
304 let names = entry.parameter_names();
305 if names.is_empty() {
306 errors.push(format!("{} has no parameters", variant_label(entry)));
307 continue;
308 }
309
310 let unique = names
311 .iter()
312 .copied()
313 .collect::<std::collections::BTreeSet<_>>();
314 if unique.len() != names.len() {
315 errors.push(format!(
316 "{} declares duplicate parameters: {names:?}",
317 variant_label(entry)
318 ));
319 }
320
321 let canonical = names.to_vec();
322 if let Some(expected) = schemas.get(entry.name) {
323 if expected != &canonical {
324 errors.push(format!(
325 "{} has parameter schema {canonical:?}, expected {expected:?}",
326 variant_label(entry)
327 ));
328 }
329 } else {
330 schemas.insert(entry.name, canonical.clone());
331 }
332
333 let expression = crate::expr::Expr::parse(entry.complexity);
334 for variable in expression.variables() {
335 if !canonical.contains(&variable) {
336 errors.push(format!(
337 "{} complexity references unknown parameter `{variable}`; declared: {canonical:?}",
338 variant_label(entry)
339 ));
340 }
341 }
342 }
343
344 if errors.is_empty() {
345 Ok(())
346 } else {
347 errors.sort();
348 Err(errors)
349 }
350}
351
352pub fn find_variant_entry(
356 name: &str,
357 variant: &BTreeMap<String, String>,
358) -> Option<&'static VariantEntry> {
359 inventory::iter::<VariantEntry>()
360 .find(|entry| entry.name == name && entry.variant_map() == *variant)
361}
362
363pub fn find_variant_by_alias(
372 input: &str,
373) -> Option<(&'static VariantEntry, BTreeMap<String, String>)> {
374 let lower = input.to_lowercase();
375 let entry = inventory::iter::<VariantEntry>()
376 .find(|entry| entry.aliases.iter().any(|a| a.to_lowercase() == lower))?;
377 Some((entry, entry.variant_map()))
378}
379
380pub fn validate_variant_aliases() -> Result<(), Vec<String>> {
387 let mut problem_names: BTreeMap<String, Vec<String>> = BTreeMap::new();
388
389 for problem in super::problem_type::problem_types() {
390 problem_names
391 .entry(problem.canonical_name.to_lowercase())
392 .or_default()
393 .push(format!(
394 "canonical problem name `{}`",
395 problem.canonical_name
396 ));
397
398 for alias in problem.aliases {
399 problem_names
400 .entry(alias.to_lowercase())
401 .or_default()
402 .push(format!(
403 "problem-level alias `{alias}` for `{}`",
404 problem.canonical_name
405 ));
406 }
407 }
408
409 let entries: Vec<_> = inventory::iter::<VariantEntry>()
410 .map(|e| (variant_label(e), e.aliases))
411 .collect();
412
413 validate_aliases_inner(&problem_names, &entries)
414}
415
416pub fn validate_aliases_inner(
421 problem_names: &BTreeMap<String, Vec<String>>,
422 entries: &[(String, &[&str])],
423) -> Result<(), Vec<String>> {
424 let mut conflicts = Vec::new();
425 let mut variant_aliases: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
426
427 for (target, aliases) in entries {
428 for alias in *aliases {
429 if alias.trim().is_empty() {
430 conflicts.push(format!(
431 "variant-level alias on {target} is empty or whitespace-only"
432 ));
433 continue;
434 }
435
436 let lower = alias.to_lowercase();
437 if let Some(collisions) = problem_names.get(&lower) {
438 for collision in collisions {
439 conflicts.push(format!(
440 "variant-level alias `{alias}` on {target} conflicts with {collision}"
441 ));
442 }
443 }
444
445 variant_aliases
446 .entry(lower)
447 .or_default()
448 .push((alias.to_string(), target.clone()));
449 }
450 }
451
452 for (lower, registrations) in variant_aliases {
453 if registrations.len() > 1 {
454 let details = registrations
455 .iter()
456 .map(|(alias, target)| format!("`{alias}` on {target}"))
457 .collect::<Vec<_>>()
458 .join("; ");
459 conflicts.push(format!(
460 "duplicate variant-level alias `{lower}` (case-insensitive): {details}"
461 ));
462 }
463 }
464
465 if conflicts.is_empty() {
466 Ok(())
467 } else {
468 conflicts.sort();
469 Err(conflicts)
470 }
471}
472
473pub fn variant_label(entry: &VariantEntry) -> String {
474 let variant = entry.variant();
475 if variant.is_empty() {
476 return entry.name.to_string();
477 }
478
479 let parts = variant
480 .iter()
481 .map(|(key, value)| format!("{key}={value}"))
482 .collect::<Vec<_>>()
483 .join(", ");
484 format!("{} {{{parts}}}", entry.name)
485}
486
487impl std::fmt::Debug for VariantEntry {
488 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489 f.debug_struct("VariantEntry")
490 .field("name", &self.name)
491 .field("variant", &self.variant())
492 .field("complexity", &self.complexity)
493 .finish()
494 }
495}
496
497inventory::collect!(VariantEntry);
498
499#[cfg(test)]
500#[path = "../unit_tests/registry/variant.rs"]
501mod tests;