Skip to main content

slack_rs/api/
call.rs

1//! API call handler with metadata attachment
2//!
3//! Executes API calls and enriches responses with execution context:
4//! - Profile name
5//! - Team ID
6//! - User ID
7//! - Method name
8
9use super::args::ApiCallArgs;
10use super::client::{ApiClient, RequestBody};
11use super::guidance::format_error_guidance;
12use reqwest::Method;
13use serde::{Deserialize, Serialize};
14use serde_json::{json, Value};
15use thiserror::Error;
16
17#[derive(Debug, Error)]
18pub enum ApiCallError {
19    #[error("Client error: {0}")]
20    ClientError(#[from] super::client::ApiClientError),
21
22    #[error("Failed to parse response: {0}")]
23    ParseError(String),
24}
25
26pub type Result<T> = std::result::Result<T, ApiCallError>;
27
28/// Execution context for API calls
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ApiCallContext {
31    pub profile_name: Option<String>,
32    pub team_id: String,
33    pub user_id: String,
34}
35
36/// API call response with metadata
37#[derive(Debug, Serialize, Deserialize)]
38pub struct ApiCallResponse {
39    /// Original API response
40    pub response: Value,
41
42    /// Execution metadata
43    pub meta: ApiCallMeta,
44}
45
46/// Execution metadata
47#[derive(Debug, Serialize, Deserialize)]
48pub struct ApiCallMeta {
49    pub profile_name: Option<String>,
50    pub team_id: String,
51    pub user_id: String,
52    pub method: String,
53    pub command: String,
54    pub token_type: String,
55}
56
57/// Execute an API call with the given arguments, context, token type, and command name
58pub async fn execute_api_call(
59    client: &ApiClient,
60    args: &ApiCallArgs,
61    token: &str,
62    context: &ApiCallContext,
63    token_type: &str,
64    command: &str,
65) -> Result<ApiCallResponse> {
66    // Determine HTTP method
67    let method = if args.use_get {
68        Method::GET
69    } else {
70        Method::POST
71    };
72
73    // Prepare request body
74    let body = if args.use_json {
75        RequestBody::Json(args.to_json())
76    } else if method == Method::POST {
77        RequestBody::Form(args.to_form())
78    } else {
79        RequestBody::None
80    };
81
82    // Make the API call
83    let response = client.call(method, &args.method, token, body).await?;
84
85    // Parse response body
86    let response_text = response
87        .text()
88        .await
89        .map_err(|e| ApiCallError::ParseError(e.to_string()))?;
90
91    let response_json: Value = serde_json::from_str(&response_text)
92        .map_err(|e| ApiCallError::ParseError(e.to_string()))?;
93
94    // Construct response with metadata
95    let api_response = ApiCallResponse {
96        response: response_json,
97        meta: ApiCallMeta {
98            profile_name: context.profile_name.clone(),
99            team_id: context.team_id.clone(),
100            user_id: context.user_id.clone(),
101            method: args.method.clone(),
102            command: command.to_string(),
103            token_type: token_type.to_string(),
104        },
105    };
106
107    Ok(api_response)
108}
109
110/// Display error guidance to stderr if the response contains a known error
111pub fn display_error_guidance(response: &ApiCallResponse) {
112    // Check if response has an error
113    if let Some(ok) = response.response.get("ok").and_then(|v| v.as_bool()) {
114        if !ok {
115            // Try to get error code from response
116            if let Some(error_code) = response.response.get("error").and_then(|v| v.as_str()) {
117                // Display guidance if available
118                if let Some(guidance) = format_error_guidance(error_code) {
119                    eprintln!("{}", guidance);
120                }
121            }
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use std::collections::HashMap;
130
131    #[test]
132    fn test_api_call_meta_serialization() {
133        let meta = ApiCallMeta {
134            profile_name: Some("default".to_string()),
135            team_id: "T123ABC".to_string(),
136            user_id: "U456DEF".to_string(),
137            method: "chat.postMessage".to_string(),
138            command: "api call".to_string(),
139            token_type: "bot".to_string(),
140        };
141
142        let json = serde_json::to_string(&meta).unwrap();
143        let deserialized: ApiCallMeta = serde_json::from_str(&json).unwrap();
144
145        assert_eq!(deserialized.profile_name, Some("default".to_string()));
146        assert_eq!(deserialized.team_id, "T123ABC");
147        assert_eq!(deserialized.user_id, "U456DEF");
148        assert_eq!(deserialized.method, "chat.postMessage");
149        assert_eq!(deserialized.command, "api call");
150        assert_eq!(deserialized.token_type, "bot");
151    }
152
153    #[test]
154    fn test_api_call_response_structure() {
155        let response = ApiCallResponse {
156            response: json!({
157                "ok": true,
158                "channel": "C123456",
159                "ts": "1234567890.123456"
160            }),
161            meta: ApiCallMeta {
162                profile_name: Some("work".to_string()),
163                team_id: "T123ABC".to_string(),
164                user_id: "U456DEF".to_string(),
165                method: "chat.postMessage".to_string(),
166                command: "api call".to_string(),
167                token_type: "bot".to_string(),
168            },
169        };
170
171        let json = serde_json::to_value(&response).unwrap();
172
173        assert!(json["response"]["ok"].as_bool().unwrap());
174        assert_eq!(json["meta"]["team_id"], "T123ABC");
175        assert_eq!(json["meta"]["method"], "chat.postMessage");
176        assert_eq!(json["meta"]["command"], "api call");
177        assert_eq!(json["meta"]["token_type"], "bot");
178    }
179
180    #[test]
181    fn test_display_error_guidance_with_known_error() {
182        // Create response with known error code
183        let response = ApiCallResponse {
184            response: json!({
185                "ok": false,
186                "error": "missing_scope"
187            }),
188            meta: ApiCallMeta {
189                profile_name: Some("default".to_string()),
190                team_id: "T123ABC".to_string(),
191                user_id: "U456DEF".to_string(),
192                method: "chat.postMessage".to_string(),
193                command: "api call".to_string(),
194                token_type: "bot".to_string(),
195            },
196        };
197
198        // This should not panic - guidance should be displayed to stderr
199        display_error_guidance(&response);
200    }
201
202    #[test]
203    fn test_display_error_guidance_with_unknown_error() {
204        // Create response with unknown error code
205        let response = ApiCallResponse {
206            response: json!({
207                "ok": false,
208                "error": "unknown_error_code"
209            }),
210            meta: ApiCallMeta {
211                profile_name: Some("default".to_string()),
212                team_id: "T123ABC".to_string(),
213                user_id: "U456DEF".to_string(),
214                method: "chat.postMessage".to_string(),
215                command: "api call".to_string(),
216                token_type: "bot".to_string(),
217            },
218        };
219
220        // This should not panic - no guidance for unknown errors
221        display_error_guidance(&response);
222    }
223
224    #[test]
225    fn test_display_error_guidance_with_success() {
226        // Create successful response
227        let response = ApiCallResponse {
228            response: json!({
229                "ok": true,
230                "channel": "C123456"
231            }),
232            meta: ApiCallMeta {
233                profile_name: Some("default".to_string()),
234                team_id: "T123ABC".to_string(),
235                user_id: "U456DEF".to_string(),
236                method: "chat.postMessage".to_string(),
237                command: "api call".to_string(),
238                token_type: "bot".to_string(),
239            },
240        };
241
242        // This should not display anything (success case)
243        display_error_guidance(&response);
244    }
245
246    #[test]
247    fn test_display_error_guidance_with_not_allowed_token_type() {
248        // Create response with not_allowed_token_type error
249        let response = ApiCallResponse {
250            response: json!({
251                "ok": false,
252                "error": "not_allowed_token_type"
253            }),
254            meta: ApiCallMeta {
255                profile_name: Some("default".to_string()),
256                team_id: "T123ABC".to_string(),
257                user_id: "U456DEF".to_string(),
258                method: "conversations.history".to_string(),
259                command: "api call".to_string(),
260                token_type: "bot".to_string(),
261            },
262        };
263
264        // This should display guidance to stderr
265        display_error_guidance(&response);
266    }
267
268    #[test]
269    fn test_display_error_guidance_with_invalid_auth() {
270        // Create response with invalid_auth error
271        let response = ApiCallResponse {
272            response: json!({
273                "ok": false,
274                "error": "invalid_auth"
275            }),
276            meta: ApiCallMeta {
277                profile_name: Some("default".to_string()),
278                team_id: "T123ABC".to_string(),
279                user_id: "U456DEF".to_string(),
280                method: "auth.test".to_string(),
281                command: "api call".to_string(),
282                token_type: "bot".to_string(),
283            },
284        };
285
286        // This should display guidance to stderr
287        display_error_guidance(&response);
288    }
289}