1use 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#[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#[derive(Debug, Serialize, Deserialize)]
38pub struct ApiCallResponse {
39 pub response: Value,
41
42 pub meta: ApiCallMeta,
44}
45
46#[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
57pub 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 let method = if args.use_get {
68 Method::GET
69 } else {
70 Method::POST
71 };
72
73 let (body, query_params) = if method == Method::GET {
75 (RequestBody::None, args.to_form())
77 } else if args.use_json {
78 (RequestBody::Json(args.to_json()), vec![])
80 } else {
81 (RequestBody::Form(args.to_form()), vec![])
83 };
84
85 let response = client
87 .call(method, &args.method, token, body, query_params)
88 .await?;
89
90 let response_text = response
92 .text()
93 .await
94 .map_err(|e| ApiCallError::ParseError(e.to_string()))?;
95
96 let response_json: Value = serde_json::from_str(&response_text)
97 .map_err(|e| ApiCallError::ParseError(e.to_string()))?;
98
99 let api_response = ApiCallResponse {
101 response: response_json,
102 meta: ApiCallMeta {
103 profile_name: context.profile_name.clone(),
104 team_id: context.team_id.clone(),
105 user_id: context.user_id.clone(),
106 method: args.method.clone(),
107 command: command.to_string(),
108 token_type: token_type.to_string(),
109 },
110 };
111
112 Ok(api_response)
113}
114
115pub fn build_error_guidance(response: &ApiCallResponse) -> Option<String> {
127 if let Some(ok) = response.response.get("ok").and_then(|v| v.as_bool()) {
129 if !ok {
130 if let Some(error_code) = response.response.get("error").and_then(|v| v.as_str()) {
132 return format_error_guidance(error_code);
134 }
135 }
136 }
137 None
138}
139
140pub fn display_error_guidance(response: &ApiCallResponse) {
142 if let Some(guidance) = build_error_guidance(response) {
143 eprintln!("{}", guidance);
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use std::collections::HashMap;
151
152 #[test]
153 fn test_api_call_meta_serialization() {
154 let meta = ApiCallMeta {
155 profile_name: Some("default".to_string()),
156 team_id: "T123ABC".to_string(),
157 user_id: "U456DEF".to_string(),
158 method: "chat.postMessage".to_string(),
159 command: "api call".to_string(),
160 token_type: "bot".to_string(),
161 };
162
163 let json = serde_json::to_string(&meta).unwrap();
164 let deserialized: ApiCallMeta = serde_json::from_str(&json).unwrap();
165
166 assert_eq!(deserialized.profile_name, Some("default".to_string()));
167 assert_eq!(deserialized.team_id, "T123ABC");
168 assert_eq!(deserialized.user_id, "U456DEF");
169 assert_eq!(deserialized.method, "chat.postMessage");
170 assert_eq!(deserialized.command, "api call");
171 assert_eq!(deserialized.token_type, "bot");
172 }
173
174 #[test]
175 fn test_api_call_response_structure() {
176 let response = ApiCallResponse {
177 response: json!({
178 "ok": true,
179 "channel": "C123456",
180 "ts": "1234567890.123456"
181 }),
182 meta: ApiCallMeta {
183 profile_name: Some("work".to_string()),
184 team_id: "T123ABC".to_string(),
185 user_id: "U456DEF".to_string(),
186 method: "chat.postMessage".to_string(),
187 command: "api call".to_string(),
188 token_type: "bot".to_string(),
189 },
190 };
191
192 let json = serde_json::to_value(&response).unwrap();
193
194 assert!(json["response"]["ok"].as_bool().unwrap());
195 assert_eq!(json["meta"]["team_id"], "T123ABC");
196 assert_eq!(json["meta"]["method"], "chat.postMessage");
197 assert_eq!(json["meta"]["command"], "api call");
198 assert_eq!(json["meta"]["token_type"], "bot");
199 }
200
201 #[test]
202 fn test_display_error_guidance_with_known_error() {
203 let response = ApiCallResponse {
205 response: json!({
206 "ok": false,
207 "error": "missing_scope"
208 }),
209 meta: ApiCallMeta {
210 profile_name: Some("default".to_string()),
211 team_id: "T123ABC".to_string(),
212 user_id: "U456DEF".to_string(),
213 method: "chat.postMessage".to_string(),
214 command: "api call".to_string(),
215 token_type: "bot".to_string(),
216 },
217 };
218
219 display_error_guidance(&response);
221 }
222
223 #[test]
224 fn test_display_error_guidance_with_unknown_error() {
225 let response = ApiCallResponse {
227 response: json!({
228 "ok": false,
229 "error": "unknown_error_code"
230 }),
231 meta: ApiCallMeta {
232 profile_name: Some("default".to_string()),
233 team_id: "T123ABC".to_string(),
234 user_id: "U456DEF".to_string(),
235 method: "chat.postMessage".to_string(),
236 command: "api call".to_string(),
237 token_type: "bot".to_string(),
238 },
239 };
240
241 display_error_guidance(&response);
243 }
244
245 #[test]
246 fn test_display_error_guidance_with_success() {
247 let response = ApiCallResponse {
249 response: json!({
250 "ok": true,
251 "channel": "C123456"
252 }),
253 meta: ApiCallMeta {
254 profile_name: Some("default".to_string()),
255 team_id: "T123ABC".to_string(),
256 user_id: "U456DEF".to_string(),
257 method: "chat.postMessage".to_string(),
258 command: "api call".to_string(),
259 token_type: "bot".to_string(),
260 },
261 };
262
263 display_error_guidance(&response);
265 }
266
267 #[test]
268 fn test_display_error_guidance_with_not_allowed_token_type() {
269 let response = ApiCallResponse {
271 response: json!({
272 "ok": false,
273 "error": "not_allowed_token_type"
274 }),
275 meta: ApiCallMeta {
276 profile_name: Some("default".to_string()),
277 team_id: "T123ABC".to_string(),
278 user_id: "U456DEF".to_string(),
279 method: "conversations.history".to_string(),
280 command: "api call".to_string(),
281 token_type: "bot".to_string(),
282 },
283 };
284
285 display_error_guidance(&response);
287 }
288
289 #[test]
290 fn test_display_error_guidance_with_invalid_auth() {
291 let response = ApiCallResponse {
293 response: json!({
294 "ok": false,
295 "error": "invalid_auth"
296 }),
297 meta: ApiCallMeta {
298 profile_name: Some("default".to_string()),
299 team_id: "T123ABC".to_string(),
300 user_id: "U456DEF".to_string(),
301 method: "auth.test".to_string(),
302 command: "api call".to_string(),
303 token_type: "bot".to_string(),
304 },
305 };
306
307 display_error_guidance(&response);
309 }
310
311 #[test]
314 fn test_build_error_guidance_missing_scope() {
315 let response = ApiCallResponse {
316 response: json!({
317 "ok": false,
318 "error": "missing_scope"
319 }),
320 meta: ApiCallMeta {
321 profile_name: Some("default".to_string()),
322 team_id: "T123ABC".to_string(),
323 user_id: "U456DEF".to_string(),
324 method: "chat.postMessage".to_string(),
325 command: "api call".to_string(),
326 token_type: "bot".to_string(),
327 },
328 };
329
330 let guidance = build_error_guidance(&response);
331 assert!(guidance.is_some());
332 let guidance = guidance.unwrap();
333 assert!(guidance.contains("Error:"));
334 assert!(guidance.contains("Cause:"));
335 assert!(guidance.contains("Resolution:"));
336 assert!(guidance.contains("missing_scope"));
337 }
338
339 #[test]
340 fn test_build_error_guidance_not_allowed_token_type() {
341 let response = ApiCallResponse {
342 response: json!({
343 "ok": false,
344 "error": "not_allowed_token_type"
345 }),
346 meta: ApiCallMeta {
347 profile_name: Some("default".to_string()),
348 team_id: "T123ABC".to_string(),
349 user_id: "U456DEF".to_string(),
350 method: "conversations.history".to_string(),
351 command: "api call".to_string(),
352 token_type: "bot".to_string(),
353 },
354 };
355
356 let guidance = build_error_guidance(&response);
357 assert!(guidance.is_some());
358 let guidance = guidance.unwrap();
359 assert!(guidance.contains("Error:"));
360 assert!(guidance.contains("Cause:"));
361 assert!(guidance.contains("Resolution:"));
362 assert!(guidance.contains("not_allowed_token_type"));
363 }
364
365 #[test]
366 fn test_build_error_guidance_invalid_auth() {
367 let response = ApiCallResponse {
368 response: json!({
369 "ok": false,
370 "error": "invalid_auth"
371 }),
372 meta: ApiCallMeta {
373 profile_name: Some("default".to_string()),
374 team_id: "T123ABC".to_string(),
375 user_id: "U456DEF".to_string(),
376 method: "auth.test".to_string(),
377 command: "api call".to_string(),
378 token_type: "bot".to_string(),
379 },
380 };
381
382 let guidance = build_error_guidance(&response);
383 assert!(guidance.is_some());
384 let guidance = guidance.unwrap();
385 assert!(guidance.contains("Error:"));
386 assert!(guidance.contains("Cause:"));
387 assert!(guidance.contains("Resolution:"));
388 assert!(guidance.contains("invalid_auth"));
389 }
390
391 #[test]
392 fn test_build_error_guidance_unknown_error() {
393 let response = ApiCallResponse {
394 response: json!({
395 "ok": false,
396 "error": "unknown_error_code"
397 }),
398 meta: ApiCallMeta {
399 profile_name: Some("default".to_string()),
400 team_id: "T123ABC".to_string(),
401 user_id: "U456DEF".to_string(),
402 method: "chat.postMessage".to_string(),
403 command: "api call".to_string(),
404 token_type: "bot".to_string(),
405 },
406 };
407
408 let guidance = build_error_guidance(&response);
409 assert!(guidance.is_none());
410 }
411
412 #[test]
413 fn test_build_error_guidance_success_response() {
414 let response = ApiCallResponse {
415 response: json!({
416 "ok": true,
417 "channel": "C123456",
418 "ts": "1234567890.123456"
419 }),
420 meta: ApiCallMeta {
421 profile_name: Some("default".to_string()),
422 team_id: "T123ABC".to_string(),
423 user_id: "U456DEF".to_string(),
424 method: "chat.postMessage".to_string(),
425 command: "api call".to_string(),
426 token_type: "bot".to_string(),
427 },
428 };
429
430 let guidance = build_error_guidance(&response);
431 assert!(guidance.is_none());
432 }
433
434 #[test]
435 fn test_build_error_guidance_token_revoked() {
436 let response = ApiCallResponse {
437 response: json!({
438 "ok": false,
439 "error": "token_revoked"
440 }),
441 meta: ApiCallMeta {
442 profile_name: Some("default".to_string()),
443 team_id: "T123ABC".to_string(),
444 user_id: "U456DEF".to_string(),
445 method: "auth.test".to_string(),
446 command: "api call".to_string(),
447 token_type: "bot".to_string(),
448 },
449 };
450
451 let guidance = build_error_guidance(&response);
452 assert!(guidance.is_some());
453 let guidance = guidance.unwrap();
454 assert!(guidance.contains("Error:"));
455 assert!(guidance.contains("Cause:"));
456 assert!(guidance.contains("Resolution:"));
457 assert!(guidance.contains("token_revoked"));
458 }
459
460 #[test]
461 fn test_build_error_guidance_no_error_field() {
462 let response = ApiCallResponse {
463 response: json!({
464 "ok": false
465 }),
466 meta: ApiCallMeta {
467 profile_name: Some("default".to_string()),
468 team_id: "T123ABC".to_string(),
469 user_id: "U456DEF".to_string(),
470 method: "chat.postMessage".to_string(),
471 command: "api call".to_string(),
472 token_type: "bot".to_string(),
473 },
474 };
475
476 let guidance = build_error_guidance(&response);
477 assert!(guidance.is_none());
478 }
479
480 #[test]
481 fn test_build_error_guidance_no_ok_field() {
482 let response = ApiCallResponse {
483 response: json!({
484 "error": "missing_scope"
485 }),
486 meta: ApiCallMeta {
487 profile_name: Some("default".to_string()),
488 team_id: "T123ABC".to_string(),
489 user_id: "U456DEF".to_string(),
490 method: "chat.postMessage".to_string(),
491 command: "api call".to_string(),
492 token_type: "bot".to_string(),
493 },
494 };
495
496 let guidance = build_error_guidance(&response);
497 assert!(guidance.is_none());
498 }
499}