Skip to main content

romm_cli/endpoints/
collections.rs

1use crate::types::{Collection, VirtualCollectionRow};
2
3use super::Endpoint;
4use serde_json::Value;
5
6/// RomM may return a bare array or a paged envelope; normalize with [`CollectionsList::into_vec`].
7#[derive(Debug, serde::Deserialize)]
8#[serde(untagged)]
9pub enum CollectionsList {
10    List(Vec<Collection>),
11    Paged { items: Vec<Collection> },
12}
13
14impl CollectionsList {
15    pub fn into_vec(self) -> Vec<Collection> {
16        match self {
17            CollectionsList::List(v) => v,
18            CollectionsList::Paged { items } => items,
19        }
20    }
21}
22
23/// Combine manual, smart, and virtual (autogenerated) collection lists for the library UI.
24pub fn merge_all_collection_sources(
25    mut manual: Vec<Collection>,
26    mut smart: Vec<Collection>,
27    virtual_rows: Vec<VirtualCollectionRow>,
28) -> Vec<Collection> {
29    for c in &mut manual {
30        c.is_smart = false;
31        c.is_virtual = false;
32        c.virtual_id = None;
33    }
34    for c in &mut smart {
35        c.is_smart = true;
36        c.is_virtual = false;
37        c.virtual_id = None;
38    }
39    let mut virtual_collections: Vec<Collection> =
40        virtual_rows.into_iter().map(Collection::from).collect();
41    manual.append(&mut smart);
42    manual.append(&mut virtual_collections);
43    manual
44}
45
46/// Combine manual and smart collection lists (tests and callers that skip virtual).
47pub fn merge_manual_and_smart(manual: Vec<Collection>, smart: Vec<Collection>) -> Vec<Collection> {
48    merge_all_collection_sources(manual, smart, Vec::new())
49}
50
51/// List manual (user) collections. RomM API: GET /api/collections.
52#[derive(Debug, Default, Clone)]
53pub struct ListCollections;
54
55impl Endpoint for ListCollections {
56    type Output = CollectionsList;
57
58    fn method(&self) -> &'static str {
59        "GET"
60    }
61
62    fn path(&self) -> String {
63        "/api/collections".into()
64    }
65}
66
67/// List smart collections. RomM API: GET /api/collections/smart (separate from manual collections).
68#[derive(Debug, Default, Clone)]
69pub struct ListSmartCollections;
70
71impl Endpoint for ListSmartCollections {
72    type Output = CollectionsList;
73
74    fn method(&self) -> &'static str {
75        "GET"
76    }
77
78    fn path(&self) -> String {
79        "/api/collections/smart".into()
80    }
81}
82
83/// List virtual (autogenerated) collections. RomM API: GET /api/collections/virtual?type=...
84#[derive(Debug, Default, Clone)]
85pub struct ListVirtualCollections;
86
87impl Endpoint for ListVirtualCollections {
88    type Output = Vec<VirtualCollectionRow>;
89
90    fn method(&self) -> &'static str {
91        "GET"
92    }
93
94    fn path(&self) -> String {
95        "/api/collections/virtual".into()
96    }
97
98    fn query(&self) -> Vec<(String, String)> {
99        vec![("type".into(), "all".into())]
100    }
101}
102
103/// `GET /api/collections/{id}`
104#[derive(Debug, Clone)]
105pub struct GetManualCollection {
106    pub id: u64,
107}
108
109impl Endpoint for GetManualCollection {
110    type Output = Value;
111
112    fn method(&self) -> &'static str {
113        "GET"
114    }
115
116    fn path(&self) -> String {
117        format!("/api/collections/{}", self.id)
118    }
119}
120
121/// `GET /api/collections/smart/{id}`
122#[derive(Debug, Clone)]
123pub struct GetSmartCollection {
124    pub id: u64,
125}
126
127impl Endpoint for GetSmartCollection {
128    type Output = Value;
129
130    fn method(&self) -> &'static str {
131        "GET"
132    }
133
134    fn path(&self) -> String {
135        format!("/api/collections/smart/{}", self.id)
136    }
137}
138
139/// `GET /api/collections/virtual/{id}`
140#[derive(Debug, Clone)]
141pub struct GetVirtualCollection {
142    pub id: String,
143}
144
145impl Endpoint for GetVirtualCollection {
146    type Output = Value;
147
148    fn method(&self) -> &'static str {
149        "GET"
150    }
151
152    fn path(&self) -> String {
153        format!("/api/collections/virtual/{}", self.id)
154    }
155}
156
157/// `DELETE /api/collections/{id}`
158#[derive(Debug, Clone)]
159pub struct DeleteManualCollection {
160    pub id: u64,
161}
162
163impl Endpoint for DeleteManualCollection {
164    type Output = Value;
165
166    fn method(&self) -> &'static str {
167        "DELETE"
168    }
169
170    fn path(&self) -> String {
171        format!("/api/collections/{}", self.id)
172    }
173}
174
175/// `DELETE /api/collections/smart/{id}`
176#[derive(Debug, Clone)]
177pub struct DeleteSmartCollection {
178    pub id: u64,
179}
180
181impl Endpoint for DeleteSmartCollection {
182    type Output = Value;
183
184    fn method(&self) -> &'static str {
185        "DELETE"
186    }
187
188    fn path(&self) -> String {
189        format!("/api/collections/smart/{}", self.id)
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196    use crate::types::{Collection, VirtualCollectionRow};
197
198    #[test]
199    fn decode_bare_array() {
200        let v = serde_json::json!([
201            {"id": 1, "name": "A", "rom_count": 2}
202        ]);
203        let list: CollectionsList = serde_json::from_value(v).unwrap();
204        let list = list.into_vec();
205        assert_eq!(list.len(), 1);
206        assert_eq!(list[0].id, 1);
207        assert_eq!(list[0].name, "A");
208    }
209
210    #[test]
211    fn decode_paged_object() {
212        let v = serde_json::json!({
213            "items": [{"id": 2, "name": "B", "rom_count": 0}],
214            "total": 1
215        });
216        let list: CollectionsList = serde_json::from_value(v).unwrap();
217        let list = list.into_vec();
218        assert_eq!(list.len(), 1);
219        assert_eq!(list[0].id, 2);
220    }
221
222    #[test]
223    fn merge_manual_and_smart_marks_flags() {
224        let manual = vec![Collection {
225            id: 1,
226            name: "m".into(),
227            collection_type: None,
228            rom_count: Some(1),
229            is_smart: true,
230            is_virtual: false,
231            virtual_id: None,
232        }];
233        let smart = vec![Collection {
234            id: 2,
235            name: "s".into(),
236            collection_type: None,
237            rom_count: Some(2),
238            is_smart: false,
239            is_virtual: false,
240            virtual_id: None,
241        }];
242        let merged = super::merge_manual_and_smart(manual, smart);
243        assert_eq!(merged.len(), 2);
244        assert!(!merged[0].is_smart);
245        assert!(merged[1].is_smart);
246    }
247
248    #[test]
249    fn merge_all_includes_virtual() {
250        let manual = vec![Collection {
251            id: 1,
252            name: "m".into(),
253            collection_type: None,
254            rom_count: Some(1),
255            is_smart: false,
256            is_virtual: false,
257            virtual_id: None,
258        }];
259        let virtual_rows = vec![VirtualCollectionRow {
260            id: "recent".into(),
261            name: "Recent".into(),
262            collection_type: "recent".into(),
263            rom_count: 3,
264            is_virtual: true,
265        }];
266        let merged = super::merge_all_collection_sources(manual, Vec::new(), virtual_rows);
267        assert_eq!(merged.len(), 2);
268        assert!(merged[1].is_virtual);
269        assert_eq!(merged[1].virtual_id.as_deref(), Some("recent"));
270    }
271}