tranc-cli 0.1.1

Tranc CLI — trade indicator queries from the command line.
//! `tranc symbols` subcommand — list available trading symbols.

use anyhow::Result;
use clap::{Parser, Subcommand, ValueEnum};

use crate::client::{build_client, get_json};
use crate::config::canonical_base_url;
use crate::output::print_json;

/// `tranc symbols` — list available trading symbols.
#[derive(Debug, Parser)]
pub struct SymbolsCmd {
    #[command(subcommand)]
    sub: SymbolsSub,
}

#[derive(Debug, Subcommand)]
enum SymbolsSub {
    /// List all available symbols, optionally filtered by asset class or query.
    List {
        /// Asset class filter.
        #[arg(long)]
        asset: Option<AssetClass>,

        /// Search query (e.g. "BTC").
        #[arg(long, short)]
        q: Option<String>,
    },
}

#[derive(Debug, Clone, Copy, ValueEnum)]
enum AssetClass {
    Crypto,
    Fx,
}

impl SymbolsCmd {
    pub async fn run(self, api_url: &str, pretty: bool) -> Result<()> {
        let base = canonical_base_url(api_url);
        match self.sub {
            SymbolsSub::List { asset, q } => run_list(&base, asset, q.as_deref(), pretty).await,
        }
    }
}

async fn run_list(
    base: &str,
    asset: Option<AssetClass>,
    q: Option<&str>,
    pretty: bool,
) -> Result<()> {
    let (client, token) = build_client()?;

    let mut params: Vec<(&str, String)> = Vec::new();
    if let Some(a) = asset {
        let s = match a {
            AssetClass::Crypto => "crypto",
            AssetClass::Fx => "fx",
        };
        params.push(("asset", s.to_string()));
    }
    if let Some(q) = q {
        params.push(("q", q.to_string()));
    }

    let url = format!("{base}/v1/symbols");
    let rb = client.get(&url).bearer_auth(&token).query(&params);
    let json = get_json(rb).await?;
    print_json(&json, pretty)
}

#[cfg(test)]
mod tests {
    use super::*;
    use clap::Parser;

    #[test]
    fn parse_symbols_list_no_filter() {
        let cmd = SymbolsCmd::try_parse_from(["symbols", "list"]).unwrap();
        assert!(matches!(
            cmd.sub,
            SymbolsSub::List {
                asset: None,
                q: None
            }
        ));
    }

    #[test]
    fn parse_symbols_list_crypto() {
        let cmd = SymbolsCmd::try_parse_from(["symbols", "list", "--asset", "crypto"]).unwrap();
        assert!(matches!(
            cmd.sub,
            SymbolsSub::List {
                asset: Some(AssetClass::Crypto),
                ..
            }
        ));
    }

    #[test]
    fn parse_symbols_list_fx_with_query() {
        let cmd =
            SymbolsCmd::try_parse_from(["symbols", "list", "--asset", "fx", "--q", "EUR"]).unwrap();
        assert!(matches!(
            cmd.sub,
            SymbolsSub::List {
                asset: Some(AssetClass::Fx),
                q: Some(_),
            }
        ));
    }
}