Skip to main content

romm_cli/commands/
api.rs

1use anyhow::{anyhow, Result};
2use clap::{Args, Subcommand};
3
4use crate::cli_presentation::CliPresentation;
5use crate::commands::OutputFormat;
6use romm_api::client::RommClient;
7
8/// Low-level escape hatch for calling arbitrary ROMM API endpoints.
9#[derive(Args, Debug)]
10#[command(after_help = "Examples:\n  \
11      romm-cli api call GET /api/platforms\n  \
12      romm-cli --json api call GET /api/roms --query limit=10")]
13pub struct ApiCommand {
14    #[command(subcommand)]
15    pub action: Option<ApiAction>,
16
17    /// HTTP method (legacy, use 'api call `<method>` `<path>`')
18    pub method: Option<String>,
19
20    /// API path (legacy, use 'api call `<method>` `<path>`')
21    pub path: Option<String>,
22
23    /// Query parameters as key=value, repeatable
24    #[arg(long = "query", global = true)]
25    pub query: Vec<String>,
26
27    /// JSON request body as a string
28    #[arg(long, global = true)]
29    pub data: Option<String>,
30}
31
32#[derive(Subcommand, Debug)]
33pub enum ApiAction {
34    /// Make a generic API call
35    Call {
36        /// HTTP method (GET, POST, etc.)
37        method: String,
38        /// API path (e.g. /api/roms)
39        path: String,
40    },
41    /// Shortcut for GET request
42    Get {
43        /// API path
44        path: String,
45    },
46    /// Shortcut for POST request
47    Post {
48        /// API path
49        path: String,
50    },
51}
52
53pub async fn handle(
54    cmd: ApiCommand,
55    client: &RommClient,
56    presentation: CliPresentation,
57) -> Result<()> {
58    let format = presentation.format;
59    let (method, path) = match cmd.action {
60        Some(ApiAction::Call { method, path }) => (method, path),
61        Some(ApiAction::Get { path }) => ("GET".to_string(), path),
62        Some(ApiAction::Post { path }) => ("POST".to_string(), path),
63        None => {
64            let m = cmd
65                .method
66                .ok_or_else(|| anyhow!("Method is required (e.g. 'api call GET /api/roms')"))?;
67            let p = cmd
68                .path
69                .ok_or_else(|| anyhow!("Path is required (e.g. 'api call GET /api/roms')"))?;
70            (m, p)
71        }
72    };
73
74    let mut query_pairs = Vec::new();
75    for q in &cmd.query {
76        if let Some((k, v)) = q.split_once('=') {
77            query_pairs.push((k.to_string(), v.to_string()));
78        } else {
79            eprintln!(
80                "warning: ignoring malformed --query value {:?}; expected key=value",
81                q
82            );
83        }
84    }
85
86    let body = if let Some(data) = &cmd.data {
87        Some(serde_json::from_str(data)?)
88    } else {
89        None
90    };
91
92    let value = client
93        .request_json(&method, &path, &query_pairs, body)
94        .await?;
95
96    match format {
97        OutputFormat::Json => {
98            println!("{}", serde_json::to_string_pretty(&value)?);
99        }
100        OutputFormat::Text => {
101            println!("{}", serde_json::to_string_pretty(&value)?);
102        }
103    }
104    Ok(())
105}