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
62        // Check SLACKRS_OUTPUT environment variable for default output mode
63        // --raw flag will override this
64        let mut raw = if let Ok(output_mode) = std::env::var("SLACKRS_OUTPUT") {
65            output_mode.trim().to_lowercase() == "raw"
66        } else {
67            false
68        };
69
70        let mut i = 1;
71        while i < args.len() {
72            let arg = &args[i];
73            if arg == "--json" {
74                use_json = true;
75            } else if arg == "--get" {
76                use_get = true;
77            } else if arg == "--raw" {
78                // --raw flag always overrides environment variable
79                raw = true;
80            } else if arg == "--profile" {
81                // Skip --profile flag and its value (space-separated format)
82                i += 1; // Skip the profile value
83            } else if arg.starts_with("--profile=") {
84                // Skip --profile=VALUE format
85                // No additional increment needed
86            } else if arg == "--token-type" {
87                // Space-separated format: --token-type VALUE
88                i += 1;
89                if i < args.len() {
90                    token_type = Some(
91                        args[i]
92                            .parse::<TokenType>()
93                            .map_err(|e| ArgsError::InvalidJson(e.to_string()))?,
94                    );
95                }
96            } else if arg.starts_with("--token-type=") {
97                // Equals format: --token-type=VALUE
98                if let Some(value) = arg.strip_prefix("--token-type=") {
99                    token_type = Some(
100                        value
101                            .parse::<TokenType>()
102                            .map_err(|e| ArgsError::InvalidJson(e.to_string()))?,
103                    );
104                }
105            } else if arg.starts_with("--") {
106                // Ignore unknown flags for forward compatibility
107            } else {
108                // Parse key=value
109                if let Some((key, value)) = arg.split_once('=') {
110                    params.insert(key.to_string(), value.to_string());
111                } else {
112                    return Err(ArgsError::InvalidKeyValue(arg.clone()));
113                }
114            }
115            i += 1;
116        }
117
118        Ok(Self {
119            method,
120            params,
121            use_json,
122            use_get,
123            token_type,
124            raw,
125        })
126    }
127
128    /// Convert to JSON body
129    pub fn to_json(&self) -> Value {
130        let mut map = serde_json::Map::new();
131        for (k, v) in &self.params {
132            map.insert(k.clone(), Value::String(v.clone()));
133        }
134        Value::Object(map)
135    }
136
137    /// Convert to form parameters
138    pub fn to_form(&self) -> Vec<(String, String)> {
139        self.params
140            .iter()
141            .map(|(k, v)| (k.clone(), v.clone()))
142            .collect()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_parse_basic() {
152        let args = vec!["chat.postMessage".to_string()];
153        let result = ApiCallArgs::parse(&args).unwrap();
154
155        assert_eq!(result.method, "chat.postMessage");
156        assert!(result.params.is_empty());
157        assert!(!result.use_json);
158        assert!(!result.use_get);
159        assert_eq!(result.token_type, None);
160    }
161
162    #[test]
163    fn test_parse_with_params() {
164        let args = vec![
165            "chat.postMessage".to_string(),
166            "channel=C123456".to_string(),
167            "text=Hello World".to_string(),
168        ];
169        let result = ApiCallArgs::parse(&args).unwrap();
170
171        assert_eq!(result.method, "chat.postMessage");
172        assert_eq!(result.params.get("channel"), Some(&"C123456".to_string()));
173        assert_eq!(result.params.get("text"), Some(&"Hello World".to_string()));
174    }
175
176    #[test]
177    fn test_parse_with_json_flag() {
178        let args = vec![
179            "chat.postMessage".to_string(),
180            "--json".to_string(),
181            "channel=C123456".to_string(),
182        ];
183        let result = ApiCallArgs::parse(&args).unwrap();
184
185        assert_eq!(result.method, "chat.postMessage");
186        assert!(result.use_json);
187        assert!(!result.use_get);
188    }
189
190    #[test]
191    fn test_parse_with_get_flag() {
192        let args = vec![
193            "users.info".to_string(),
194            "--get".to_string(),
195            "user=U123456".to_string(),
196        ];
197        let result = ApiCallArgs::parse(&args).unwrap();
198
199        assert_eq!(result.method, "users.info");
200        assert!(!result.use_json);
201        assert!(result.use_get);
202    }
203
204    #[test]
205    fn test_parse_with_both_flags() {
206        let args = vec![
207            "chat.postMessage".to_string(),
208            "--json".to_string(),
209            "--get".to_string(),
210            "channel=C123456".to_string(),
211        ];
212        let result = ApiCallArgs::parse(&args).unwrap();
213
214        assert!(result.use_json);
215        assert!(result.use_get);
216    }
217
218    #[test]
219    fn test_parse_missing_method() {
220        let args: Vec<String> = vec![];
221        let result = ApiCallArgs::parse(&args);
222
223        assert!(result.is_err());
224        match result {
225            Err(ArgsError::MissingMethod) => {}
226            _ => panic!("Expected MissingMethod error"),
227        }
228    }
229
230    #[test]
231    fn test_parse_invalid_key_value() {
232        let args = vec!["chat.postMessage".to_string(), "invalid_arg".to_string()];
233        let result = ApiCallArgs::parse(&args);
234
235        assert!(result.is_err());
236        match result {
237            Err(ArgsError::InvalidKeyValue(arg)) => {
238                assert_eq!(arg, "invalid_arg");
239            }
240            _ => panic!("Expected InvalidKeyValue error"),
241        }
242    }
243
244    #[test]
245    fn test_to_json() {
246        let args = ApiCallArgs {
247            method: "chat.postMessage".to_string(),
248            params: [
249                ("channel".to_string(), "C123456".to_string()),
250                ("text".to_string(), "Hello".to_string()),
251            ]
252            .iter()
253            .cloned()
254            .collect(),
255            use_json: true,
256            use_get: false,
257            token_type: None,
258            raw: false,
259        };
260
261        let json = args.to_json();
262        assert_eq!(json["channel"], "C123456");
263        assert_eq!(json["text"], "Hello");
264    }
265
266    #[test]
267    fn test_to_form() {
268        let args = ApiCallArgs {
269            method: "chat.postMessage".to_string(),
270            params: [
271                ("channel".to_string(), "C123456".to_string()),
272                ("text".to_string(), "Hello".to_string()),
273            ]
274            .iter()
275            .cloned()
276            .collect(),
277            use_json: false,
278            use_get: false,
279            token_type: None,
280            raw: false,
281        };
282
283        let form = args.to_form();
284        assert_eq!(form.len(), 2);
285        assert!(form.contains(&("channel".to_string(), "C123456".to_string())));
286        assert!(form.contains(&("text".to_string(), "Hello".to_string())));
287    }
288
289    #[test]
290    fn test_parse_token_type_space_separated() {
291        let args = vec![
292            "chat.postMessage".to_string(),
293            "--token-type".to_string(),
294            "user".to_string(),
295            "channel=C123456".to_string(),
296        ];
297        let result = ApiCallArgs::parse(&args).unwrap();
298
299        assert_eq!(result.method, "chat.postMessage");
300        assert_eq!(result.token_type, Some(TokenType::User));
301    }
302
303    #[test]
304    fn test_parse_token_type_equals_format() {
305        let args = vec![
306            "chat.postMessage".to_string(),
307            "--token-type=bot".to_string(),
308            "channel=C123456".to_string(),
309        ];
310        let result = ApiCallArgs::parse(&args).unwrap();
311
312        assert_eq!(result.method, "chat.postMessage");
313        assert_eq!(result.token_type, Some(TokenType::Bot));
314    }
315
316    #[test]
317    fn test_parse_token_type_both_formats() {
318        // Test space-separated with bot
319        let args1 = vec![
320            "users.info".to_string(),
321            "--token-type".to_string(),
322            "bot".to_string(),
323        ];
324        let result1 = ApiCallArgs::parse(&args1).unwrap();
325        assert_eq!(result1.token_type, Some(TokenType::Bot));
326
327        // Test equals format with user
328        let args2 = vec!["users.info".to_string(), "--token-type=user".to_string()];
329        let result2 = ApiCallArgs::parse(&args2).unwrap();
330        assert_eq!(result2.token_type, Some(TokenType::User));
331    }
332}