broccoli_server_sdk/types/
verdict.rs1use 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 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 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}