Skip to main content

romm_cli/endpoints/
collections.rs

1use crate::types::{Collection, VirtualCollectionRow};
2
3use super::Endpoint;
4
5/// RomM may return a bare array or a paged envelope; normalize with [`CollectionsList::into_vec`].
6#[derive(Debug, serde::Deserialize)]
7#[serde(untagged)]
8pub enum CollectionsList {
9    List(Vec<Collection>),
10    Paged { items: Vec<Collection> },
11}
12
13impl CollectionsList {
14    pub fn into_vec(self) -> Vec<Collection> {
15        match self {
16            CollectionsList::List(v) => v,
17            CollectionsList::Paged { items } => items,
18        }
19    }
20}
21
22/// Combine manual, smart, and virtual (autogenerated) collection lists for the library UI.
23pub fn merge_all_collection_sources(
24    mut manual: Vec<Collection>,
25    mut smart: Vec<Collection>,
26    virtual_rows: Vec<VirtualCollectionRow>,
27) -> Vec<Collection> {
28    for c in &mut manual {
29        c.is_smart = false;
30        c.is_virtual = false;
31        c.virtual_id = None;
32    }
33    for c in &mut smart {
34        c.is_smart = true;
35        c.is_virtual = false;
36        c.virtual_id = None;
37    }
38    let mut virtual_collections: Vec<Collection> =
39        virtual_rows.into_iter().map(Collection::from).collect();
40    manual.append(&mut smart);
41    manual.append(&mut virtual_collections);
42    manual
43}
44
45/// Combine manual and smart collection lists (tests and callers that skip virtual).
46pub fn merge_manual_and_smart(manual: Vec<Collection>, smart: Vec<Collection>) -> Vec<Collection> {
47    merge_all_collection_sources(manual, smart, Vec::new())
48}
49
50/// List manual (user) collections. RomM API: GET /api/collections.
51#[derive(Debug, Default, Clone)]
52pub struct ListCollections;
53
54impl Endpoint for ListCollections {
55    type Output = CollectionsList;
56
57    fn method(&self) -> &'static str {
58        "GET"
59    }
60
61    fn path(&self) -> String {
62        "/api/collections".into()
63    }
64}
65
66/// List smart collections. RomM API: GET /api/collections/smart (separate from manual collections).
67#[derive(Debug, Default, Clone)]
68pub struct ListSmartCollections;
69
70impl Endpoint for ListSmartCollections {
71    type Output = CollectionsList;
72
73    fn method(&self) -> &'static str {
74        "GET"
75    }
76
77    fn path(&self) -> String {
78        "/api/collections/smart".into()
79    }
80}
81
82/// List virtual (autogenerated) collections. RomM API: GET /api/collections/virtual?type=...
83#[derive(Debug, Default, Clone)]
84pub struct ListVirtualCollections;
85
86impl Endpoint for ListVirtualCollections {
87    type Output = Vec<VirtualCollectionRow>;
88
89    fn method(&self) -> &'static str {
90        "GET"
91    }
92
93    fn path(&self) -> String {
94        "/api/collections/virtual".into()
95    }
96
97    fn query(&self) -> Vec<(String, String)> {
98        vec![("type".into(), "all".into())]
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::types::{Collection, VirtualCollectionRow};
106
107    #[test]
108    fn decode_bare_array() {
109        let v = serde_json::json!([
110            {"id": 1, "name": "A", "rom_count": 2}
111        ]);
112        let list: CollectionsList = serde_json::from_value(v).unwrap();
113        let list = list.into_vec();
114        assert_eq!(list.len(), 1);
115        assert_eq!(list[0].id, 1);
116        assert_eq!(list[0].name, "A");
117    }
118
119    #[test]
120    fn decode_paged_object() {
121        let v = serde_json::json!({
122            "items": [{"id": 2, "name": "B", "rom_count": 0}],
123            "total": 1
124        });
125        let list: CollectionsList = serde_json::from_value(v).unwrap();
126        let list = list.into_vec();
127        assert_eq!(list.len(), 1);
128        assert_eq!(list[0].id, 2);
129    }
130
131    #[test]
132    fn merge_manual_and_smart_marks_flags() {
133        let manual = vec![Collection {
134            id: 1,
135            name: "m".into(),
136            collection_type: None,
137            rom_count: Some(1),
138            is_smart: true,
139            is_virtual: false,
140            virtual_id: None,
141        }];
142        let smart = vec![Collection {
143            id: 2,
144            name: "s".into(),
145            collection_type: None,
146            rom_count: Some(2),
147            is_smart: false,
148            is_virtual: false,
149            virtual_id: None,
150        }];
151        let merged = super::merge_manual_and_smart(manual, smart);
152        assert_eq!(merged.len(), 2);
153        assert!(!merged[0].is_smart);
154        assert!(merged[1].is_smart);
155    }
156
157    #[test]
158    fn merge_all_includes_virtual() {
159        let manual = vec![Collection {
160            id: 1,
161            name: "m".into(),
162            collection_type: None,
163            rom_count: Some(1),
164            is_smart: false,
165            is_virtual: false,
166            virtual_id: None,
167        }];
168        let virtual_rows = vec![VirtualCollectionRow {
169            id: "recent".into(),
170            name: "Recent".into(),
171            collection_type: "recent".into(),
172            rom_count: 3,
173            is_virtual: true,
174        }];
175        let merged = super::merge_all_collection_sources(manual, Vec::new(), virtual_rows);
176        assert_eq!(merged.len(), 2);
177        assert!(merged[1].is_virtual);
178        assert_eq!(merged[1].virtual_id.as_deref(), Some("recent"));
179    }
180}