Skip to main content

romm_api/core/
resolve.rs

1//! Shared name/slug → ID resolution for platforms and collections.
2
3use crate::client::RommClient;
4use crate::endpoints::collections::{ListCollections, ListSmartCollections};
5use crate::endpoints::platforms::ListPlatforms;
6use crate::error::RommError;
7use crate::types::{Collection, Platform};
8
9/// Resolves a platform ID from a string query by matching against slugs, names, and custom names.
10pub fn resolve_platform_id_from_list(
11    query: &str,
12    platforms: &[Platform],
13) -> Result<u64, RommError> {
14    let normalized = query.trim().to_ascii_lowercase();
15
16    if let Some(platform) = platforms.iter().find(|p| {
17        p.slug.eq_ignore_ascii_case(&normalized) || p.fs_slug.eq_ignore_ascii_case(&normalized)
18    }) {
19        return Ok(platform.id);
20    }
21
22    let exact_name_matches: Vec<&Platform> = platforms
23        .iter()
24        .filter(|p| {
25            p.name.eq_ignore_ascii_case(&normalized)
26                || p.display_name
27                    .as_deref()
28                    .is_some_and(|name| name.eq_ignore_ascii_case(&normalized))
29                || p.custom_name
30                    .as_deref()
31                    .is_some_and(|name| name.eq_ignore_ascii_case(&normalized))
32        })
33        .collect();
34
35    match exact_name_matches.len() {
36        1 => Ok(exact_name_matches[0].id),
37        0 => Err(RommError::Other(format!(
38            "No platform found for '{query}'. Use 'romm-cli platforms list' to inspect available values."
39        ))),
40        _ => {
41            let names = exact_name_matches
42                .iter()
43                .map(|p| format!("{} ({})", p.name, p.id))
44                .collect::<Vec<_>>()
45                .join(", ");
46            Err(RommError::Other(format!(
47                "Platform '{query}' is ambiguous. Matches: {names}. Please use a more specific --platform value."
48            )))
49        }
50    }
51}
52
53/// Resolves a platform query (slug or name) to a numeric ID.
54pub async fn resolve_platform_id(
55    client: &RommClient,
56    platform_query: Option<&str>,
57) -> Result<Option<u64>, RommError> {
58    let Some(query) = platform_query.map(str::trim).filter(|q| !q.is_empty()) else {
59        return Ok(None);
60    };
61    let platforms = client.call(&ListPlatforms).await?;
62    resolve_platform_id_from_list(query, &platforms).map(Some)
63}
64
65/// Resolves multiple platform queries to a list of unique numeric IDs.
66pub async fn resolve_platform_ids(
67    client: &RommClient,
68    names: &[String],
69) -> Result<Vec<u64>, RommError> {
70    if names.is_empty() {
71        return Ok(Vec::new());
72    }
73    let platforms = client.call(&ListPlatforms).await?;
74    let mut out = Vec::new();
75    for n in names {
76        let id = resolve_platform_id_from_list(n.trim(), &platforms)?;
77        if !out.contains(&id) {
78            out.push(id);
79        }
80    }
81    Ok(out)
82}
83
84fn match_collections_by_name<'a>(q: &str, collections: &'a [Collection]) -> Vec<&'a Collection> {
85    let n = q.trim().to_ascii_lowercase();
86    collections
87        .iter()
88        .filter(|c| c.name.eq_ignore_ascii_case(&n))
89        .collect()
90}
91
92/// Resolves a manual collection by ID or exact name.
93pub async fn resolve_manual_collection_id(
94    client: &RommClient,
95    query: Option<&str>,
96) -> Result<Option<u64>, RommError> {
97    let Some(q) = query.map(str::trim).filter(|s| !s.is_empty()) else {
98        return Ok(None);
99    };
100    if let Ok(id) = q.parse::<u64>() {
101        return Ok(Some(id));
102    }
103    let list = client.call(&ListCollections).await?.into_vec();
104    let matches = match_collections_by_name(q, &list);
105    match matches.len() {
106        0 => Err(RommError::Other(format!(
107            "No manual collection named '{q}'. Use `romm-cli collections list`."
108        ))),
109        1 => Ok(Some(matches[0].id)),
110        _ => Err(RommError::Other(format!(
111            "Manual collection '{q}' is ambiguous among {} matches; use a numeric id.",
112            matches.len()
113        ))),
114    }
115}
116
117/// Resolves a smart collection by ID or exact name.
118pub async fn resolve_smart_collection_id(
119    client: &RommClient,
120    query: Option<&str>,
121) -> Result<Option<u64>, RommError> {
122    let Some(q) = query.map(str::trim).filter(|s| !s.is_empty()) else {
123        return Ok(None);
124    };
125    if let Ok(id) = q.parse::<u64>() {
126        return Ok(Some(id));
127    }
128    let list = client.call(&ListSmartCollections).await?.into_vec();
129    let matches = match_collections_by_name(q, &list);
130    match matches.len() {
131        0 => Err(RommError::Other(format!(
132            "No smart collection named '{q}'. Use `romm-cli collections list`."
133        ))),
134        1 => Ok(Some(matches[0].id)),
135        _ => Err(RommError::Other(format!(
136            "Smart collection '{q}' is ambiguous among {} matches; use a numeric id.",
137            matches.len()
138        ))),
139    }
140}