Skip to main content

broccoli_server_sdk/types/
verdict.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use std::{fmt, str::FromStr};
3
4#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5pub enum Verdict {
6    Accepted,
7    WrongAnswer,
8    TimeLimitExceeded,
9    MemoryLimitExceeded,
10    RuntimeError,
11    SystemError,
12    CompileError,
13    Skipped,
14    Other(String),
15}
16
17impl Verdict {
18    /// Severity of the verdict (higher = worse).
19    pub fn severity(&self) -> u8 {
20        match self {
21            Self::Accepted => 0,
22            Self::Skipped => 0,
23            Self::WrongAnswer => 1,
24            Self::TimeLimitExceeded => 2,
25            Self::MemoryLimitExceeded => 3,
26            Self::RuntimeError => 4,
27            Self::SystemError => 5,
28            Self::CompileError => 6,
29            Self::Other(_) => 5,
30        }
31    }
32
33    /// Maps this verdict to the DB-compatible string representation.
34    ///
35    /// `CompileError` -> `"SystemError"` in the DB.
36    pub fn to_db_str(&self) -> &str {
37        match self {
38            Self::Accepted => "Accepted",
39            Self::WrongAnswer => "WrongAnswer",
40            Self::TimeLimitExceeded => "TimeLimitExceeded",
41            Self::MemoryLimitExceeded => "MemoryLimitExceeded",
42            Self::RuntimeError => "RuntimeError",
43            Self::Skipped => "Skipped",
44            Self::SystemError | Self::CompileError => "SystemError",
45            Self::Other(custom) => custom.as_str(),
46        }
47    }
48
49    pub fn is_accepted(&self) -> bool {
50        matches!(self, Self::Accepted)
51    }
52
53    pub fn is_skipped(&self) -> bool {
54        matches!(self, Self::Skipped)
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ParseVerdictError {
60    invalid: String,
61}
62
63impl fmt::Display for ParseVerdictError {
64    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65        write!(f, "Invalid verdict '{}'", self.invalid)
66    }
67}
68
69impl std::error::Error for ParseVerdictError {}
70
71impl fmt::Display for Verdict {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        match self {
74            Self::CompileError => f.write_str("CompileError"),
75            other => f.write_str(other.to_db_str()),
76        }
77    }
78}
79
80impl FromStr for Verdict {
81    type Err = ParseVerdictError;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        let s = s.trim();
85        if s.is_empty() {
86            return Err(ParseVerdictError {
87                invalid: s.to_string(),
88            });
89        }
90
91        match s {
92            "Accepted" => Ok(Self::Accepted),
93            "WrongAnswer" => Ok(Self::WrongAnswer),
94            "TimeLimitExceeded" => Ok(Self::TimeLimitExceeded),
95            "MemoryLimitExceeded" => Ok(Self::MemoryLimitExceeded),
96            "RuntimeError" => Ok(Self::RuntimeError),
97            "SystemError" => Ok(Self::SystemError),
98            "CompileError" => Ok(Self::CompileError),
99            "Skipped" => Ok(Self::Skipped),
100            _ => {
101                if let Some(custom) = s
102                    .strip_prefix("Other(")
103                    .and_then(|value| value.strip_suffix(')'))
104                    .filter(|value| !value.trim().is_empty())
105                {
106                    return Ok(Self::Other(custom.to_string()));
107                }
108
109                Ok(Self::Other(s.to_string()))
110            }
111        }
112    }
113}
114
115impl Serialize for Verdict {
116    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117    where
118        S: Serializer,
119    {
120        serializer.serialize_str(match self {
121            Self::CompileError => "CompileError",
122            other => other.to_db_str(),
123        })
124    }
125}
126
127impl<'de> Deserialize<'de> for Verdict {
128    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
129    where
130        D: Deserializer<'de>,
131    {
132        let raw = String::deserialize(deserializer)?;
133        match raw.as_str() {
134            "CompileError" => Ok(Self::CompileError),
135            _ => Ok(Self::from_str(raw.as_str()).unwrap_or(Self::Other(raw))),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn severity_order() {
146        assert!(Verdict::Accepted.severity() < Verdict::WrongAnswer.severity());
147        assert!(Verdict::WrongAnswer.severity() < Verdict::TimeLimitExceeded.severity());
148        assert!(Verdict::TimeLimitExceeded.severity() < Verdict::MemoryLimitExceeded.severity());
149        assert!(Verdict::MemoryLimitExceeded.severity() < Verdict::RuntimeError.severity());
150        assert!(Verdict::RuntimeError.severity() < Verdict::SystemError.severity());
151        assert!(Verdict::SystemError.severity() < Verdict::CompileError.severity());
152        assert_eq!(Verdict::Skipped.severity(), 0);
153        assert_eq!(Verdict::Other("PluginStatus".into()).severity(), 5);
154    }
155
156    #[test]
157    fn to_db_str_maps_correctly() {
158        assert_eq!(Verdict::CompileError.to_db_str(), "SystemError");
159        assert_eq!(Verdict::Skipped.to_db_str(), "Skipped");
160        assert_eq!(Verdict::Accepted.to_db_str(), "Accepted");
161        assert_eq!(
162            Verdict::Other("PluginStatus".into()).to_db_str(),
163            "PluginStatus"
164        );
165    }
166
167    #[test]
168    fn serde_roundtrip() {
169        let v = Verdict::CompileError;
170        let json = serde_json::to_string(&v).unwrap();
171        assert_eq!(json, "\"CompileError\"");
172        let parsed: Verdict = serde_json::from_str(&json).unwrap();
173        assert_eq!(parsed, v);
174    }
175
176    #[test]
177    fn deserialize_all_variants() {
178        for name in [
179            "Accepted",
180            "WrongAnswer",
181            "TimeLimitExceeded",
182            "MemoryLimitExceeded",
183            "RuntimeError",
184            "SystemError",
185            "CompileError",
186            "Skipped",
187        ] {
188            let json = format!("\"{name}\"");
189            let v: Verdict = serde_json::from_str(&json).unwrap();
190            assert_eq!(format!("{v:?}"), name);
191        }
192    }
193
194    #[test]
195    fn deserialize_unknown_verdict_as_other() {
196        let verdict: Verdict = serde_json::from_str("\"PluginStatus\"").unwrap();
197        assert_eq!(verdict, Verdict::Other("PluginStatus".into()));
198    }
199
200    #[test]
201    fn parse_tagged_other_verdict() {
202        let verdict = Verdict::from_str("Other(PluginStatus)").unwrap();
203        assert_eq!(verdict, Verdict::Other("PluginStatus".into()));
204    }
205
206    #[test]
207    fn reject_empty_verdict() {
208        assert!(Verdict::from_str("   ").is_err());
209    }
210
211    #[test]
212    fn predicates() {
213        assert!(Verdict::Accepted.is_accepted());
214        assert!(!Verdict::WrongAnswer.is_accepted());
215        assert!(Verdict::Skipped.is_skipped());
216        assert!(!Verdict::Accepted.is_skipped());
217    }
218}