Skip to main content

romm_cli/
services.rs

1//! Small service objects that wrap `RommClient` for higher-level operations.
2//!
3//! These are used by the CLI commands to keep a clear separation between
4//! \"how we talk to ROMM\" (HTTP) and \"what we want to do\" (list
5//! platforms, search ROMs, etc.).
6
7use anyhow::{anyhow, Result};
8
9use crate::client::RommClient;
10use crate::endpoints::collections::{ListCollections, ListSmartCollections};
11use crate::endpoints::{
12    platforms::{GetPlatform, ListPlatforms},
13    roms::{GetRom, GetRoms},
14};
15use crate::types::{Collection, Platform, Rom, RomList};
16
17/// Service for interacting with platform-related API endpoints.
18///
19/// This service provides higher-level methods for listing and retrieving
20/// platforms, abstracting away the underlying endpoint definitions.
21pub struct PlatformService<'a> {
22    client: &'a RommClient,
23}
24
25impl<'a> PlatformService<'a> {
26    /// Creates a new `PlatformService` using the provided client.
27    pub fn new(client: &'a RommClient) -> Self {
28        Self { client }
29    }
30
31    /// Lists all platforms from the RomM API.
32    pub async fn list_platforms(&self) -> Result<Vec<Platform>> {
33        let platforms = self.client.call(&ListPlatforms).await?;
34        Ok(platforms)
35    }
36
37    /// Retrieves a single platform by its unique identifier.
38    pub async fn get_platform(&self, id: u64) -> Result<Platform> {
39        let platform = self.client.call(&GetPlatform { id }).await?;
40        Ok(platform)
41    }
42}
43
44/// Service for interacting with ROM-related API endpoints.
45///
46/// This service provides methods for searching and retrieving ROMs,
47/// abstracting away the underlying endpoint definitions.
48pub struct RomService<'a> {
49    client: &'a RommClient,
50}
51
52impl<'a> RomService<'a> {
53    /// Creates a new `RomService` using the provided client.
54    pub fn new(client: &'a RommClient) -> Self {
55        Self { client }
56    }
57
58    /// Searches or lists ROMs using the provided request descriptor.
59    pub async fn search_roms(&self, ep: &GetRoms) -> Result<RomList> {
60        let results = self.client.call(ep).await?;
61        Ok(results)
62    }
63
64    /// Retrieves a single ROM by its unique identifier.
65    pub async fn get_rom(&self, id: u64) -> Result<Rom> {
66        let rom = self.client.call(&GetRom { id }).await?;
67        Ok(rom)
68    }
69}
70
71/// Resolves a platform ID from a string query by matching against slugs, names, and custom names.
72///
73/// This is used to handle platform lookups from CLI arguments where the user
74/// might provide a name or slug instead of a numeric ID.
75pub fn resolve_platform_id_from_list(query: &str, platforms: &[Platform]) -> Result<u64> {
76    let normalized = query.trim().to_ascii_lowercase();
77
78    if let Some(platform) = platforms.iter().find(|p| {
79        p.slug.eq_ignore_ascii_case(&normalized) || p.fs_slug.eq_ignore_ascii_case(&normalized)
80    }) {
81        return Ok(platform.id);
82    }
83
84    let exact_name_matches: Vec<&Platform> = platforms
85        .iter()
86        .filter(|p| {
87            p.name.eq_ignore_ascii_case(&normalized)
88                || p.display_name
89                    .as_deref()
90                    .is_some_and(|name| name.eq_ignore_ascii_case(&normalized))
91                || p.custom_name
92                    .as_deref()
93                    .is_some_and(|name| name.eq_ignore_ascii_case(&normalized))
94        })
95        .collect();
96
97    match exact_name_matches.len() {
98        1 => Ok(exact_name_matches[0].id),
99        0 => Err(anyhow!(
100            "No platform found for '{}'. Use 'romm-cli platforms list' to inspect available values.",
101            query
102        )),
103        _ => {
104            let names = exact_name_matches
105                .iter()
106                .map(|p| format!("{} ({})", p.name, p.id))
107                .collect::<Vec<_>>()
108                .join(", ");
109            Err(anyhow!(
110                "Platform '{}' is ambiguous. Matches: {}. Please use a more specific --platform value.",
111                query,
112                names
113            ))
114        }
115    }
116}
117
118/// Resolves a platform query (slug or name) to a numeric ID.
119///
120/// If the query is empty or `None`, returns `Ok(None)`.
121pub async fn resolve_platform_id(
122    client: &RommClient,
123    platform_query: Option<&str>,
124) -> Result<Option<u64>> {
125    let Some(query) = platform_query.map(str::trim).filter(|q| !q.is_empty()) else {
126        return Ok(None);
127    };
128    let service = PlatformService::new(client);
129    let platforms = service.list_platforms().await?;
130    resolve_platform_id_from_list(query, &platforms).map(Some)
131}
132
133/// Resolves multiple platform queries to a list of unique numeric IDs.
134pub async fn resolve_platform_ids(client: &RommClient, names: &[String]) -> Result<Vec<u64>> {
135    if names.is_empty() {
136        return Ok(Vec::new());
137    }
138    let service = PlatformService::new(client);
139    let platforms = service.list_platforms().await?;
140    let mut out = Vec::new();
141    for n in names {
142        let id = resolve_platform_id_from_list(n.trim(), &platforms)?;
143        if !out.contains(&id) {
144            out.push(id);
145        }
146    }
147    Ok(out)
148}
149
150fn match_collections_by_name<'a>(q: &str, collections: &'a [Collection]) -> Vec<&'a Collection> {
151    let n = q.trim().to_ascii_lowercase();
152    collections
153        .iter()
154        .filter(|c| c.name.eq_ignore_ascii_case(&n))
155        .collect()
156}
157
158/// Resolves a manual collection by ID or exact name.
159pub async fn resolve_manual_collection_id(
160    client: &RommClient,
161    query: Option<&str>,
162) -> Result<Option<u64>> {
163    let Some(q) = query.map(str::trim).filter(|s| !s.is_empty()) else {
164        return Ok(None);
165    };
166    if let Ok(id) = q.parse::<u64>() {
167        return Ok(Some(id));
168    }
169    let list = client.call(&ListCollections).await?.into_vec();
170    let matches = match_collections_by_name(q, &list);
171    match matches.len() {
172        0 => Err(anyhow!(
173            "No manual collection named '{}'. Use `romm-cli collections list`.",
174            q
175        )),
176        1 => Ok(Some(matches[0].id)),
177        _ => Err(anyhow!(
178            "Manual collection '{}' is ambiguous among {} matches; use a numeric id.",
179            q,
180            matches.len()
181        )),
182    }
183}
184
185/// Resolves a smart collection by ID or exact name.
186pub async fn resolve_smart_collection_id(
187    client: &RommClient,
188    query: Option<&str>,
189) -> Result<Option<u64>> {
190    let Some(q) = query.map(str::trim).filter(|s| !s.is_empty()) else {
191        return Ok(None);
192    };
193    if let Ok(id) = q.parse::<u64>() {
194        return Ok(Some(id));
195    }
196    let list = client.call(&ListSmartCollections).await?.into_vec();
197    let matches = match_collections_by_name(q, &list);
198    match matches.len() {
199        0 => Err(anyhow!(
200            "No smart collection named '{}'. Use `romm-cli collections list`.",
201            q
202        )),
203        1 => Ok(Some(matches[0].id)),
204        _ => Err(anyhow!(
205            "Smart collection '{}' is ambiguous among {} matches; use a numeric id.",
206            q,
207            matches.len()
208        )),
209    }
210}