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