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 encapsulating platform-related operations.
18pub struct PlatformService<'a> {
19    client: &'a RommClient,
20}
21
22impl<'a> PlatformService<'a> {
23    pub fn new(client: &'a RommClient) -> Self {
24        Self { client }
25    }
26
27    /// List all platforms from the ROMM API.
28    pub async fn list_platforms(&self) -> Result<Vec<Platform>> {
29        let platforms = self.client.call(&ListPlatforms).await?;
30        Ok(platforms)
31    }
32
33    /// Get a single platform by ID.
34    pub async fn get_platform(&self, id: u64) -> Result<Platform> {
35        let platform = self.client.call(&GetPlatform { id }).await?;
36        Ok(platform)
37    }
38}
39
40/// Service encapsulating ROM-related operations.
41pub struct RomService<'a> {
42    client: &'a RommClient,
43}
44
45impl<'a> RomService<'a> {
46    pub fn new(client: &'a RommClient) -> Self {
47        Self { client }
48    }
49
50    /// Search/list ROMs using a fully-configured `GetRoms` descriptor.
51    pub async fn search_roms(&self, ep: &GetRoms) -> Result<RomList> {
52        let results = self.client.call(ep).await?;
53        Ok(results)
54    }
55
56    /// Get a single ROM by ID.
57    pub async fn get_rom(&self, id: u64) -> Result<Rom> {
58        let rom = self.client.call(&GetRom { id }).await?;
59        Ok(rom)
60    }
61}
62
63/// Resolve a numeric platform ID, or match by slug / display name / custom name (same rules as the CLI).
64pub fn resolve_platform_id_from_list(query: &str, platforms: &[Platform]) -> Result<u64> {
65    let normalized = query.trim().to_ascii_lowercase();
66
67    if let Some(platform) = platforms.iter().find(|p| {
68        p.slug.eq_ignore_ascii_case(&normalized) || p.fs_slug.eq_ignore_ascii_case(&normalized)
69    }) {
70        return Ok(platform.id);
71    }
72
73    let exact_name_matches: Vec<&Platform> = platforms
74        .iter()
75        .filter(|p| {
76            p.name.eq_ignore_ascii_case(&normalized)
77                || p.display_name
78                    .as_deref()
79                    .is_some_and(|name| name.eq_ignore_ascii_case(&normalized))
80                || p.custom_name
81                    .as_deref()
82                    .is_some_and(|name| name.eq_ignore_ascii_case(&normalized))
83        })
84        .collect();
85
86    match exact_name_matches.len() {
87        1 => Ok(exact_name_matches[0].id),
88        0 => Err(anyhow!(
89            "No platform found for '{}'. Use 'romm-cli platforms list' to inspect available values.",
90            query
91        )),
92        _ => {
93            let names = exact_name_matches
94                .iter()
95                .map(|p| format!("{} ({})", p.name, p.id))
96                .collect::<Vec<_>>()
97                .join(", ");
98            Err(anyhow!(
99                "Platform '{}' is ambiguous. Matches: {}. Please use a more specific --platform value.",
100                query,
101                names
102            ))
103        }
104    }
105}
106
107/// Resolve optional platform slug/name to a single RomM `platform_ids` value.
108pub async fn resolve_platform_id(
109    client: &RommClient,
110    platform_query: Option<&str>,
111) -> Result<Option<u64>> {
112    let Some(query) = platform_query.map(str::trim).filter(|q| !q.is_empty()) else {
113        return Ok(None);
114    };
115    let service = PlatformService::new(client);
116    let platforms = service.list_platforms().await?;
117    resolve_platform_id_from_list(query, &platforms).map(Some)
118}
119
120/// Resolve several platform names/slugs to IDs (deduped, stable order).
121pub async fn resolve_platform_ids(client: &RommClient, names: &[String]) -> Result<Vec<u64>> {
122    if names.is_empty() {
123        return Ok(Vec::new());
124    }
125    let service = PlatformService::new(client);
126    let platforms = service.list_platforms().await?;
127    let mut out = Vec::new();
128    for n in names {
129        let id = resolve_platform_id_from_list(n.trim(), &platforms)?;
130        if !out.contains(&id) {
131            out.push(id);
132        }
133    }
134    Ok(out)
135}
136
137fn match_collections_by_name<'a>(q: &str, collections: &'a [Collection]) -> Vec<&'a Collection> {
138    let n = q.trim().to_ascii_lowercase();
139    collections
140        .iter()
141        .filter(|c| c.name.eq_ignore_ascii_case(&n))
142        .collect()
143}
144
145/// Resolve manual collection by numeric id or exact name (manual collections only).
146pub async fn resolve_manual_collection_id(
147    client: &RommClient,
148    query: Option<&str>,
149) -> Result<Option<u64>> {
150    let Some(q) = query.map(str::trim).filter(|s| !s.is_empty()) else {
151        return Ok(None);
152    };
153    if let Ok(id) = q.parse::<u64>() {
154        return Ok(Some(id));
155    }
156    let list = client.call(&ListCollections).await?.into_vec();
157    let matches = match_collections_by_name(q, &list);
158    match matches.len() {
159        0 => Err(anyhow!(
160            "No manual collection named '{}'. Use `romm-cli collections list`.",
161            q
162        )),
163        1 => Ok(Some(matches[0].id)),
164        _ => Err(anyhow!(
165            "Manual collection '{}' is ambiguous among {} matches; use a numeric id.",
166            q,
167            matches.len()
168        )),
169    }
170}
171
172/// Resolve smart collection by numeric id or exact name.
173pub async fn resolve_smart_collection_id(
174    client: &RommClient,
175    query: Option<&str>,
176) -> Result<Option<u64>> {
177    let Some(q) = query.map(str::trim).filter(|s| !s.is_empty()) else {
178        return Ok(None);
179    };
180    if let Ok(id) = q.parse::<u64>() {
181        return Ok(Some(id));
182    }
183    let list = client.call(&ListSmartCollections).await?.into_vec();
184    let matches = match_collections_by_name(q, &list);
185    match matches.len() {
186        0 => Err(anyhow!(
187            "No smart collection named '{}'. Use `romm-cli collections list`.",
188            q
189        )),
190        1 => Ok(Some(matches[0].id)),
191        _ => Err(anyhow!(
192            "Smart collection '{}' is ambiguous among {} matches; use a numeric id.",
193            q,
194            matches.len()
195        )),
196    }
197}