htb-cli 0.1.8

Hack The Box CLI
Documentation
use serde::{Deserialize, Serialize};

use crate::output::Tabular;

#[derive(Debug, Deserialize, Serialize)]
pub struct Sherlock {
    pub id: u64,
    pub name: String,
    #[serde(default)]
    pub difficulty: Option<String>,
    #[serde(default)]
    pub state: Option<String>,
    #[serde(default)]
    pub category_id: Option<u64>,
    #[serde(default)]
    pub category_name: Option<String>,
    #[serde(default)]
    pub solves: u32,
    #[serde(default)]
    pub is_owned: bool,
    #[serde(default)]
    pub rating: Option<f64>,
    #[serde(default)]
    pub rating_count: Option<u32>,
    #[serde(default)]
    pub progress: Option<u32>,
    #[serde(default)]
    pub release_date: Option<String>,
    #[serde(default)]
    pub play_methods: Vec<String>,
    #[serde(default)]
    pub labels: Vec<super::machine::Label>,
}

#[derive(Debug, Deserialize)]
pub struct SherlockCategoriesResponse {
    pub info: Vec<SherlockCategory>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct SherlockCategory {
    pub id: u64,
    pub name: String,
    #[serde(default)]
    pub icon: Option<String>,
}

impl Tabular for Sherlock {
    fn headers() -> Vec<&'static str> {
        vec![
            "ID",
            "Name",
            "Difficulty",
            "Category",
            "Solves",
            "Rating",
            "Progress",
            "State",
        ]
    }

    fn row(&self) -> Vec<String> {
        let progress = self.progress.map(|p| format!("{p}%")).unwrap_or_default();
        vec![
            self.id.to_string(),
            self.name.clone(),
            self.difficulty.clone().unwrap_or_default(),
            self.category_name.clone().unwrap_or_default(),
            self.solves.to_string(),
            self.rating.map(|r| format!("{r:.1}")).unwrap_or_default(),
            progress,
            self.state.clone().unwrap_or_default(),
        ]
    }
}

impl Tabular for SherlockCategory {
    fn headers() -> Vec<&'static str> {
        vec!["ID", "Name"]
    }

    fn row(&self) -> Vec<String> {
        vec![self.id.to_string(), self.name.clone()]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::Paginated;

    #[test]
    fn deserialize_sherlocks_list() {
        let json = include_str!("../../tests/fixtures/sherlocks-list.json");
        let result: Paginated<Sherlock> = serde_json::from_str(json).unwrap();
        assert!(!result.data.is_empty());
        assert_eq!(result.data[0].name, "Brutus");
        assert_eq!(result.data[0].category_name.as_deref(), Some("DFIR"));
    }

    #[test]
    fn deserialize_sherlock_categories() {
        let json = include_str!("../../tests/fixtures/sherlocks-categories.json");
        let result: SherlockCategoriesResponse = serde_json::from_str(json).unwrap();
        assert!(!result.info.is_empty());
        assert!(result.info.iter().any(|c| c.name == "DFIR"));
    }
}