1use std::collections::BTreeMap;
13
14use rustev_contract::judgment::OutValue;
15use serde::{Deserialize, Serialize};
16
17pub const MAX_GRADE: u8 = 10;
19pub const MAX_ENTRIES: usize = 1000;
21pub const MAX_PATH: usize = 16;
23pub const MAX_NAME: usize = 256;
25pub const ABSENT: &str = "absent";
28
29#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct Gold {
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub relevance: Option<BTreeMap<String, u8>>,
36 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub items: Option<BTreeMap<String, BTreeMap<String, GoldValue>>>,
39 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub requested: Option<BTreeMap<String, GoldValue>>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
44 pub fields: Option<BTreeMap<String, GoldValue>>,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
47 pub rubric: Option<BTreeMap<String, String>>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53#[serde(untagged)]
54pub enum GoldValue {
55 Bool(bool),
56 Integer(i64),
57 Text(String),
58 Path(Vec<String>),
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum FieldGold<'a> {
64 Value(&'a GoldValue),
65 Absent,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Match {
71 Equal,
72 Unequal,
73 TypeMismatch,
76}
77
78impl Match {
79 fn of(equal: bool) -> Match {
80 if equal { Match::Equal } else { Match::Unequal }
81 }
82}
83
84pub fn match_value(value: &OutValue, gold: &GoldValue) -> Match {
86 match (value, gold) {
87 (OutValue::Text(v) | OutValue::Enum(v), GoldValue::Text(g)) => Match::of(v == g),
88 (OutValue::Integer(v), GoldValue::Integer(g)) => Match::of(v == g),
89 (OutValue::Bool(v), GoldValue::Bool(g)) => Match::of(v == g),
90 (OutValue::Decimal(v), GoldValue::Text(g)) => Match::of(&v.to_canonical_string() == g),
91 (OutValue::List(v), GoldValue::Path(g)) => {
92 let mut texts = Vec::with_capacity(v.len());
93 for e in v {
94 match e {
95 OutValue::Text(t) => texts.push(t),
96 _ => return Match::TypeMismatch,
97 }
98 }
99 Match::of(texts.len() == g.len() && texts.iter().zip(g).all(|(a, b)| *a == b))
100 }
101 _ => Match::TypeMismatch,
102 }
103}
104
105fn name_ok(s: &str) -> bool {
106 !s.is_empty() && s.len() <= MAX_NAME
107}
108
109fn map_ok<V>(what: &str, m: &BTreeMap<String, V>) -> Result<(), String> {
110 if m.len() > MAX_ENTRIES {
111 return Err(format!("gold {what} holds more than {MAX_ENTRIES} entries"));
112 }
113 if !m.keys().all(|k| name_ok(k)) {
114 return Err(format!("a gold {what} name is 1 to {MAX_NAME} bytes"));
115 }
116 Ok(())
117}
118
119impl GoldValue {
120 pub fn check(&self) -> Result<(), String> {
122 match self {
123 GoldValue::Path(p) if p.is_empty() || p.len() > MAX_PATH => {
124 Err(format!("a gold path holds 1 to {MAX_PATH} strings"))
125 }
126 _ => Ok(()),
127 }
128 }
129}
130
131impl Gold {
132 pub fn check(&self) -> Result<(), String> {
134 if let Some(r) = &self.relevance {
135 map_ok("relevance", r)?;
136 if r.values().any(|g| *g > MAX_GRADE) {
137 return Err(format!("a relevance grade is 0 to {MAX_GRADE}"));
138 }
139 }
140 if let Some(items) = &self.items {
141 map_ok("items", items)?;
142 for attrs in items.values() {
143 map_ok("item attribute", attrs)?;
144 attrs.values().try_for_each(GoldValue::check)?;
145 }
146 }
147 for (what, m) in [("requested", &self.requested), ("fields", &self.fields)] {
148 if let Some(m) = m {
149 map_ok(what, m)?;
150 m.values().try_for_each(GoldValue::check)?;
151 }
152 }
153 if let Some(r) = &self.rubric {
154 map_ok("rubric", r)?;
159 }
160 Ok(())
161 }
162
163 pub fn field(&self, name: &str) -> Option<FieldGold<'_>> {
166 match self.fields.as_ref()?.get(name)? {
167 GoldValue::Text(t) if t == ABSENT => Some(FieldGold::Absent),
168 v => Some(FieldGold::Value(v)),
169 }
170 }
171}