romm-cli 0.22.0

Rust-based CLI and TUI for the ROMM API
Documentation
use crate::types::{Collection, VirtualCollectionRow};

use super::Endpoint;

/// RomM may return a bare array or a paged envelope; normalize with [`CollectionsList::into_vec`].
#[derive(Debug, serde::Deserialize)]
#[serde(untagged)]
pub enum CollectionsList {
    List(Vec<Collection>),
    Paged { items: Vec<Collection> },
}

impl CollectionsList {
    pub fn into_vec(self) -> Vec<Collection> {
        match self {
            CollectionsList::List(v) => v,
            CollectionsList::Paged { items } => items,
        }
    }
}

/// Combine manual, smart, and virtual (autogenerated) collection lists for the library UI.
pub fn merge_all_collection_sources(
    mut manual: Vec<Collection>,
    mut smart: Vec<Collection>,
    virtual_rows: Vec<VirtualCollectionRow>,
) -> Vec<Collection> {
    for c in &mut manual {
        c.is_smart = false;
        c.is_virtual = false;
        c.virtual_id = None;
    }
    for c in &mut smart {
        c.is_smart = true;
        c.is_virtual = false;
        c.virtual_id = None;
    }
    let mut virtual_collections: Vec<Collection> =
        virtual_rows.into_iter().map(Collection::from).collect();
    manual.append(&mut smart);
    manual.append(&mut virtual_collections);
    manual
}

/// Combine manual and smart collection lists (tests and callers that skip virtual).
pub fn merge_manual_and_smart(manual: Vec<Collection>, smart: Vec<Collection>) -> Vec<Collection> {
    merge_all_collection_sources(manual, smart, Vec::new())
}

/// List manual (user) collections. RomM API: GET /api/collections.
#[derive(Debug, Default, Clone)]
pub struct ListCollections;

impl Endpoint for ListCollections {
    type Output = CollectionsList;

    fn method(&self) -> &'static str {
        "GET"
    }

    fn path(&self) -> String {
        "/api/collections".into()
    }
}

/// List smart collections. RomM API: GET /api/collections/smart (separate from manual collections).
#[derive(Debug, Default, Clone)]
pub struct ListSmartCollections;

impl Endpoint for ListSmartCollections {
    type Output = CollectionsList;

    fn method(&self) -> &'static str {
        "GET"
    }

    fn path(&self) -> String {
        "/api/collections/smart".into()
    }
}

/// List virtual (autogenerated) collections. RomM API: GET /api/collections/virtual?type=...
#[derive(Debug, Default, Clone)]
pub struct ListVirtualCollections;

impl Endpoint for ListVirtualCollections {
    type Output = Vec<VirtualCollectionRow>;

    fn method(&self) -> &'static str {
        "GET"
    }

    fn path(&self) -> String {
        "/api/collections/virtual".into()
    }

    fn query(&self) -> Vec<(String, String)> {
        vec![("type".into(), "all".into())]
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::types::{Collection, VirtualCollectionRow};

    #[test]
    fn decode_bare_array() {
        let v = serde_json::json!([
            {"id": 1, "name": "A", "rom_count": 2}
        ]);
        let list: CollectionsList = serde_json::from_value(v).unwrap();
        let list = list.into_vec();
        assert_eq!(list.len(), 1);
        assert_eq!(list[0].id, 1);
        assert_eq!(list[0].name, "A");
    }

    #[test]
    fn decode_paged_object() {
        let v = serde_json::json!({
            "items": [{"id": 2, "name": "B", "rom_count": 0}],
            "total": 1
        });
        let list: CollectionsList = serde_json::from_value(v).unwrap();
        let list = list.into_vec();
        assert_eq!(list.len(), 1);
        assert_eq!(list[0].id, 2);
    }

    #[test]
    fn merge_manual_and_smart_marks_flags() {
        let manual = vec![Collection {
            id: 1,
            name: "m".into(),
            collection_type: None,
            rom_count: Some(1),
            is_smart: true,
            is_virtual: false,
            virtual_id: None,
        }];
        let smart = vec![Collection {
            id: 2,
            name: "s".into(),
            collection_type: None,
            rom_count: Some(2),
            is_smart: false,
            is_virtual: false,
            virtual_id: None,
        }];
        let merged = super::merge_manual_and_smart(manual, smart);
        assert_eq!(merged.len(), 2);
        assert!(!merged[0].is_smart);
        assert!(merged[1].is_smart);
    }

    #[test]
    fn merge_all_includes_virtual() {
        let manual = vec![Collection {
            id: 1,
            name: "m".into(),
            collection_type: None,
            rom_count: Some(1),
            is_smart: false,
            is_virtual: false,
            virtual_id: None,
        }];
        let virtual_rows = vec![VirtualCollectionRow {
            id: "recent".into(),
            name: "Recent".into(),
            collection_type: "recent".into(),
            rom_count: 3,
            is_virtual: true,
        }];
        let merged = super::merge_all_collection_sources(manual, Vec::new(), virtual_rows);
        assert_eq!(merged.len(), 2);
        assert!(merged[1].is_virtual);
        assert_eq!(merged[1].virtual_id.as_deref(), Some("recent"));
    }
}