use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum FaceGroup {
Feature,
Refused,
Body,
Unexplained,
}
impl FaceGroup {
pub const ALL: [FaceGroup; 4] = [FaceGroup::Feature, FaceGroup::Refused, FaceGroup::Body, FaceGroup::Unexplained];
pub fn name(self) -> &'static str {
match self {
FaceGroup::Feature => "feature",
FaceGroup::Refused => "refused",
FaceGroup::Body => "body",
FaceGroup::Unexplained => "unexplained",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Refusal {
pub candidate: String,
pub reason: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "code", rename_all = "kebab-case")]
pub enum BodyReason {
StockTop,
StockBottom,
StockSide,
#[serde(rename_all = "camelCase")]
OtherSetup { axis: String, feature: String },
}
impl BodyReason {
pub fn code(&self) -> &'static str {
match self {
BodyReason::StockTop => "stock-top",
BodyReason::StockBottom => "stock-bottom",
BodyReason::StockSide => "stock-side",
BodyReason::OtherSetup { .. } => "other-setup",
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FaceAccount {
pub solid: String,
pub face: String,
pub group: FaceGroup,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub features: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub refusals: Vec<Refusal>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub body: Option<BodyReason>,
pub why: String,
}
impl FaceAccount {
pub fn new(solid: String, face: String, why: String) -> Self {
Self { solid, face, group: FaceGroup::Unexplained, features: Vec::new(), refusals: Vec::new(), body: None, why }
}
pub fn settle(&mut self) {
self.features.sort();
self.features.dedup();
self.group = if !self.features.is_empty() {
FaceGroup::Feature
} else if !self.refusals.is_empty() {
FaceGroup::Refused
} else if self.body.is_some() {
FaceGroup::Body
} else {
FaceGroup::Unexplained
};
}
}
pub fn group_counts(accounting: &[FaceAccount]) -> [usize; 4] {
let mut counts = [0; 4];
for a in accounting {
counts[FaceGroup::ALL.iter().position(|g| *g == a.group).unwrap()] += 1;
}
counts
}
pub fn retain_claims(accounting: &mut [FaceAccount], keep: impl Fn(&str) -> Option<String>) {
for a in accounting {
a.features = a.features.iter().filter_map(|id| keep(id)).collect();
a.settle();
}
}
pub fn check(accounting: &[FaceAccount], features: &[super::Feature]) -> Result<(), String> {
let mut seen = std::collections::BTreeSet::new();
for a in accounting {
if !seen.insert((a.solid.as_str(), a.face.as_str())) {
return Err(format!("face {}/{} is accounted twice", a.solid, a.face));
}
for id in &a.features {
if !features.iter().any(|f| &f.id == id) {
return Err(format!("face {}/{} names missing feature {id}", a.solid, a.face));
}
}
let mut settled = a.clone();
settled.settle();
if settled.group != a.group {
return Err(format!("face {}/{} is in group {} but its evidence says {}", a.solid, a.face, a.group.name(), settled.group.name()));
}
}
Ok(())
}