Skip to main content

slack_rs/api/
args.rs

1//! Argument parsing for `api call` command
2//!
3//! Parses command-line arguments into structured API call parameters:
4//! - Method name (e.g., "chat.postMessage")
5//! - Key-value pairs (e.g., "channel=C123456" "text=hello")
6//! - Flags: --json, --get
7
8use crate::profile::TokenType;
9use serde_json::{json, Value};
10use std::collections::HashMap;
11use thiserror::Error;
12
13#[derive(Debug, Error)]
14pub enum ArgsError {
15    #[error("Missing method argument")]
16    MissingMethod,
17
18    #[error("Invalid key-value pair: {0}")]
19    InvalidKeyValue(String),
20
21    #[error("Invalid JSON: {0}")]
22    InvalidJson(String),
23}
24
25pub type Result<T> = std::result::Result<T, ArgsError>;
26
27/// Parsed API call arguments
28#[derive(Debug, Clone, PartialEq)]
29pub struct ApiCallArgs {
30    /// API method name (e.g., "chat.postMessage")
31    pub method: String,
32
33    /// Request parameters
34    pub params: HashMap<String, String>,
35
36    /// Use JSON body instead of form encoding
37    pub use_json: bool,
38
39    /// Use GET method instead of POST
40    pub use_get: bool,
41
42    /// Token type preference (CLI flag override)
43    pub token_type: Option<TokenType>,
44
45    /// Output raw Slack API response without envelope
46    pub raw: bool,
47}
48
49impl ApiCallArgs {
50    /// Parse arguments from command-line args
51    pub fn parse(args: &[String]) -> Result<Self> {
52        if args.is_empty() {
53            return Err(ArgsError::MissingMethod);
54        }
55
56        let method = args[0].clone();
57        let mut params = HashMap::new();
58        let mut use_json = false;
59        let mut use_get = false;
60        let mut token_type = None;
61        let mut raw = false;
62
63        let mut i = 1;
64        while i < args.len() {
65            let arg = &args[i];
66            if arg == "--json" {
67                use_json = true;
68            } else if arg == "--get" {
69                use_get = true;
70            } else if arg == "--raw" {
71                raw = true;
72            } else if arg == "--token-type" {
73                // Space-separated format: --token-type VALUE
74                i += 1;
75                if i < args.len() {
76                    token_type = Some(
77                        args[i]
78                            .parse::<TokenType>()
79                            .map_err(|e| ArgsError::InvalidJson(e.to_string()))?,
80                    );
81                }
82            } else if arg.starts_with("--token-type=") {
83                // Equals format: --token-type=VALUE
84                if let Some(value) = arg.strip_prefix("--token-type=") {
85                    token_type = Some(
86                        value
87                            .parse::<TokenType>()
88                            .map_err(|e| ArgsError::InvalidJson(e.to_string()))?,
89                    );
90                }
91            } else if arg.starts_with("--") {
92                // Ignore unknown flags for forward compatibility
93            } else {
94                // Parse key=value
95                if let Some((key, value)) = arg.split_once('=') {
96                    params.insert(key.to_string(), value.to_string());
97                } else {
98                    return Err(ArgsError::InvalidKeyValue(arg.clone()));
99                }
100            }
101            i += 1;
102        }
103
104        Ok(Self {
105            method,
106            params,
107            use_json,
108            use_get,
109            token_type,
110            raw,
111        })
112    }
113
114    /// Convert to JSON body
115    pub fn to_json(&self) -> Value {
116        let mut map = serde_json::Map::new();
117        for (k, v) in &self.params {
118            map.insert(k.clone(), Value::String(v.clone()));
119        }
120        Value::Object(map)
121    }
122
123    /// Convert to form parameters
124    pub fn to_form(&self) -> Vec<(String, String)> {
125        self.params
126            .iter()
127            .map(|(k, v)| (k.clone(), v.clone()))
128            .collect()
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn test_parse_basic() {
138        let args = vec!["chat.postMessage".to_string()];
139        let result = ApiCallArgs::parse(&args).unwrap();
140
141        assert_eq!(result.method, "chat.postMessage");
142        assert!(result.params.is_empty());
143        assert!(!result.use_json);
144        assert!(!result.use_get);
145        assert_eq!(result.token_type, None);
146    }
147
148    #[test]
149    fn test_parse_with_params() {
150        let args = vec![
151            "chat.postMessage".to_string(),
152            "channel=C123456".to_string(),
153            "text=Hello World".to_string(),
154        ];
155        let result = ApiCallArgs::parse(&args).unwrap();
156
157        assert_eq!(result.method, "chat.postMessage");
158        assert_eq!(result.params.get("channel"), Some(&"C123456".to_string()));
159        assert_eq!(result.params.get("text"), Some(&"Hello World".to_string()));
160    }
161
162    #[test]
163    fn test_parse_with_json_flag() {
164        let args = vec![
165            "chat.postMessage".to_string(),
166            "--json".to_string(),
167            "channel=C123456".to_string(),
168        ];
169        let result = ApiCallArgs::parse(&args).unwrap();
170
171        assert_eq!(result.method, "chat.postMessage");
172        assert!(result.use_json);
173        assert!(!result.use_get);
174    }
175
176    #[test]
177    fn test_parse_with_get_flag() {
178        let args = vec![
179            "users.info".to_string(),
180            "--get".to_string(),
181            "user=U123456".to_string(),
182        ];
183        let result = ApiCallArgs::parse(&args).unwrap();
184
185        assert_eq!(result.method, "users.info");
186        assert!(!result.use_json);
187        assert!(result.use_get);
188    }
189
190    #[test]
191    fn test_parse_with_both_flags() {
192        let args = vec![
193            "chat.postMessage".to_string(),
194            "--json".to_string(),
195            "--get".to_string(),
196            "channel=C123456".to_string(),
197        ];
198        let result = ApiCallArgs::parse(&args).unwrap();
199
200        assert!(result.use_json);
201        assert!(result.use_get);
202    }
203
204    #[test]
205    fn test_parse_missing_method() {
206        let args: Vec<String> = vec![];
207        let result = ApiCallArgs::parse(&args);
208
209        assert!(result.is_err());
210        match result {
211            Err(ArgsError::MissingMethod) => {}
212            _ => panic!("Expected MissingMethod error"),
213        }
214    }
215
216    #[test]
217    fn test_parse_invalid_key_value() {
218        let args = vec!["chat.postMessage".to_string(), "invalid_arg".to_string()];
219        let result = ApiCallArgs::parse(&args);
220
221        assert!(result.is_err());
222        match result {
223            Err(ArgsError::InvalidKeyValue(arg)) => {
224                assert_eq!(arg, "invalid_arg");
225            }
226            _ => panic!("Expected InvalidKeyValue error"),
227        }
228    }
229
230    #[test]
231    fn test_to_json() {
232        let args = ApiCallArgs {
233            method: "chat.postMessage".to_string(),
234            params: [
235                ("channel".to_string(), "C123456".to_string()),
236                ("text".to_string(), "Hello".to_string()),
237            ]
238            .iter()
239            .cloned()
240            .collect(),
241            use_json: true,
242            use_get: false,
243            token_type: None,
244            raw: false,
245        };
246
247        let json = args.to_json();
248        assert_eq!(json["channel"], "C123456");
249        assert_eq!(json["text"], "Hello");
250    }
251
252    #[test]
253    fn test_to_form() {
254        let args = ApiCallArgs {
255            method: "chat.postMessage".to_string(),
256            params: [
257                ("channel".to_string(), "C123456".to_string()),
258                ("text".to_string(), "Hello".to_string()),
259            ]
260            .iter()
261            .cloned()
262            .collect(),
263            use_json: false,
264            use_get: false,
265            token_type: None,
266            raw: false,
267        };
268
269        let form = args.to_form();
270        assert_eq!(form.len(), 2);
271        assert!(form.contains(&("channel".to_string(), "C123456".to_string())));
272        assert!(form.contains(&("text".to_string(), "Hello".to_string())));
273    }
274
275    #[test]
276    fn test_parse_token_type_space_separated() {
277        let args = vec![
278            "chat.postMessage".to_string(),
279            "--token-type".to_string(),
280            "user".to_string(),
281            "channel=C123456".to_string(),
282        ];
283        let result = ApiCallArgs::parse(&args).unwrap();
284
285        assert_eq!(result.method, "chat.postMessage");
286        assert_eq!(result.token_type, Some(TokenType::User));
287    }
288
289    #[test]
290    fn test_parse_token_type_equals_format() {
291        let args = vec![
292            "chat.postMessage".to_string(),
293            "--token-type=bot".to_string(),
294            "channel=C123456".to_string(),
295        ];
296        let result = ApiCallArgs::parse(&args).unwrap();
297
298        assert_eq!(result.method, "chat.postMessage");
299        assert_eq!(result.token_type, Some(TokenType::Bot));
300    }
301
302    #[test]
303    fn test_parse_token_type_both_formats() {
304        // Test space-separated with bot
305        let args1 = vec![
306            "users.info".to_string(),
307            "--token-type".to_string(),
308            "bot".to_string(),
309        ];
310        let result1 = ApiCallArgs::parse(&args1).unwrap();
311        assert_eq!(result1.token_type, Some(TokenType::Bot));
312
313        // Test equals format with user
314        let args2 = vec!["users.info".to_string(), "--token-type=user".to_string()];
315        let result2 = ApiCallArgs::parse(&args2).unwrap();
316        assert_eq!(result2.token_type, Some(TokenType::User));
317    }
318}