Skip to main content

indodax_cli/mcp/
service.rs

1use std::fmt;
2use std::str::FromStr;
3
4/// Service groups that can be enabled in the MCP server.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ServiceGroup {
7    Market,
8    Account,
9    Trade,
10    Funding,
11    Paper,
12    Auth,
13    Alert,
14}
15
16impl ServiceGroup {
17    /// All known service groups.
18    pub fn all() -> Vec<ServiceGroup> {
19        vec![
20            ServiceGroup::Market,
21            ServiceGroup::Account,
22            ServiceGroup::Trade,
23            ServiceGroup::Funding,
24            ServiceGroup::Paper,
25            ServiceGroup::Auth,
26            ServiceGroup::Alert,
27        ]
28    }
29
30    /// Default service groups (safe operations, no auth required for some).
31    pub fn default_groups() -> Vec<ServiceGroup> {
32        vec![ServiceGroup::Market, ServiceGroup::Account, ServiceGroup::Paper, ServiceGroup::Alert]
33    }
34
35    /// Whether this group contains dangerous operations (trade, funding).
36    pub fn is_dangerous(&self) -> bool {
37        matches!(self, ServiceGroup::Trade | ServiceGroup::Funding)
38    }
39
40    /// Parse a comma-separated list of service group names.
41    /// Valid names: market, account, trade, funding, paper, auth
42    /// "all" expands to all groups.
43    pub fn parse(s: &str) -> Result<Vec<ServiceGroup>, String> {
44        let trimmed = s.trim();
45        if trimmed.eq_ignore_ascii_case("all") {
46            return Ok(Self::all());
47        }
48
49        let mut groups = Vec::new();
50        for part in trimmed.split(',') {
51            let part = part.trim();
52            if part.is_empty() {
53                continue;
54            }
55            match part.to_ascii_lowercase().as_str() {
56                "market" => groups.push(ServiceGroup::Market),
57                "account" => groups.push(ServiceGroup::Account),
58                "trade" => groups.push(ServiceGroup::Trade),
59                "funding" => groups.push(ServiceGroup::Funding),
60                "paper" => groups.push(ServiceGroup::Paper),
61                "auth" => groups.push(ServiceGroup::Auth),
62                "alert" => groups.push(ServiceGroup::Alert),
63                _ => return Err(format!("Unknown service group: '{}'", part)),
64            }
65        }
66
67        if groups.is_empty() {
68            return Err("No service groups specified".into());
69        }
70
71        Ok(groups)
72    }
73}
74
75impl fmt::Display for ServiceGroup {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        match self {
78            ServiceGroup::Market => write!(f, "market"),
79            ServiceGroup::Account => write!(f, "account"),
80            ServiceGroup::Trade => write!(f, "trade"),
81            ServiceGroup::Funding => write!(f, "funding"),
82            ServiceGroup::Paper => write!(f, "paper"),
83            ServiceGroup::Auth => write!(f, "auth"),
84            ServiceGroup::Alert => write!(f, "alert"),
85        }
86    }
87}
88
89impl FromStr for ServiceGroup {
90    type Err = String;
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        match s.to_ascii_lowercase().as_str() {
93            "market" => Ok(ServiceGroup::Market),
94            "account" => Ok(ServiceGroup::Account),
95            "trade" => Ok(ServiceGroup::Trade),
96            "funding" => Ok(ServiceGroup::Funding),
97            "paper" => Ok(ServiceGroup::Paper),
98            "auth" => Ok(ServiceGroup::Auth),
99            "alert" => Ok(ServiceGroup::Alert),
100            _ => Err(format!("Unknown service group: '{}'", s)),
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_parse_empty() {
111        let result = ServiceGroup::parse("");
112        assert!(result.is_err());
113    }
114
115    #[test]
116    fn test_parse_single() {
117        let groups = ServiceGroup::parse("market").unwrap();
118        assert_eq!(groups, vec![ServiceGroup::Market]);
119    }
120
121    #[test]
122    fn test_parse_multiple() {
123        let groups = ServiceGroup::parse("market,trade,paper").unwrap();
124        assert_eq!(
125            groups,
126            vec![ServiceGroup::Market, ServiceGroup::Trade, ServiceGroup::Paper]
127        );
128    }
129
130    #[test]
131    fn test_parse_all() {
132        let groups = ServiceGroup::parse("all").unwrap();
133        assert_eq!(groups.len(), 7);
134        assert!(groups.contains(&ServiceGroup::Market));
135        assert!(groups.contains(&ServiceGroup::Funding));
136        assert!(groups.contains(&ServiceGroup::Alert));
137    }
138
139    #[test]
140    fn test_parse_case_insensitive() {
141        let groups = ServiceGroup::parse("Market,TRADE").unwrap();
142        assert_eq!(groups, vec![ServiceGroup::Market, ServiceGroup::Trade]);
143    }
144
145    #[test]
146    fn test_parse_unknown_group() {
147        let result = ServiceGroup::parse("market,unknown");
148        assert!(result.is_err());
149        assert!(result.unwrap_err().contains("unknown"));
150    }
151
152    #[test]
153    fn test_parse_with_spaces() {
154        let groups = ServiceGroup::parse(" market , paper ").unwrap();
155        assert_eq!(groups, vec![ServiceGroup::Market, ServiceGroup::Paper]);
156    }
157
158    #[test]
159    fn test_default_groups() {
160        let groups = ServiceGroup::default_groups();
161        assert_eq!(groups.len(), 4);
162        assert!(groups.contains(&ServiceGroup::Market));
163        assert!(groups.contains(&ServiceGroup::Account));
164        assert!(groups.contains(&ServiceGroup::Paper));
165        assert!(groups.contains(&ServiceGroup::Alert));
166    }
167
168    #[test]
169    fn test_is_dangerous() {
170        assert!(!ServiceGroup::Market.is_dangerous());
171        assert!(!ServiceGroup::Account.is_dangerous());
172        assert!(ServiceGroup::Trade.is_dangerous());
173        assert!(ServiceGroup::Funding.is_dangerous());
174        assert!(!ServiceGroup::Paper.is_dangerous());
175        assert!(!ServiceGroup::Auth.is_dangerous());
176    }
177
178    #[test]
179    fn test_display() {
180        assert_eq!(ServiceGroup::Market.to_string(), "market");
181        assert_eq!(ServiceGroup::Trade.to_string(), "trade");
182    }
183
184    #[test]
185    fn test_from_str() {
186        assert_eq!("market".parse::<ServiceGroup>().unwrap(), ServiceGroup::Market);
187        assert_eq!("TRADE".parse::<ServiceGroup>().unwrap(), ServiceGroup::Trade);
188        assert!("invalid".parse::<ServiceGroup>().is_err());
189    }
190
191    #[test]
192    fn test_all_contains_all() {
193        let all = ServiceGroup::all();
194        assert_eq!(all.len(), 7);
195        for group in &[ServiceGroup::Market, ServiceGroup::Account, ServiceGroup::Trade,
196                       ServiceGroup::Funding, ServiceGroup::Paper, ServiceGroup::Auth,
197                       ServiceGroup::Alert] {
198            assert!(all.contains(group));
199        }
200    }
201}