use std::collections::BTreeMap;
use rustev_contract::judgment::OutValue;
use serde::{Deserialize, Serialize};
pub const MAX_GRADE: u8 = 10;
pub const MAX_ENTRIES: usize = 1000;
pub const MAX_PATH: usize = 16;
pub const MAX_NAME: usize = 256;
pub const ABSENT: &str = "absent";
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Gold {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relevance: Option<BTreeMap<String, u8>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub items: Option<BTreeMap<String, BTreeMap<String, GoldValue>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested: Option<BTreeMap<String, GoldValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fields: Option<BTreeMap<String, GoldValue>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rubric: Option<BTreeMap<String, String>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum GoldValue {
Bool(bool),
Integer(i64),
Text(String),
Path(Vec<String>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FieldGold<'a> {
Value(&'a GoldValue),
Absent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Match {
Equal,
Unequal,
TypeMismatch,
}
impl Match {
fn of(equal: bool) -> Match {
if equal { Match::Equal } else { Match::Unequal }
}
}
pub fn match_value(value: &OutValue, gold: &GoldValue) -> Match {
match (value, gold) {
(OutValue::Text(v) | OutValue::Enum(v), GoldValue::Text(g)) => Match::of(v == g),
(OutValue::Integer(v), GoldValue::Integer(g)) => Match::of(v == g),
(OutValue::Bool(v), GoldValue::Bool(g)) => Match::of(v == g),
(OutValue::Decimal(v), GoldValue::Text(g)) => Match::of(&v.to_canonical_string() == g),
(OutValue::List(v), GoldValue::Path(g)) => {
let mut texts = Vec::with_capacity(v.len());
for e in v {
match e {
OutValue::Text(t) => texts.push(t),
_ => return Match::TypeMismatch,
}
}
Match::of(texts.len() == g.len() && texts.iter().zip(g).all(|(a, b)| *a == b))
}
_ => Match::TypeMismatch,
}
}
fn name_ok(s: &str) -> bool {
!s.is_empty() && s.len() <= MAX_NAME
}
fn map_ok<V>(what: &str, m: &BTreeMap<String, V>) -> Result<(), String> {
if m.len() > MAX_ENTRIES {
return Err(format!("gold {what} holds more than {MAX_ENTRIES} entries"));
}
if !m.keys().all(|k| name_ok(k)) {
return Err(format!("a gold {what} name is 1 to {MAX_NAME} bytes"));
}
Ok(())
}
impl GoldValue {
pub fn check(&self) -> Result<(), String> {
match self {
GoldValue::Path(p) if p.is_empty() || p.len() > MAX_PATH => {
Err(format!("a gold path holds 1 to {MAX_PATH} strings"))
}
_ => Ok(()),
}
}
}
impl Gold {
pub fn check(&self) -> Result<(), String> {
if let Some(r) = &self.relevance {
map_ok("relevance", r)?;
if r.values().any(|g| *g > MAX_GRADE) {
return Err(format!("a relevance grade is 0 to {MAX_GRADE}"));
}
}
if let Some(items) = &self.items {
map_ok("items", items)?;
for attrs in items.values() {
map_ok("item attribute", attrs)?;
attrs.values().try_for_each(GoldValue::check)?;
}
}
for (what, m) in [("requested", &self.requested), ("fields", &self.fields)] {
if let Some(m) = m {
map_ok(what, m)?;
m.values().try_for_each(GoldValue::check)?;
}
}
if let Some(r) = &self.rubric {
map_ok("rubric", r)?;
}
Ok(())
}
pub fn field(&self, name: &str) -> Option<FieldGold<'_>> {
match self.fields.as_ref()?.get(name)? {
GoldValue::Text(t) if t == ABSENT => Some(FieldGold::Absent),
v => Some(FieldGold::Value(v)),
}
}
}