1use serde::{Deserialize, Deserializer, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct SolverStats {
7 pub time_spent_millis: u64,
8 pub score_calculation_count: u64,
9 pub score_calculation_speed: u64,
10 pub move_evaluation_count: u64,
11 pub move_evaluation_speed: u64,
12}
13
14impl SolverStats {
15 pub fn new(
16 time_spent_millis: u64,
17 score_calculation_count: u64,
18 score_calculation_speed: u64,
19 move_evaluation_count: u64,
20 move_evaluation_speed: u64,
21 ) -> Self {
22 Self {
23 time_spent_millis,
24 score_calculation_count,
25 score_calculation_speed,
26 move_evaluation_count,
27 move_evaluation_speed,
28 }
29 }
30
31 pub fn summary(&self) -> String {
33 format!(
34 "Time: {}ms | Moves: {} ({}/sec) | Score calcs: {} ({}/sec)",
35 self.time_spent_millis,
36 self.move_evaluation_count,
37 self.move_evaluation_speed,
38 self.score_calculation_count,
39 self.score_calculation_speed
40 )
41 }
42}
43
44fn deserialize_score<'de, D>(deserializer: D) -> Result<String, D::Error>
47where
48 D: Deserializer<'de>,
49{
50 use serde::de::{Error, Visitor};
51
52 struct ScoreVisitor;
53
54 impl<'de> Visitor<'de> for ScoreVisitor {
55 type Value = String;
56
57 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
58 formatter.write_str("a string, null, or an object with score type key")
59 }
60
61 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
62 where
63 E: Error,
64 {
65 Ok(value.to_string())
66 }
67
68 fn visit_unit<E>(self) -> Result<Self::Value, E>
69 where
70 E: Error,
71 {
72 Ok("uninitialized".to_string())
74 }
75
76 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
77 where
78 M: serde::de::MapAccess<'de>,
79 {
80 if let Some((_score_type, score_value)) = map.next_entry::<String, String>()? {
82 Ok(score_value)
84 } else {
85 Err(Error::custom("expected score object to have one entry"))
86 }
87 }
88 }
89
90 deserializer.deserialize_any(ScoreVisitor)
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct SolveResponse {
96 pub solution: String,
97 #[serde(deserialize_with = "deserialize_score")]
100 pub score: String,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub stats: Option<SolverStats>,
103}
104
105impl SolveResponse {
106 pub fn new(solution: String, score: impl Into<String>) -> Self {
107 Self {
108 solution,
109 score: score.into(),
110 stats: None,
111 }
112 }
113
114 pub fn with_stats(solution: String, score: impl Into<String>, stats: SolverStats) -> Self {
115 Self {
116 solution,
117 score: score.into(),
118 stats: Some(stats),
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct ScoreDto {
126 pub score_string: String,
127 pub hard_score: i64,
128 pub soft_score: i64,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub medium_score: Option<i64>,
131 pub is_feasible: bool,
132}
133
134impl ScoreDto {
135 pub fn hard_soft(hard: i64, soft: i64) -> Self {
136 Self {
137 score_string: format!("{}hard/{}soft", hard, soft),
138 hard_score: hard,
139 soft_score: soft,
140 medium_score: None,
141 is_feasible: hard >= 0,
142 }
143 }
144
145 pub fn hard_medium_soft(hard: i64, medium: i64, soft: i64) -> Self {
146 Self {
147 score_string: format!("{}hard/{}medium/{}soft", hard, medium, soft),
148 hard_score: hard,
149 soft_score: soft,
150 medium_score: Some(medium),
151 is_feasible: hard >= 0,
152 }
153 }
154
155 pub fn simple(score: i64) -> Self {
156 Self {
157 score_string: score.to_string(),
158 hard_score: score,
159 soft_score: 0,
160 medium_score: None,
161 is_feasible: true,
162 }
163 }
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
168pub enum SolveState {
169 Pending,
170 Running,
171 Completed,
172 Failed,
173 Stopped,
174}
175
176impl SolveState {
177 pub fn is_terminal(&self) -> bool {
178 matches!(
179 self,
180 SolveState::Completed | SolveState::Failed | SolveState::Stopped
181 )
182 }
183
184 pub fn is_running(&self) -> bool {
185 matches!(self, SolveState::Running)
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(rename_all = "camelCase")]
191pub struct SolveStatus {
192 pub state: SolveState,
193 pub time_spent_ms: u64,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 pub best_score: Option<ScoreDto>,
196 #[serde(skip_serializing_if = "Option::is_none")]
197 pub error: Option<String>,
198}
199
200impl SolveStatus {
201 pub fn pending() -> Self {
202 Self {
203 state: SolveState::Pending,
204 time_spent_ms: 0,
205 best_score: None,
206 error: None,
207 }
208 }
209
210 pub fn running(time_spent_ms: u64, best_score: Option<ScoreDto>) -> Self {
211 Self {
212 state: SolveState::Running,
213 time_spent_ms,
214 best_score,
215 error: None,
216 }
217 }
218
219 pub fn completed(time_spent_ms: u64, score: ScoreDto) -> Self {
220 Self {
221 state: SolveState::Completed,
222 time_spent_ms,
223 best_score: Some(score),
224 error: None,
225 }
226 }
227
228 pub fn failed(time_spent_ms: u64, error: impl Into<String>) -> Self {
229 Self {
230 state: SolveState::Failed,
231 time_spent_ms,
232 best_score: None,
233 error: Some(error.into()),
234 }
235 }
236
237 pub fn stopped(time_spent_ms: u64, best_score: Option<ScoreDto>) -> Self {
238 Self {
239 state: SolveState::Stopped,
240 time_spent_ms,
241 best_score,
242 error: None,
243 }
244 }
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
248#[serde(rename_all = "camelCase")]
249pub struct AsyncSolveResponse {
250 pub solve_id: String,
251}
252
253impl AsyncSolveResponse {
254 pub fn new(solve_id: impl Into<String>) -> Self {
255 Self {
256 solve_id: solve_id.into(),
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Hash)]
262pub struct SolveHandle {
263 pub id: String,
264}
265
266impl SolveHandle {
267 pub fn new(id: impl Into<String>) -> Self {
268 Self { id: id.into() }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_solve_response_new() {
278 let response = SolveResponse::new(r#"{"lessons": []}"#.to_string(), "0hard/-10soft");
279
280 assert_eq!(response.solution, r#"{"lessons": []}"#);
281 assert_eq!(response.score, "0hard/-10soft");
282 }
283
284 #[test]
285 fn test_score_dto_hard_soft() {
286 let score = ScoreDto::hard_soft(-5, -100);
287
288 assert_eq!(score.hard_score, -5);
289 assert_eq!(score.soft_score, -100);
290 assert!(score.medium_score.is_none());
291 assert!(!score.is_feasible);
292 assert_eq!(score.score_string, "-5hard/-100soft");
293 }
294
295 #[test]
296 fn test_score_dto_hard_medium_soft() {
297 let score = ScoreDto::hard_medium_soft(0, -10, -50);
298
299 assert_eq!(score.hard_score, 0);
300 assert_eq!(score.medium_score, Some(-10));
301 assert_eq!(score.soft_score, -50);
302 assert!(score.is_feasible);
303 assert_eq!(score.score_string, "0hard/-10medium/-50soft");
304 }
305
306 #[test]
307 fn test_score_dto_simple() {
308 let score = ScoreDto::simple(42);
309
310 assert_eq!(score.hard_score, 42);
311 assert_eq!(score.soft_score, 0);
312 assert!(score.is_feasible);
313 assert_eq!(score.score_string, "42");
314 }
315
316 #[test]
317 fn test_solve_state_is_terminal() {
318 assert!(!SolveState::Pending.is_terminal());
319 assert!(!SolveState::Running.is_terminal());
320 assert!(SolveState::Completed.is_terminal());
321 assert!(SolveState::Failed.is_terminal());
322 assert!(SolveState::Stopped.is_terminal());
323 }
324
325 #[test]
326 fn test_solve_state_is_running() {
327 assert!(!SolveState::Pending.is_running());
328 assert!(SolveState::Running.is_running());
329 assert!(!SolveState::Completed.is_running());
330 }
331
332 #[test]
333 fn test_solve_status_pending() {
334 let status = SolveStatus::pending();
335
336 assert_eq!(status.state, SolveState::Pending);
337 assert_eq!(status.time_spent_ms, 0);
338 assert!(status.best_score.is_none());
339 assert!(status.error.is_none());
340 }
341
342 #[test]
343 fn test_solve_status_running() {
344 let score = ScoreDto::hard_soft(-10, -50);
345 let status = SolveStatus::running(5000, Some(score));
346
347 assert_eq!(status.state, SolveState::Running);
348 assert_eq!(status.time_spent_ms, 5000);
349 assert!(status.best_score.is_some());
350 }
351
352 #[test]
353 fn test_solve_status_completed() {
354 let score = ScoreDto::hard_soft(0, -20);
355 let status = SolveStatus::completed(30000, score);
356
357 assert_eq!(status.state, SolveState::Completed);
358 assert_eq!(status.time_spent_ms, 30000);
359 assert!(status.best_score.is_some());
360 assert!(status.best_score.as_ref().unwrap().is_feasible);
361 }
362
363 #[test]
364 fn test_solve_status_failed() {
365 let status = SolveStatus::failed(1000, "Timeout exceeded");
366
367 assert_eq!(status.state, SolveState::Failed);
368 assert_eq!(status.error, Some("Timeout exceeded".to_string()));
369 }
370
371 #[test]
372 fn test_solve_status_stopped() {
373 let score = ScoreDto::hard_soft(-5, -30);
374 let status = SolveStatus::stopped(15000, Some(score));
375
376 assert_eq!(status.state, SolveState::Stopped);
377 assert!(status.best_score.is_some());
378 }
379
380 #[test]
381 fn test_async_solve_response() {
382 let response = AsyncSolveResponse::new("solve-12345");
383 assert_eq!(response.solve_id, "solve-12345");
384 }
385
386 #[test]
387 fn test_solve_handle() {
388 let handle = SolveHandle::new("solve-12345");
389 assert_eq!(handle.id, "solve-12345");
390 }
391
392 #[test]
393 fn test_solve_response_json_serialization() {
394 let response = SolveResponse::new(r#"{"data": "test"}"#.to_string(), "0hard/-15soft");
395
396 let json = serde_json::to_string(&response).unwrap();
397 assert!(json.contains("\"solution\""));
398 assert!(json.contains("\"score\":\"0hard/-15soft\""));
399
400 let parsed: SolveResponse = serde_json::from_str(&json).unwrap();
401 assert_eq!(parsed, response);
402 }
403
404 #[test]
405 fn test_score_dto_json_omits_medium_when_none() {
406 let score = ScoreDto::hard_soft(0, -10);
407 let json = serde_json::to_string(&score).unwrap();
408 assert!(!json.contains("mediumScore"));
409 }
410
411 #[test]
412 fn test_solve_status_json_serialization() {
413 let status = SolveStatus::running(10000, Some(ScoreDto::hard_soft(-2, -100)));
414
415 let json = serde_json::to_string(&status).unwrap();
416 assert!(json.contains("\"state\":\"RUNNING\""));
417 assert!(json.contains("\"timeSpentMs\":10000"));
418
419 let parsed: SolveStatus = serde_json::from_str(&json).unwrap();
420 assert_eq!(parsed, status);
421 }
422
423 #[test]
424 fn test_solve_state_json_serialization() {
425 assert_eq!(
426 serde_json::to_string(&SolveState::Pending).unwrap(),
427 "\"PENDING\""
428 );
429 assert_eq!(
430 serde_json::to_string(&SolveState::Running).unwrap(),
431 "\"RUNNING\""
432 );
433 assert_eq!(
434 serde_json::to_string(&SolveState::Completed).unwrap(),
435 "\"COMPLETED\""
436 );
437 assert_eq!(
438 serde_json::to_string(&SolveState::Failed).unwrap(),
439 "\"FAILED\""
440 );
441 assert_eq!(
442 serde_json::to_string(&SolveState::Stopped).unwrap(),
443 "\"STOPPED\""
444 );
445 }
446
447 #[test]
448 fn test_score_dto_clone() {
449 let score = ScoreDto::hard_soft(0, -10);
450 let cloned = score.clone();
451 assert_eq!(score, cloned);
452 }
453
454 #[test]
455 fn test_solve_response_debug() {
456 let response = SolveResponse::new("{}".to_string(), "0");
457 let debug = format!("{:?}", response);
458 assert!(debug.contains("SolveResponse"));
459 }
460
461 #[test]
462 fn test_solve_status_debug() {
463 let status = SolveStatus::pending();
464 let debug = format!("{:?}", status);
465 assert!(debug.contains("SolveStatus"));
466 }
467}