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 ConstructionError {
125 pub(crate) fn length_mismatch(field: &str, actual: usize, expected: usize) -> Self {
126 Self::Conversion(format!("{field} has length {actual}, expected {expected}"))
127 }
128}
129
130impl From<String> for ConstructionError {
131 fn from(message: String) -> Self {
132 Self::Conversion(message)
133 }
134}
135
136impl From<&str> for ConstructionError {
137 fn from(message: &str) -> Self {
138 Self::Conversion(message.to_string())
139 }
140}
141
142impl From<std::convert::Infallible> for ConstructionError {
143 fn from(value: std::convert::Infallible) -> Self {
144 match value {}
145 }
146}
147
148pub type ConstructProblemFn =
150 fn(serde_json::Value) -> Result<Box<dyn DynProblem>, ConstructionError>;
151
152#[derive(Clone, Copy)]
154pub struct RandomRegistration {
155 pub inputs: fn() -> Vec<CreateInputInfo>,
157 pub generate: ConstructProblemFn,
159}
160
161pub trait RandomGenerate: DynProblem + Sized {
163 fn inputs() -> Vec<CreateInputInfo>;
165
166 fn generate(data: serde_json::Value) -> Result<Self, ConstructionError>;
168}
169
170pub fn validate_create_inputs(
172 inputs: &[CreateInputInfo],
173 data: &serde_json::Value,
174) -> Result<(), ConstructionError> {
175 validate_input_contract(
176 inputs.iter().map(|input| (input.name, input.required)),
177 data,
178 )
179}
180
181pub fn validate_direct_create_inputs(
186 fields: &[FieldInfo],
187 data: &serde_json::Value,
188) -> Result<(), ConstructionError> {
189 validate_input_contract(fields.iter().map(|field| (field.name, true)), data)
190}
191
192fn validate_input_contract<'a>(
193 inputs: impl IntoIterator<Item = (&'a str, bool)>,
194 data: &serde_json::Value,
195) -> Result<(), ConstructionError> {
196 let object = data.as_object().ok_or(ConstructionError::ExpectedObject)?;
197 let mut declared = BTreeMap::new();
198 for (name, required) in inputs {
199 if declared.insert(name, required).is_some() {
200 return Err(ConstructionError::DuplicateInput(name.to_string()));
201 }
202 }
203
204 let unknown = object
205 .keys()
206 .filter(|name| !declared.contains_key(name.as_str()))
207 .cloned()
208 .collect::<Vec<_>>();
209 if !unknown.is_empty() {
210 return Err(ConstructionError::UnknownInputs(unknown));
211 }
212
213 let missing = declared
214 .into_iter()
215 .filter(|(name, required)| *required && !object.contains_key(*name))
216 .map(|(name, _)| name.to_string())
217 .collect::<Vec<_>>();
218 if !missing.is_empty() {
219 return Err(ConstructionError::MissingInputs(missing));
220 }
221
222 Ok(())
223}
224
225pub struct VariantEntry {
230 pub name: &'static str,
232 pub variant_fn: fn() -> Vec<(&'static str, &'static str)>,
234 pub complexity: &'static str,
236 pub complexity_eval_fn: fn(&dyn Any) -> f64,
240 pub parameter_names_fn: fn() -> &'static [&'static str],
242 pub parameter_measure_fn: fn(&dyn Any) -> crate::types::ProblemParameters,
244 pub is_default: bool,
246 pub aliases: &'static [&'static str],
252 pub create_inputs: Option<fn() -> Vec<CreateInputInfo>>,
255 pub construct_fn: ConstructProblemFn,
257 pub random: Option<RandomRegistration>,
259 pub factory: fn(serde_json::Value) -> Result<Box<dyn DynProblem>, serde_json::Error>,
261 pub serialize_fn: fn(&dyn Any) -> Option<serde_json::Value>,
263 pub borrow_fn: fn(&dyn Any) -> Option<&dyn DynProblem>,
265}
266
267impl VariantEntry {
268 pub fn inputs(&self) -> Vec<CreateInputInfo> {
270 match self.create_inputs {
271 Some(inputs) => inputs(),
272 None => super::find_problem_type(self.name)
273 .expect("registered variant must have a problem schema")
274 .fields
275 .iter()
276 .cloned()
277 .map(CreateInputInfo::from_field)
278 .collect(),
279 }
280 }
281
282 pub fn variant(&self) -> Vec<(&'static str, &'static str)> {
284 (self.variant_fn)()
285 }
286
287 pub fn variant_map(&self) -> BTreeMap<String, String> {
289 self.variant()
290 .into_iter()
291 .map(|(k, v)| (k.to_string(), v.to_string()))
292 .collect()
293 }
294
295 pub fn parameter_names(&self) -> &'static [&'static str] {
297 (self.parameter_names_fn)()
298 }
299}
300
301pub fn variant_entries() -> Vec<&'static VariantEntry> {
303 inventory::iter::<VariantEntry>().collect()
304}
305
306pub fn validate_variant_parameter_schemas() -> Result<(), Vec<String>> {
308 let mut errors = Vec::new();
309 let mut schemas = BTreeMap::<&str, Vec<&str>>::new();
310
311 for entry in inventory::iter::<VariantEntry> {
312 let names = entry.parameter_names();
313 if names.is_empty() {
314 errors.push(format!("{} has no parameters", variant_label(entry)));
315 continue;
316 }
317
318 let unique = names
319 .iter()
320 .copied()
321 .collect::<std::collections::BTreeSet<_>>();
322 if unique.len() != names.len() {
323 errors.push(format!(
324 "{} declares duplicate parameters: {names:?}",
325 variant_label(entry)
326 ));
327 }
328
329 let canonical = names.to_vec();
330 if let Some(expected) = schemas.get(entry.name) {
331 if expected != &canonical {
332 errors.push(format!(
333 "{} has parameter schema {canonical:?}, expected {expected:?}",
334 variant_label(entry)
335 ));
336 }
337 } else {
338 schemas.insert(entry.name, canonical.clone());
339 }
340
341 let expression = crate::expr::Expr::parse(entry.complexity);
342 for variable in expression.variables() {
343 if !canonical.contains(&variable) {
344 errors.push(format!(
345 "{} complexity references unknown parameter `{variable}`; declared: {canonical:?}",
346 variant_label(entry)
347 ));
348 }
349 }
350 }
351
352 if errors.is_empty() {
353 Ok(())
354 } else {
355 errors.sort();
356 Err(errors)
357 }
358}
359
360pub fn find_variant_entry(
364 name: &str,
365 variant: &BTreeMap<String, String>,
366) -> Option<&'static VariantEntry> {
367 inventory::iter::<VariantEntry>()
368 .find(|entry| entry.name == name && entry.variant_map() == *variant)
369}
370
371pub fn find_variant_by_alias(
380 input: &str,
381) -> Option<(&'static VariantEntry, BTreeMap<String, String>)> {
382 let lower = input.to_lowercase();
383 let entry = inventory::iter::<VariantEntry>()
384 .find(|entry| entry.aliases.iter().any(|a| a.to_lowercase() == lower))?;
385 Some((entry, entry.variant_map()))
386}
387
388pub fn validate_variant_aliases() -> Result<(), Vec<String>> {
395 let mut problem_names: BTreeMap<String, Vec<String>> = BTreeMap::new();
396
397 for problem in super::problem_type::problem_types() {
398 problem_names
399 .entry(problem.canonical_name.to_lowercase())
400 .or_default()
401 .push(format!(
402 "canonical problem name `{}`",
403 problem.canonical_name
404 ));
405
406 for alias in problem.aliases {
407 problem_names
408 .entry(alias.to_lowercase())
409 .or_default()
410 .push(format!(
411 "problem-level alias `{alias}` for `{}`",
412 problem.canonical_name
413 ));
414 }
415 }
416
417 let entries: Vec<_> = inventory::iter::<VariantEntry>()
418 .map(|e| (variant_label(e), e.aliases))
419 .collect();
420
421 validate_aliases_inner(&problem_names, &entries)
422}
423
424pub fn validate_aliases_inner(
429 problem_names: &BTreeMap<String, Vec<String>>,
430 entries: &[(String, &[&str])],
431) -> Result<(), Vec<String>> {
432 let mut conflicts = Vec::new();
433 let mut variant_aliases: BTreeMap<String, Vec<(String, String)>> = BTreeMap::new();
434
435 for (target, aliases) in entries {
436 for alias in *aliases {
437 if alias.trim().is_empty() {
438 conflicts.push(format!(
439 "variant-level alias on {target} is empty or whitespace-only"
440 ));
441 continue;
442 }
443
444 let lower = alias.to_lowercase();
445 if let Some(collisions) = problem_names.get(&lower) {
446 for collision in collisions {
447 conflicts.push(format!(
448 "variant-level alias `{alias}` on {target} conflicts with {collision}"
449 ));
450 }
451 }
452
453 variant_aliases
454 .entry(lower)
455 .or_default()
456 .push((alias.to_string(), target.clone()));
457 }
458 }
459
460 for (lower, registrations) in variant_aliases {
461 if registrations.len() > 1 {
462 let details = registrations
463 .iter()
464 .map(|(alias, target)| format!("`{alias}` on {target}"))
465 .collect::<Vec<_>>()
466 .join("; ");
467 conflicts.push(format!(
468 "duplicate variant-level alias `{lower}` (case-insensitive): {details}"
469 ));
470 }
471 }
472
473 if conflicts.is_empty() {
474 Ok(())
475 } else {
476 conflicts.sort();
477 Err(conflicts)
478 }
479}
480
481pub fn variant_label(entry: &VariantEntry) -> String {
482 let variant = entry.variant();
483 if variant.is_empty() {
484 return entry.name.to_string();
485 }
486
487 let parts = variant
488 .iter()
489 .map(|(key, value)| format!("{key}={value}"))
490 .collect::<Vec<_>>()
491 .join(", ");
492 format!("{} {{{parts}}}", entry.name)
493}
494
495impl std::fmt::Debug for VariantEntry {
496 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
497 f.debug_struct("VariantEntry")
498 .field("name", &self.name)
499 .field("variant", &self.variant())
500 .field("complexity", &self.complexity)
501 .finish()
502 }
503}
504
505inventory::collect!(VariantEntry);
506
507#[cfg(test)]
508#[path = "../unit_tests/registry/variant.rs"]
509mod tests;