Skip to main content

romm_api/endpoints/
collections.rs

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