Skip to main content

htb_cli/models/
sherlock.rs

1use serde::{Deserialize, Serialize};
2
3use crate::output::Tabular;
4
5#[derive(Debug, Deserialize, Serialize)]
6pub struct Sherlock {
7    pub id: u64,
8    pub name: String,
9    #[serde(default)]
10    pub difficulty: Option<String>,
11    #[serde(default)]
12    pub state: Option<String>,
13    #[serde(default)]
14    pub category_id: Option<u64>,
15    #[serde(default)]
16    pub category_name: Option<String>,
17    #[serde(default)]
18    pub solves: u32,
19    #[serde(default)]
20    pub is_owned: bool,
21    #[serde(default)]
22    pub rating: Option<f64>,
23    #[serde(default)]
24    pub rating_count: Option<u32>,
25    #[serde(default)]
26    pub progress: Option<u32>,
27    #[serde(default)]
28    pub release_date: Option<String>,
29    #[serde(default)]
30    pub play_methods: Vec<String>,
31    #[serde(default)]
32    pub labels: Vec<super::machine::Label>,
33}
34
35#[derive(Debug, Deserialize)]
36pub struct SherlockCategoriesResponse {
37    pub info: Vec<SherlockCategory>,
38}
39
40#[derive(Debug, Deserialize, Serialize)]
41pub struct SherlockCategory {
42    pub id: u64,
43    pub name: String,
44    #[serde(default)]
45    pub icon: Option<String>,
46}
47
48impl Tabular for Sherlock {
49    fn headers() -> Vec<&'static str> {
50        vec![
51            "ID",
52            "Name",
53            "Difficulty",
54            "Category",
55            "Solves",
56            "Rating",
57            "Progress",
58            "State",
59        ]
60    }
61
62    fn row(&self) -> Vec<String> {
63        let progress = self.progress.map(|p| format!("{p}%")).unwrap_or_default();
64        vec![
65            self.id.to_string(),
66            self.name.clone(),
67            self.difficulty.clone().unwrap_or_default(),
68            self.category_name.clone().unwrap_or_default(),
69            self.solves.to_string(),
70            self.rating.map(|r| format!("{r:.1}")).unwrap_or_default(),
71            progress,
72            self.state.clone().unwrap_or_default(),
73        ]
74    }
75}
76
77impl Tabular for SherlockCategory {
78    fn headers() -> Vec<&'static str> {
79        vec!["ID", "Name"]
80    }
81
82    fn row(&self) -> Vec<String> {
83        vec![self.id.to_string(), self.name.clone()]
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90    use crate::models::Paginated;
91
92    #[test]
93    fn deserialize_sherlocks_list() {
94        let json = include_str!("../../tests/fixtures/sherlocks-list.json");
95        let result: Paginated<Sherlock> = serde_json::from_str(json).unwrap();
96        assert!(!result.data.is_empty());
97        assert_eq!(result.data[0].name, "Brutus");
98        assert_eq!(result.data[0].category_name.as_deref(), Some("DFIR"));
99    }
100
101    #[test]
102    fn deserialize_sherlock_categories() {
103        let json = include_str!("../../tests/fixtures/sherlocks-categories.json");
104        let result: SherlockCategoriesResponse = serde_json::from_str(json).unwrap();
105        assert!(!result.info.is_empty());
106        assert!(result.info.iter().any(|c| c.name == "DFIR"));
107    }
108}