Skip to main content

composio_sdk/
client.rs

1//! HTTP client for Composio API
2//!
3//! This module provides the main HTTP client for interacting with the Composio API.
4//! It uses the builder pattern for flexible configuration and includes automatic
5//! retry logic for transient failures.
6//!
7//! # Example
8//!
9//! ```no_run
10//! use composio_sdk::client::ComposioClient;
11//! use std::time::Duration;
12//!
13//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
14//! let client = ComposioClient::builder()
15//!     .api_key("your_api_key")
16//!     .timeout(Duration::from_secs(60))
17//!     .max_retries(5)
18//!     .build()?;
19//! # Ok(())
20//! # }
21//! ```
22
23use crate::config::ComposioConfig;
24use crate::error::ComposioError;
25use crate::retry::RetryPolicy;
26use serde::Deserialize;
27use std::time::Duration;
28
29/// Main client for interacting with Composio API
30///
31/// The client manages HTTP connections and configuration for all API requests.
32/// It includes automatic retry logic for transient failures and proper error handling.
33///
34/// # Example
35///
36/// ```no_run
37/// use composio_sdk::client::ComposioClient;
38///
39/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
40/// let client = ComposioClient::builder()
41///     .api_key("your_api_key")
42///     .build()?;
43/// # Ok(())
44/// # }
45/// ```
46#[derive(Debug, Clone)]
47pub struct ComposioClient {
48    http_client: reqwest::Client,
49    config: ComposioConfig,
50}
51
52/// Builder for ComposioClient
53///
54/// Provides a fluent API for configuring the Composio client with custom settings.
55/// All configuration options are optional and will use sensible defaults if not specified.
56///
57/// # Example
58///
59/// ```no_run
60/// use composio_sdk::client::ComposioClient;
61/// use composio_sdk::models::versioning::{ToolkitVersion, ToolkitVersionParam};
62/// use std::time::Duration;
63/// use std::collections::HashMap;
64///
65/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
66/// let mut versions = HashMap::new();
67/// versions.insert("github".to_string(), ToolkitVersion::Specific("20250906_01".to_string()));
68///
69/// let client = ComposioClient::builder()
70///     .api_key("your_api_key")
71///     .base_url("https://custom.api.com")
72///     .timeout(Duration::from_secs(60))
73///     .max_retries(5)
74///     .initial_retry_delay(Duration::from_secs(2))
75///     .max_retry_delay(Duration::from_secs(30))
76///     .toolkit_versions(ToolkitVersionParam::Versions(versions))
77///     .build()?;
78/// # Ok(())
79/// # }
80/// ```
81#[derive(Debug, Default)]
82pub struct ComposioClientBuilder {
83    api_key: Option<String>,
84    base_url: Option<String>,
85    timeout: Option<Duration>,
86    max_retries: Option<u32>,
87    initial_retry_delay: Option<Duration>,
88    max_retry_delay: Option<Duration>,
89    toolkit_versions: Option<crate::models::versioning::ToolkitVersionParam>,
90    file_download_dir: Option<std::path::PathBuf>,
91    auto_upload_download_files: Option<bool>,
92    telemetry_enabled: Option<bool>,
93}
94
95impl ComposioClient {
96    /// Create a new client builder
97    ///
98    /// Returns a `ComposioClientBuilder` that can be used to configure and build
99    /// a `ComposioClient` instance.
100    ///
101    /// # Example
102    ///
103    /// ```no_run
104    /// use composio_sdk::client::ComposioClient;
105    ///
106    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
107    /// let client = ComposioClient::builder()
108    ///     .api_key("your_api_key")
109    ///     .build()?;
110    /// # Ok(())
111    /// # }
112    /// ```
113    pub fn builder() -> ComposioClientBuilder {
114        ComposioClientBuilder::default()
115    }
116
117    /// Get a reference to the HTTP client
118    ///
119    /// This is useful for advanced use cases where you need direct access to the
120    /// underlying reqwest client.
121    pub fn http_client(&self) -> &reqwest::Client {
122        &self.http_client
123    }
124
125    /// Get a reference to the configuration
126    ///
127    /// Returns the configuration used by this client.
128    pub fn config(&self) -> &ComposioConfig {
129        &self.config
130    }
131
132    /// Create a new session for a user
133    ///
134    /// Returns a `SessionBuilder` that can be used to configure and create
135    /// a Tool Router session for the specified user.
136    ///
137    /// # Arguments
138    ///
139    /// * `user_id` - User identifier for session isolation
140    ///
141    /// # Example
142    ///
143    /// ```no_run
144    /// use composio_sdk::client::ComposioClient;
145    ///
146    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
147    /// let client = ComposioClient::builder()
148    ///     .api_key("your_api_key")
149    ///     .build()?;
150    ///
151    /// let session = client
152    ///     .create_session("user_123")
153    ///     .toolkits(vec!["github", "gmail"])
154    ///     .send()
155    ///     .await?;
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub fn create_session(&self, user_id: impl Into<String>) -> crate::session::SessionBuilder<'_> {
160        crate::session::SessionBuilder::new(self, user_id.into())
161    }
162
163    /// Get an existing session by ID
164    ///
165    /// Retrieves session details for a previously created Tool Router session.
166    /// This is useful for inspecting session configuration and available tools.
167    ///
168    /// # Arguments
169    ///
170    /// * `session_id` - The session ID to retrieve
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if:
175    /// - Session not found (404)
176    /// - Network error occurs
177    /// - API returns an error response
178    ///
179    /// # Example
180    ///
181    /// ```no_run
182    /// use composio_sdk::client::ComposioClient;
183    ///
184    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
185    /// let client = ComposioClient::builder()
186    ///     .api_key("your_api_key")
187    ///     .build()?;
188    ///
189    /// let session = client.get_session("sess_abc123").await?;
190    /// println!("Session ID: {}", session.session_id());
191    /// # Ok(())
192    /// # }
193    /// ```
194    pub async fn get_session(
195        &self,
196        session_id: impl Into<String>,
197    ) -> Result<crate::session::Session, ComposioError> {
198        let session_id = session_id.into();
199        let url = format!(
200            "{}/tool_router/session/{}",
201            self.config.base_url, session_id
202        );
203
204        // Execute request with retry logic
205        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
206            let response = self
207                .http_client
208                .get(&url)
209                .send()
210                .await
211                .map_err(ComposioError::NetworkError)?;
212
213            // Check for errors
214            if !response.status().is_success() {
215                return Err(ComposioError::from_response(response).await);
216            }
217
218            Ok(response)
219        })
220        .await?;
221
222        // Parse response
223        let session_response: crate::models::SessionResponse =
224            response.json().await.map_err(ComposioError::NetworkError)?;
225
226        // Convert to Session
227        Ok(crate::session::Session::from_response(
228            self.clone(),
229            session_response,
230        ))
231    }
232
233    /// Create an MCP server
234    pub async fn create_mcp_server(
235        &self,
236        params: crate::models::mcp::MCPCreateParams,
237    ) -> Result<crate::models::mcp::MCPCreateResponse, ComposioError> {
238        let url = format!("{}/api/v3/mcp/servers", self.config.base_url);
239
240        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
241            let response = self
242                .http_client
243                .post(&url)
244                .header("x-api-key", &self.config.api_key)
245                .json(&params)
246                .send()
247                .await
248                .map_err(ComposioError::NetworkError)?;
249
250            if !response.status().is_success() {
251                return Err(ComposioError::from_response(response).await);
252            }
253
254            Ok(response)
255        })
256        .await?;
257
258        response.json().await.map_err(ComposioError::NetworkError)
259    }
260
261    /// Retrieve an MCP server by ID
262    pub async fn get_mcp_server(
263        &self,
264        id: impl Into<String>,
265    ) -> Result<crate::models::mcp::MCPItem, ComposioError> {
266        let id = id.into();
267        let url = format!("{}/api/v3/mcp/{}", self.config.base_url, id);
268
269        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
270            let response = self
271                .http_client
272                .get(&url)
273                .header("x-api-key", &self.config.api_key)
274                .send()
275                .await
276                .map_err(ComposioError::NetworkError)?;
277
278            if !response.status().is_success() {
279                return Err(ComposioError::from_response(response).await);
280            }
281
282            Ok(response)
283        })
284        .await?;
285
286        response.json().await.map_err(ComposioError::NetworkError)
287    }
288
289    /// Update an MCP server
290    pub async fn update_mcp_server(
291        &self,
292        id: impl Into<String>,
293        params: crate::models::mcp::MCPUpdateParams,
294    ) -> Result<crate::models::mcp::MCPUpdateResponse, ComposioError> {
295        let id = id.into();
296        let url = format!("{}/api/v3/mcp/{}", self.config.base_url, id);
297
298        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
299            let response = self
300                .http_client
301                .patch(&url)
302                .header("x-api-key", &self.config.api_key)
303                .json(&params)
304                .send()
305                .await
306                .map_err(ComposioError::NetworkError)?;
307
308            if !response.status().is_success() {
309                return Err(ComposioError::from_response(response).await);
310            }
311
312            Ok(response)
313        })
314        .await?;
315
316        response.json().await.map_err(ComposioError::NetworkError)
317    }
318
319    /// Delete an MCP server by ID
320    pub async fn delete_mcp_server(
321        &self,
322        id: impl Into<String>,
323    ) -> Result<crate::models::mcp::MCPDeleteResponse, ComposioError> {
324        let id = id.into();
325        let url = format!("{}/api/v3/mcp/{}", self.config.base_url, id);
326
327        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
328            let response = self
329                .http_client
330                .delete(&url)
331                .header("x-api-key", &self.config.api_key)
332                .send()
333                .await
334                .map_err(ComposioError::NetworkError)?;
335
336            if !response.status().is_success() {
337                return Err(ComposioError::from_response(response).await);
338            }
339
340            Ok(response)
341        })
342        .await?;
343
344        response.json().await.map_err(ComposioError::NetworkError)
345    }
346
347    /// List MCP servers
348    pub async fn list_mcp_servers(
349        &self,
350        params: crate::models::mcp::MCPListParams,
351    ) -> Result<crate::models::mcp::MCPListResponse, ComposioError> {
352        let url = format!("{}/api/v3/mcp/servers", self.config.base_url);
353
354        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
355            let response = self
356                .http_client
357                .get(&url)
358                .header("x-api-key", &self.config.api_key)
359                .query(&params)
360                .send()
361                .await
362                .map_err(ComposioError::NetworkError)?;
363
364            if !response.status().is_success() {
365                return Err(ComposioError::from_response(response).await);
366            }
367
368            Ok(response)
369        })
370        .await?;
371
372        response.json().await.map_err(ComposioError::NetworkError)
373    }
374
375    /// List MCP servers for app-scoped retrieval endpoint
376    pub async fn list_mcp_servers_for_app(
377        &self,
378        params: crate::models::mcp::MCPRetrieveAppParams,
379    ) -> Result<crate::models::mcp::MCPRetrieveAppResponse, ComposioError> {
380        let url = format!("{}/api/v3/mcp/servers/app", self.config.base_url);
381
382        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
383            let response = self
384                .http_client
385                .get(&url)
386                .header("x-api-key", &self.config.api_key)
387                .query(&params)
388                .send()
389                .await
390                .map_err(ComposioError::NetworkError)?;
391
392            if !response.status().is_success() {
393                return Err(ComposioError::from_response(response).await);
394            }
395
396            Ok(response)
397        })
398        .await?;
399
400        response.json().await.map_err(ComposioError::NetworkError)
401    }
402
403    /// Generate MCP URLs for users and/or connected accounts
404    pub async fn generate_mcp_server(
405        &self,
406        params: crate::models::mcp::MCPGenerateUrlParams,
407    ) -> Result<crate::models::mcp::MCPGenerateUrlResponse, ComposioError> {
408        let url = format!("{}/api/v3/mcp/servers/generate", self.config.base_url);
409
410        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
411            let response = self
412                .http_client
413                .post(&url)
414                .header("x-api-key", &self.config.api_key)
415                .json(&params)
416                .send()
417                .await
418                .map_err(ComposioError::NetworkError)?;
419
420            if !response.status().is_success() {
421                return Err(ComposioError::from_response(response).await);
422            }
423
424            Ok(response)
425        })
426        .await?;
427
428        response.json().await.map_err(ComposioError::NetworkError)
429    }
430
431    /// Create a custom MCP server
432    pub async fn create_custom_mcp_server(
433        &self,
434        params: crate::models::mcp::MCPCustomCreateParams,
435    ) -> Result<crate::models::mcp::MCPCustomCreateResponse, ComposioError> {
436        let url = format!("{}/api/v3/mcp/servers/custom", self.config.base_url);
437
438        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
439            let response = self
440                .http_client
441                .post(&url)
442                .header("x-api-key", &self.config.api_key)
443                .json(&params)
444                .send()
445                .await
446                .map_err(ComposioError::NetworkError)?;
447
448            if !response.status().is_success() {
449                return Err(ComposioError::from_response(response).await);
450            }
451
452            Ok(response)
453        })
454        .await?;
455
456        response.json().await.map_err(ComposioError::NetworkError)
457    }
458
459    /// Convert a legacy UUID to NanoId for a supported resource type.
460    pub async fn get_migration_nanoid(
461        &self,
462        params: crate::models::migration::MigrationGetNanoIdParams,
463    ) -> Result<crate::models::migration::MigrationGetNanoIdResponse, ComposioError> {
464        let url = format!("{}/api/v3/migration/get-nanoid", self.config.base_url);
465
466        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
467            let response = self
468                .http_client
469                .get(&url)
470                .header("x-api-key", &self.config.api_key)
471                .query(&params)
472                .send()
473                .await
474                .map_err(ComposioError::NetworkError)?;
475
476            if !response.status().is_success() {
477                return Err(ComposioError::from_response(response).await);
478            }
479
480            Ok(response)
481        })
482        .await?;
483
484        response.json().await.map_err(ComposioError::NetworkError)
485    }
486
487    /// Create a new CLI session.
488    pub async fn create_cli_session(
489        &self,
490    ) -> Result<crate::models::cli::CliCreateSessionResponse, ComposioError> {
491        let url = format!("{}/api/v3/cli/create-session", self.config.base_url);
492
493        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
494            let response = self
495                .http_client
496                .post(&url)
497                .header("x-api-key", &self.config.api_key)
498                .send()
499                .await
500                .map_err(ComposioError::NetworkError)?;
501
502            if !response.status().is_success() {
503                return Err(ComposioError::from_response(response).await);
504            }
505
506            Ok(response)
507        })
508        .await?;
509
510        response.json().await.map_err(ComposioError::NetworkError)
511    }
512
513    /// Retrieve a CLI session using UUID or code.
514    pub async fn get_cli_session(
515        &self,
516        params: crate::models::cli::CliGetSessionParams,
517    ) -> Result<crate::models::cli::CliGetSessionResponse, ComposioError> {
518        let url = format!("{}/api/v3/cli/get-session", self.config.base_url);
519
520        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
521            let response = self
522                .http_client
523                .get(&url)
524                .header("x-api-key", &self.config.api_key)
525                .query(&params)
526                .send()
527                .await
528                .map_err(ComposioError::NetworkError)?;
529
530            if !response.status().is_success() {
531                return Err(ComposioError::from_response(response).await);
532            }
533
534            Ok(response)
535        })
536        .await?;
537
538        response.json().await.map_err(ComposioError::NetworkError)
539    }
540
541    /// Retrieve project configuration.
542    pub async fn get_project_config(
543        &self,
544    ) -> Result<crate::models::project::ProjectConfigResponse, ComposioError> {
545        let url = format!("{}/api/v3/org/project/config", self.config.base_url);
546
547        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
548            let response = self
549                .http_client
550                .get(&url)
551                .header("x-api-key", &self.config.api_key)
552                .send()
553                .await
554                .map_err(ComposioError::NetworkError)?;
555
556            if !response.status().is_success() {
557                return Err(ComposioError::from_response(response).await);
558            }
559
560            Ok(response)
561        })
562        .await?;
563
564        response.json().await.map_err(ComposioError::NetworkError)
565    }
566
567    /// Update project configuration.
568    pub async fn update_project_config(
569        &self,
570        params: crate::models::project::ProjectConfigUpdateParams,
571    ) -> Result<crate::models::project::ProjectConfigResponse, ComposioError> {
572        let url = format!("{}/api/v3/org/project/config", self.config.base_url);
573
574        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
575            let response = self
576                .http_client
577                .patch(&url)
578                .header("x-api-key", &self.config.api_key)
579                .json(&params)
580                .send()
581                .await
582                .map_err(ComposioError::NetworkError)?;
583
584            if !response.status().is_success() {
585                return Err(ComposioError::from_response(response).await);
586            }
587
588            Ok(response)
589        })
590        .await?;
591
592        response.json().await.map_err(ComposioError::NetworkError)
593    }
594
595    /// List connected accounts
596    ///
597    /// Retrieves a list of connected accounts based on the provided filters.
598    ///
599    /// # Arguments
600    ///
601    /// * `params` - Filter parameters for the query
602    ///
603    /// # Example
604    ///
605    /// ```no_run
606    /// use composio_sdk::client::ComposioClient;
607    /// use composio_sdk::models::connected_accounts::ConnectedAccountListParams;
608    ///
609    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
610    /// let client = ComposioClient::builder()
611    ///     .api_key("your_api_key")
612    ///     .build()?;
613    ///
614    /// let params = ConnectedAccountListParams {
615    ///     user_ids: Some(vec!["user_123".to_string()]),
616    ///     toolkit_slugs: Some(vec!["github".to_string()]),
617    ///     ..Default::default()
618    /// };
619    ///
620    /// let accounts = client.list_connected_accounts(params).await?;
621    /// # Ok(())
622    /// # }
623    /// ```
624    pub async fn list_connected_accounts(
625        &self,
626        params: crate::models::connected_accounts::ConnectedAccountListParams,
627    ) -> Result<crate::models::connected_accounts::ConnectedAccountListResponse, ComposioError>
628    {
629        let mut url = format!("{}/api/v3/connected_accounts", self.config.base_url);
630
631        // Build query parameters
632        let mut query_params = vec![];
633
634        if let Some(user_ids) = &params.user_ids {
635            query_params.push(format!("user_ids={}", user_ids.join(",")));
636        }
637        if let Some(auth_config_ids) = &params.auth_config_ids {
638            query_params.push(format!("auth_config_ids={}", auth_config_ids.join(",")));
639        }
640        if let Some(toolkit_slugs) = &params.toolkit_slugs {
641            query_params.push(format!("toolkit_slugs={}", toolkit_slugs.join(",")));
642        }
643        if let Some(connected_account_ids) = &params.connected_account_ids {
644            query_params.push(format!(
645                "connected_account_ids={}",
646                connected_account_ids.join(",")
647            ));
648        }
649        if let Some(statuses) = &params.statuses {
650            let status_strings: Vec<String> = statuses
651                .iter()
652                .map(|s| {
653                    serde_json::to_string(s)
654                        .unwrap_or_default()
655                        .trim_matches('"')
656                        .to_string()
657                })
658                .collect();
659            query_params.push(format!("statuses={}", status_strings.join(",")));
660        }
661        if let Some(show_disabled) = params.show_disabled {
662            query_params.push(format!("show_disabled={}", show_disabled));
663        }
664        if let Some(limit) = params.limit {
665            query_params.push(format!("limit={}", limit));
666        }
667        if let Some(cursor) = &params.cursor {
668            query_params.push(format!("cursor={}", cursor));
669        }
670        if let Some(order_by) = &params.order_by {
671            query_params.push(format!("order_by={}", order_by));
672        }
673        if let Some(order_direction) = &params.order_direction {
674            query_params.push(format!("order_direction={}", order_direction));
675        }
676
677        if !query_params.is_empty() {
678            url.push_str("?");
679            url.push_str(&query_params.join("&"));
680        }
681
682        // Execute request with retry logic
683        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
684            let response = self
685                .http_client
686                .get(&url)
687                .header("x-api-key", &self.config.api_key)
688                .send()
689                .await
690                .map_err(ComposioError::NetworkError)?;
691
692            // Check for errors
693            if !response.status().is_success() {
694                return Err(ComposioError::from_response(response).await);
695            }
696
697            Ok(response)
698        })
699        .await?;
700
701        // Parse response
702        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
703    }
704
705    /// Get a specific connected account by ID
706    ///
707    /// # Arguments
708    ///
709    /// * `account_id` - The connected account ID
710    ///
711    /// # Example
712    ///
713    /// ```no_run
714    /// use composio_sdk::client::ComposioClient;
715    ///
716    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
717    /// let client = ComposioClient::builder()
718    ///     .api_key("your_api_key")
719    ///     .build()?;
720    ///
721    /// let account = client.get_connected_account("ca_abc123").await?;
722    /// # Ok(())
723    /// # }
724    /// ```
725    pub async fn get_connected_account(
726        &self,
727        account_id: impl Into<String>,
728    ) -> Result<crate::models::connected_accounts::ConnectedAccountInfo, ComposioError> {
729        let account_id = account_id.into();
730        let url = format!(
731            "{}/api/v3/connected_accounts/{}",
732            self.config.base_url, account_id
733        );
734
735        // Execute request with retry logic
736        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
737            let response = self
738                .http_client
739                .get(&url)
740                .header("x-api-key", &self.config.api_key)
741                .send()
742                .await
743                .map_err(ComposioError::NetworkError)?;
744
745            // Check for errors
746            if !response.status().is_success() {
747                return Err(ComposioError::from_response(response).await);
748            }
749
750            Ok(response)
751        })
752        .await?;
753
754        // Parse response
755        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
756    }
757
758    /// Create a direct connected-account auth link.
759    pub async fn create_connected_account_link(
760        &self,
761        params: crate::models::link::ConnectedAccountLinkCreateParams,
762    ) -> Result<crate::models::link::ConnectedAccountLinkCreateResponse, ComposioError> {
763        let url = format!("{}/api/v3/connected_accounts/link", self.config.base_url);
764
765        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
766            let response = self
767                .http_client
768                .post(&url)
769                .header("x-api-key", &self.config.api_key)
770                .json(&params)
771                .send()
772                .await
773                .map_err(ComposioError::NetworkError)?;
774
775            if !response.status().is_success() {
776                return Err(ComposioError::from_response(response).await);
777            }
778
779            Ok(response)
780        })
781        .await?;
782
783        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
784    }
785
786    /// Refresh a connected account's authentication state.
787    pub async fn refresh_connected_account(
788        &self,
789        account_id: impl Into<String>,
790        params: crate::models::connected_accounts::ConnectedAccountRefreshParams,
791    ) -> Result<crate::models::connected_accounts::ConnectedAccountRefreshResponse, ComposioError>
792    {
793        let account_id = account_id.into();
794        let url = format!(
795            "{}/api/v3/connected_accounts/{}/refresh",
796            self.config.base_url, account_id
797        );
798
799        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
800            let response = self
801                .http_client
802                .post(&url)
803                .header("x-api-key", &self.config.api_key)
804                .json(&params)
805                .send()
806                .await
807                .map_err(ComposioError::NetworkError)?;
808
809            if !response.status().is_success() {
810                return Err(ComposioError::from_response(response).await);
811            }
812
813            Ok(response)
814        })
815        .await?;
816
817        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
818    }
819
820    /// Enable or disable a connected account.
821    pub async fn update_connected_account_status(
822        &self,
823        account_id: impl Into<String>,
824        params: crate::models::connected_accounts::ConnectedAccountUpdateStatusParams,
825    ) -> Result<
826        crate::models::connected_accounts::ConnectedAccountUpdateStatusResponse,
827        ComposioError,
828    > {
829        let account_id = account_id.into();
830        let url = format!(
831            "{}/api/v3/connected_accounts/{}/status",
832            self.config.base_url, account_id
833        );
834
835        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
836            let response = self
837                .http_client
838                .patch(&url)
839                .header("x-api-key", &self.config.api_key)
840                .json(&params)
841                .send()
842                .await
843                .map_err(ComposioError::NetworkError)?;
844
845            if !response.status().is_success() {
846                return Err(ComposioError::from_response(response).await);
847            }
848
849            Ok(response)
850        })
851        .await?;
852
853        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
854    }
855
856    /// Delete a connected account.
857    pub async fn delete_connected_account(
858        &self,
859        account_id: impl Into<String>,
860    ) -> Result<crate::models::connected_accounts::ConnectedAccountDeleteResponse, ComposioError>
861    {
862        let account_id = account_id.into();
863        let url = format!(
864            "{}/api/v3/connected_accounts/{}",
865            self.config.base_url, account_id
866        );
867
868        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
869            let response = self
870                .http_client
871                .delete(&url)
872                .header("x-api-key", &self.config.api_key)
873                .send()
874                .await
875                .map_err(ComposioError::NetworkError)?;
876
877            if !response.status().is_success() {
878                return Err(ComposioError::from_response(response).await);
879            }
880
881            Ok(response)
882        })
883        .await?;
884
885        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
886    }
887
888    // ========================================================================
889    // Files Methods
890    // ========================================================================
891
892    /// List files available in the project.
893    pub async fn list_files(
894        &self,
895        params: crate::models::files::FileListParams,
896    ) -> Result<crate::models::files::FileListResponse, ComposioError> {
897        let url = format!("{}/api/v3/files/list", self.config.base_url);
898
899        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
900            let response = self
901                .http_client
902                .get(&url)
903                .header("x-api-key", &self.config.api_key)
904                .query(&params)
905                .send()
906                .await
907                .map_err(ComposioError::NetworkError)?;
908
909            if !response.status().is_success() {
910                return Err(ComposioError::from_response(response).await);
911            }
912
913            Ok(response)
914        })
915        .await?;
916
917        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
918    }
919
920    /// Request a presigned upload URL for a file.
921    pub async fn create_file_upload_request(
922        &self,
923        params: crate::models::files::FileCreatePresignedUrlParams,
924    ) -> Result<crate::models::files::FileCreatePresignedUrlResponse, ComposioError> {
925        let url = format!("{}/api/v3/files/upload/request", self.config.base_url);
926
927        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
928            let response = self
929                .http_client
930                .post(&url)
931                .header("x-api-key", &self.config.api_key)
932                .json(&params)
933                .send()
934                .await
935                .map_err(ComposioError::NetworkError)?;
936
937            if !response.status().is_success() {
938                return Err(ComposioError::from_response(response).await);
939            }
940
941            Ok(response)
942        })
943        .await?;
944
945        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
946    }
947
948    // ========================================================================
949    // Toolkits Methods
950    // ========================================================================
951
952    /// List all toolkits
953    ///
954    /// Retrieves a list of available toolkits based on the provided filters.
955    /// Toolkits are collections of tools that can be used to perform various tasks.
956    ///
957    /// # Arguments
958    ///
959    /// * `params` - Filter parameters for the query
960    ///
961    /// # Example
962    ///
963    /// ```no_run
964    /// use composio_sdk::client::ComposioClient;
965    /// use composio_sdk::models::toolkits::ToolkitListParams;
966    ///
967    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
968    /// let client = ComposioClient::builder()
969    ///     .api_key("your_api_key")
970    ///     .build()?;
971    ///
972    /// let params = ToolkitListParams {
973    ///     category: Some("communication".to_string()),
974    ///     limit: Some(20),
975    ///     ..Default::default()
976    /// };
977    ///
978    /// let toolkits = client.list_toolkits(params).await?;
979    /// for toolkit in toolkits.items {
980    ///     println!("Toolkit: {} ({})", toolkit.name, toolkit.slug);
981    /// }
982    /// # Ok(())
983    /// # }
984    /// ```
985    pub async fn list_toolkits(
986        &self,
987        params: crate::models::toolkits::ToolkitListParams,
988    ) -> Result<crate::models::toolkits::ToolkitListResponse, ComposioError> {
989        let mut url = format!("{}/api/v3/toolkits", self.config.base_url);
990
991        // Build query parameters
992        let mut query_params = vec![];
993
994        if let Some(category) = &params.category {
995            query_params.push(format!("category={}", category));
996        }
997        if let Some(cursor) = &params.cursor {
998            query_params.push(format!("cursor={}", cursor));
999        }
1000        if let Some(limit) = params.limit {
1001            query_params.push(format!("limit={}", limit));
1002        }
1003        if let Some(sort_by) = &params.sort_by {
1004            let sort_str = match sort_by {
1005                crate::models::toolkits::SortBy::Usage => "usage",
1006                crate::models::toolkits::SortBy::Alphabetically => "alphabetically",
1007            };
1008            query_params.push(format!("sort_by={}", sort_str));
1009        }
1010        if let Some(managed_by) = &params.managed_by {
1011            let managed_str = match managed_by {
1012                crate::models::toolkits::ManagedBy::Composio => "composio",
1013                crate::models::toolkits::ManagedBy::All => "all",
1014                crate::models::toolkits::ManagedBy::Project => "project",
1015            };
1016            query_params.push(format!("managed_by={}", managed_str));
1017        }
1018        if let Some(search) = &params.search {
1019            query_params.push(format!("search={}", search));
1020        }
1021        if let Some(show_deprecated) = params.show_deprecated {
1022            query_params.push(format!("show_deprecated={}", show_deprecated));
1023        }
1024
1025        if !query_params.is_empty() {
1026            url.push_str("?");
1027            url.push_str(&query_params.join("&"));
1028        }
1029
1030        // Execute request with retry logic
1031        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1032            let response = self
1033                .http_client
1034                .get(&url)
1035                .header("x-api-key", &self.config.api_key)
1036                .send()
1037                .await
1038                .map_err(ComposioError::NetworkError)?;
1039
1040            // Check for errors
1041            if !response.status().is_success() {
1042                return Err(ComposioError::from_response(response).await);
1043            }
1044
1045            Ok(response)
1046        })
1047        .await?;
1048
1049        // Parse response
1050        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1051    }
1052
1053    /// Get a specific toolkit by slug
1054    ///
1055    /// Retrieves detailed information about a specific toolkit including
1056    /// authentication schemes, available tools, and configuration details.
1057    ///
1058    /// # Arguments
1059    ///
1060    /// * `slug` - The toolkit slug (e.g., "github", "gmail", "slack")
1061    ///
1062    /// # Example
1063    ///
1064    /// ```no_run
1065    /// use composio_sdk::client::ComposioClient;
1066    ///
1067    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1068    /// let client = ComposioClient::builder()
1069    ///     .api_key("your_api_key")
1070    ///     .build()?;
1071    ///
1072    /// let toolkit = client.get_toolkit("github").await?;
1073    /// println!("Toolkit: {}", toolkit.name);
1074    /// println!("Auth schemes: {:?}", toolkit.auth_schemes);
1075    /// # Ok(())
1076    /// # }
1077    /// ```
1078    pub async fn get_toolkit(
1079        &self,
1080        slug: impl Into<String>,
1081    ) -> Result<crate::models::toolkits::ToolkitRetrieveResponse, ComposioError> {
1082        self.get_toolkit_with_params(
1083            slug,
1084            crate::models::toolkits::ToolkitRetrieveParams::default(),
1085        )
1086        .await
1087    }
1088
1089    /// Get a specific toolkit by slug with optional query parameters.
1090    pub async fn get_toolkit_with_params(
1091        &self,
1092        slug: impl Into<String>,
1093        params: crate::models::toolkits::ToolkitRetrieveParams,
1094    ) -> Result<crate::models::toolkits::ToolkitRetrieveResponse, ComposioError> {
1095        let slug = slug.into();
1096        let url = format!("{}/api/v3/toolkits/{}", self.config.base_url, slug);
1097
1098        // Execute request with retry logic
1099        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1100            let response = self
1101                .http_client
1102                .get(&url)
1103                .header("x-api-key", &self.config.api_key)
1104                .query(&params)
1105                .send()
1106                .await
1107                .map_err(ComposioError::NetworkError)?;
1108
1109            // Check for errors
1110            if !response.status().is_success() {
1111                return Err(ComposioError::from_response(response).await);
1112            }
1113
1114            Ok(response)
1115        })
1116        .await?;
1117
1118        // Parse response
1119        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1120    }
1121
1122    /// List all toolkit categories
1123    ///
1124    /// Retrieves a list of all available toolkit categories.
1125    /// Categories help organize toolkits by functionality or industry.
1126    ///
1127    /// # Example
1128    ///
1129    /// ```no_run
1130    /// use composio_sdk::client::ComposioClient;
1131    ///
1132    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1133    /// let client = ComposioClient::builder()
1134    ///     .api_key("your_api_key")
1135    ///     .build()?;
1136    ///
1137    /// let categories = client.list_toolkit_categories().await?;
1138    /// for category in categories.items {
1139    ///     println!("Category: {}", category.name);
1140    /// }
1141    /// # Ok(())
1142    /// # }
1143    /// ```
1144    pub async fn list_toolkit_categories(
1145        &self,
1146    ) -> Result<crate::models::toolkits::ToolkitCategoriesResponse, ComposioError> {
1147        let url = format!("{}/api/v3/toolkits/categories", self.config.base_url);
1148
1149        // Execute request with retry logic
1150        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1151            let response = self
1152                .http_client
1153                .get(&url)
1154                .header("x-api-key", &self.config.api_key)
1155                .send()
1156                .await
1157                .map_err(ComposioError::NetworkError)?;
1158
1159            // Check for errors
1160            if !response.status().is_success() {
1161                return Err(ComposioError::from_response(response).await);
1162            }
1163
1164            Ok(response)
1165        })
1166        .await?;
1167
1168        // Parse response
1169        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1170    }
1171
1172    /// Authorize a user to a toolkit
1173    ///
1174    /// Creates an authentication link for a user to connect to a specific toolkit.
1175    /// If an auth config is not found, it will be created using Composio managed auth.
1176    ///
1177    /// This is a convenience method that:
1178    /// 1. Gets or creates an auth config for the toolkit
1179    /// 2. Initiates a connection for the user
1180    /// 3. Returns the connection request with redirect URL
1181    ///
1182    /// # Arguments
1183    ///
1184    /// * `user_id` - The ID of the user to authorize
1185    /// * `toolkit` - The slug of the toolkit to authorize (e.g., "github", "gmail")
1186    ///
1187    /// # Returns
1188    ///
1189    /// Returns a connection request with a `redirect_url` that the user should visit
1190    /// to complete the authentication flow.
1191    ///
1192    /// # Example
1193    ///
1194    /// ```no_run
1195    /// use composio_sdk::client::ComposioClient;
1196    ///
1197    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1198    /// let client = ComposioClient::builder()
1199    ///     .api_key("your_api_key")
1200    ///     .build()?;
1201    ///
1202    /// let connection = client.authorize_toolkit("user_123", "github").await?;
1203    /// println!("Visit this URL to authenticate: {}", connection.redirect_url.unwrap());
1204    /// # Ok(())
1205    /// # }
1206    /// ```
1207    pub async fn authorize_toolkit(
1208        &self,
1209        user_id: impl Into<String>,
1210        toolkit: impl Into<String>,
1211    ) -> Result<crate::models::connected_accounts::ConnectionRequest, ComposioError> {
1212        let user_id = user_id.into();
1213        let toolkit = toolkit.into();
1214
1215        // Get or create auth config
1216        let auth_config_id = self.get_or_create_auth_config(&toolkit).await?;
1217
1218        // Initiate connection
1219        self.initiate_connection(user_id, auth_config_id, None)
1220            .await
1221    }
1222
1223    /// Get or create an auth config for a toolkit (internal helper)
1224    ///
1225    /// This method checks if an auth config exists for the toolkit.
1226    /// If found, returns the most recent one. If not found, creates a new one
1227    /// using Composio managed auth.
1228    async fn get_or_create_auth_config(&self, toolkit: &str) -> Result<String, ComposioError> {
1229        use crate::models::auth_configs::{
1230            AuthConfigCreateParams, AuthConfigListParams, AuthConfigOptions,
1231        };
1232
1233        // List existing auth configs for this toolkit
1234        let params = AuthConfigListParams {
1235            toolkit_slug: Some(toolkit.to_string()),
1236            ..Default::default()
1237        };
1238
1239        let auth_configs = self.list_auth_configs(params).await?;
1240
1241        // If we have existing configs, return the most recent one
1242        if !auth_configs.items.is_empty() {
1243            let mut configs = auth_configs.items;
1244            configs.sort_by(|a, b| b.created_at.cmp(&a.created_at));
1245            return Ok(configs[0].id.clone());
1246        }
1247
1248        // Create new auth config using Composio managed auth
1249        let create_request = AuthConfigCreateParams {
1250            toolkit: toolkit.to_string(),
1251            options: AuthConfigOptions::Default {
1252                scopes: None,
1253                user_scopes: None,
1254                restrict_to_following_tools: Some(vec![]),
1255            },
1256        };
1257
1258        let created = self.create_auth_config(create_request).await?;
1259        Ok(created.auth_config.id)
1260    }
1261
1262    /// Initiate a connection for a user (internal helper)
1263    ///
1264    /// Creates a new connected account initiation request.
1265    async fn initiate_connection(
1266        &self,
1267        user_id: String,
1268        auth_config_id: String,
1269        callback_url: Option<String>,
1270    ) -> Result<crate::models::connected_accounts::ConnectionRequest, ComposioError> {
1271        use crate::models::connected_accounts::InitiateConnectionParams;
1272
1273        let url = format!("{}/api/v3/connected_accounts", self.config.base_url);
1274
1275        let request_body = InitiateConnectionParams {
1276            user_id,
1277            auth_config_id,
1278            callback_url,
1279            allow_multiple: None,
1280            config: None,
1281        };
1282
1283        // Execute request with retry logic
1284        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1285            let response = self
1286                .http_client
1287                .post(&url)
1288                .header("x-api-key", &self.config.api_key)
1289                .json(&request_body)
1290                .send()
1291                .await
1292                .map_err(ComposioError::NetworkError)?;
1293
1294            // Check for errors
1295            if !response.status().is_success() {
1296                return Err(ComposioError::from_response(response).await);
1297            }
1298
1299            Ok(response)
1300        })
1301        .await?;
1302
1303        // Parse response - API returns connection info with redirect_url
1304        #[derive(Deserialize)]
1305        struct ConnectionResponse {
1306            id: String,
1307            status: Option<crate::models::connected_accounts::ConnectionStatus>,
1308            redirect_url: Option<String>,
1309        }
1310
1311        let conn_response: ConnectionResponse =
1312            response.json().await.map_err(ComposioError::NetworkError)?;
1313
1314        Ok(crate::models::connected_accounts::ConnectionRequest::new(
1315            conn_response.id,
1316            conn_response
1317                .status
1318                .unwrap_or(crate::models::connected_accounts::ConnectionStatus::Initiated),
1319            conn_response.redirect_url,
1320        ))
1321    }
1322
1323    /// Retrieve an auth config by ID.
1324    pub async fn get_auth_config(
1325        &self,
1326        auth_config_id: impl Into<String>,
1327    ) -> Result<crate::models::auth_configs::AuthConfigRetrieveResponse, ComposioError> {
1328        let auth_config_id = auth_config_id.into();
1329        let url = format!(
1330            "{}/api/v3/auth_configs/{}",
1331            self.config.base_url, auth_config_id
1332        );
1333
1334        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1335            let response = self
1336                .http_client
1337                .get(&url)
1338                .header("x-api-key", &self.config.api_key)
1339                .send()
1340                .await
1341                .map_err(ComposioError::NetworkError)?;
1342
1343            if !response.status().is_success() {
1344                return Err(ComposioError::from_response(response).await);
1345            }
1346
1347            Ok(response)
1348        })
1349        .await?;
1350
1351        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1352    }
1353
1354    /// Update an auth config by ID.
1355    pub async fn update_auth_config(
1356        &self,
1357        auth_config_id: impl Into<String>,
1358        params: crate::models::auth_configs::AuthConfigUpdateParams,
1359    ) -> Result<crate::models::auth_configs::AuthConfigUpdateResponse, ComposioError> {
1360        let auth_config_id = auth_config_id.into();
1361        let url = format!(
1362            "{}/api/v3/auth_configs/{}",
1363            self.config.base_url, auth_config_id
1364        );
1365
1366        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1367            let response = self
1368                .http_client
1369                .patch(&url)
1370                .header("x-api-key", &self.config.api_key)
1371                .json(&params)
1372                .send()
1373                .await
1374                .map_err(ComposioError::NetworkError)?;
1375
1376            if !response.status().is_success() {
1377                return Err(ComposioError::from_response(response).await);
1378            }
1379
1380            Ok(response)
1381        })
1382        .await?;
1383
1384        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1385    }
1386
1387    /// Delete an auth config by ID.
1388    pub async fn delete_auth_config(
1389        &self,
1390        auth_config_id: impl Into<String>,
1391    ) -> Result<crate::models::auth_configs::AuthConfigDeleteResponse, ComposioError> {
1392        let auth_config_id = auth_config_id.into();
1393        let url = format!(
1394            "{}/api/v3/auth_configs/{}",
1395            self.config.base_url, auth_config_id
1396        );
1397
1398        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1399            let response = self
1400                .http_client
1401                .delete(&url)
1402                .header("x-api-key", &self.config.api_key)
1403                .send()
1404                .await
1405                .map_err(ComposioError::NetworkError)?;
1406
1407            if !response.status().is_success() {
1408                return Err(ComposioError::from_response(response).await);
1409            }
1410
1411            Ok(response)
1412        })
1413        .await?;
1414
1415        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1416    }
1417
1418    /// Update auth config status to ENABLED or DISABLED.
1419    pub async fn update_auth_config_status(
1420        &self,
1421        auth_config_id: impl Into<String>,
1422        status: crate::models::auth_configs::AuthConfigStatus,
1423    ) -> Result<crate::models::auth_configs::AuthConfigStatusUpdateResponse, ComposioError> {
1424        let auth_config_id = auth_config_id.into();
1425        let url = format!(
1426            "{}/api/v3/auth_configs/{}/{}",
1427            self.config.base_url,
1428            auth_config_id,
1429            status.as_str()
1430        );
1431
1432        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1433            let response = self
1434                .http_client
1435                .patch(&url)
1436                .header("x-api-key", &self.config.api_key)
1437                .send()
1438                .await
1439                .map_err(ComposioError::NetworkError)?;
1440
1441            if !response.status().is_success() {
1442                return Err(ComposioError::from_response(response).await);
1443            }
1444
1445            Ok(response)
1446        })
1447        .await?;
1448
1449        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1450    }
1451
1452    /// List auth configs (internal helper)
1453    async fn list_auth_configs(
1454        &self,
1455        params: crate::models::auth_configs::AuthConfigListParams,
1456    ) -> Result<crate::models::auth_configs::AuthConfigListResponse, ComposioError> {
1457        let mut url = format!("{}/api/v3/auth_configs", self.config.base_url);
1458
1459        // Build query parameters
1460        let mut query_params = vec![];
1461
1462        if let Some(toolkit_slug) = &params.toolkit_slug {
1463            query_params.push(format!("toolkit_slug={}", toolkit_slug));
1464        }
1465        if let Some(is_composio_managed) = params.is_composio_managed {
1466            query_params.push(format!("is_composio_managed={}", is_composio_managed));
1467        }
1468        if let Some(show_disabled) = params.show_disabled {
1469            query_params.push(format!("show_disabled={}", show_disabled));
1470        }
1471        if let Some(search) = &params.search {
1472            query_params.push(format!("search={}", search));
1473        }
1474        if let Some(limit) = params.limit {
1475            query_params.push(format!("limit={}", limit));
1476        }
1477        if let Some(cursor) = &params.cursor {
1478            query_params.push(format!("cursor={}", cursor));
1479        }
1480
1481        if !query_params.is_empty() {
1482            url.push_str("?");
1483            url.push_str(&query_params.join("&"));
1484        }
1485
1486        // Execute request with retry logic
1487        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1488            let response = self
1489                .http_client
1490                .get(&url)
1491                .header("x-api-key", &self.config.api_key)
1492                .send()
1493                .await
1494                .map_err(ComposioError::NetworkError)?;
1495
1496            // Check for errors
1497            if !response.status().is_success() {
1498                return Err(ComposioError::from_response(response).await);
1499            }
1500
1501            Ok(response)
1502        })
1503        .await?;
1504
1505        // Parse response
1506        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1507    }
1508
1509    /// Create auth config (internal helper)
1510    async fn create_auth_config(
1511        &self,
1512        request: crate::models::auth_configs::AuthConfigCreateParams,
1513    ) -> Result<crate::models::auth_configs::AuthConfigCreateResponse, ComposioError> {
1514        let url = format!("{}/api/v3/auth_configs", self.config.base_url);
1515
1516        // Execute request with retry logic
1517        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1518            let response = self
1519                .http_client
1520                .post(&url)
1521                .header("x-api-key", &self.config.api_key)
1522                .json(&request)
1523                .send()
1524                .await
1525                .map_err(ComposioError::NetworkError)?;
1526
1527            // Check for errors
1528            if !response.status().is_success() {
1529                return Err(ComposioError::from_response(response).await);
1530            }
1531
1532            Ok(response)
1533        })
1534        .await?;
1535
1536        // Parse response
1537        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1538    }
1539
1540    /// Get connected account initiation fields for a toolkit
1541    ///
1542    /// Retrieves the required and optional fields needed to initiate a connection
1543    /// for a specific toolkit and authentication scheme.
1544    ///
1545    /// # Arguments
1546    ///
1547    /// * `toolkit` - The toolkit slug (e.g., "github", "gmail")
1548    /// * `auth_scheme` - The authentication scheme (e.g., "OAUTH2", "API_KEY")
1549    /// * `required_only` - If true, returns only required fields; if false, returns both required and optional
1550    ///
1551    /// # Example
1552    ///
1553    /// ```no_run
1554    /// use composio_sdk::client::ComposioClient;
1555    ///
1556    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1557    /// let client = ComposioClient::builder()
1558    ///     .api_key("your_api_key")
1559    ///     .build()?;
1560    ///
1561    /// let fields = client.get_connected_account_initiation_fields(
1562    ///     "github",
1563    ///     "OAUTH2",
1564    ///     false
1565    /// ).await?;
1566    ///
1567    /// for field in fields {
1568    ///     println!("Field: {} (required: {})", field.name, field.required);
1569    /// }
1570    /// # Ok(())
1571    /// # }
1572    /// ```
1573    pub async fn get_connected_account_initiation_fields(
1574        &self,
1575        toolkit: impl Into<String>,
1576        auth_scheme: impl Into<String>,
1577        required_only: bool,
1578    ) -> Result<Vec<crate::models::toolkits::AuthField>, ComposioError> {
1579        let toolkit = toolkit.into();
1580        let auth_scheme = auth_scheme.into();
1581
1582        let toolkit_info = self.get_toolkit(&toolkit).await?;
1583
1584        let details = toolkit_info.auth_config_details.ok_or_else(|| {
1585            ComposioError::InvalidInput(format!(
1586                "No auth config details found for toolkit: {}",
1587                toolkit
1588            ))
1589        })?;
1590
1591        for auth_detail in details {
1592            if auth_detail.mode == auth_scheme {
1593                if required_only {
1594                    return Ok(auth_detail.fields.connected_account_initiation.required);
1595                } else {
1596                    let mut fields = auth_detail.fields.connected_account_initiation.required;
1597                    fields.extend(auth_detail.fields.connected_account_initiation.optional);
1598                    return Ok(fields);
1599                }
1600            }
1601        }
1602
1603        Err(ComposioError::InvalidInput(format!(
1604            "Auth config details not found with toolkit={} and auth_scheme={}",
1605            toolkit, auth_scheme
1606        )))
1607    }
1608
1609    /// Get auth config creation fields for a toolkit
1610    ///
1611    /// Retrieves the required and optional fields needed to create an auth config
1612    /// for a specific toolkit and authentication scheme.
1613    ///
1614    /// # Arguments
1615    ///
1616    /// * `toolkit` - The toolkit slug (e.g., "github", "gmail")
1617    /// * `auth_scheme` - The authentication scheme (e.g., "OAUTH2", "API_KEY")
1618    /// * `required_only` - If true, returns only required fields; if false, returns both required and optional
1619    ///
1620    /// # Example
1621    ///
1622    /// ```no_run
1623    /// use composio_sdk::client::ComposioClient;
1624    ///
1625    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1626    /// let client = ComposioClient::builder()
1627    ///     .api_key("your_api_key")
1628    ///     .build()?;
1629    ///
1630    /// let fields = client.get_auth_config_creation_fields(
1631    ///     "github",
1632    ///     "OAUTH2",
1633    ///     true
1634    /// ).await?;
1635    ///
1636    /// for field in fields {
1637    ///     println!("Required field: {}", field.name);
1638    /// }
1639    /// # Ok(())
1640    /// # }
1641    /// ```
1642    pub async fn get_auth_config_creation_fields(
1643        &self,
1644        toolkit: impl Into<String>,
1645        auth_scheme: impl Into<String>,
1646        required_only: bool,
1647    ) -> Result<Vec<crate::models::toolkits::AuthField>, ComposioError> {
1648        let toolkit = toolkit.into();
1649        let auth_scheme = auth_scheme.into();
1650
1651        let toolkit_info = self.get_toolkit(&toolkit).await?;
1652
1653        let details = toolkit_info.auth_config_details.ok_or_else(|| {
1654            ComposioError::InvalidInput(format!(
1655                "No auth config details found for toolkit: {}",
1656                toolkit
1657            ))
1658        })?;
1659
1660        for auth_detail in details {
1661            if auth_detail.mode == auth_scheme {
1662                if required_only {
1663                    return Ok(auth_detail.fields.auth_config_creation.required);
1664                } else {
1665                    let mut fields = auth_detail.fields.auth_config_creation.required;
1666                    fields.extend(auth_detail.fields.auth_config_creation.optional);
1667                    return Ok(fields);
1668                }
1669            }
1670        }
1671
1672        Err(ComposioError::InvalidInput(format!(
1673            "Auth config details not found with toolkit={} and auth_scheme={}",
1674            toolkit, auth_scheme
1675        )))
1676    }
1677
1678    // ========================================================================
1679    // Tools Methods
1680    // ========================================================================
1681
1682    /// List tools with filtering options
1683    ///
1684    /// Retrieves a list of available tools based on the provided filters.
1685    /// Tools are individual actions that can be performed on external services.
1686    ///
1687    /// # Arguments
1688    ///
1689    /// * `params` - Filter parameters for the query
1690    ///
1691    /// # Example
1692    ///
1693    /// ```no_run
1694    /// use composio_sdk::client::ComposioClient;
1695    /// use composio_sdk::models::tools::ToolListParams;
1696    ///
1697    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1698    /// let client = ComposioClient::builder()
1699    ///     .api_key("your_api_key")
1700    ///     .build()?;
1701    ///
1702    /// let params = ToolListParams {
1703    ///     toolkit_slug: Some("github".to_string()),
1704    ///     limit: Some(20),
1705    ///     ..Default::default()
1706    /// };
1707    ///
1708    /// let tools = client.list_tools(params).await?;
1709    /// for tool in tools.items {
1710    ///     println!("Tool: {} ({})", tool.name, tool.slug);
1711    /// }
1712    /// # Ok(())
1713    /// # }
1714    /// ```
1715    pub async fn list_tools(
1716        &self,
1717        params: crate::models::tools::ToolListParams,
1718    ) -> Result<crate::models::tools::ToolListResponse, ComposioError> {
1719        let mut url = format!("{}/api/v3/tools", self.config.base_url);
1720
1721        // Build query parameters
1722        let mut query_params = vec![];
1723
1724        if let Some(tool_slugs) = &params.tool_slugs {
1725            query_params.push(format!("tool_slugs={}", tool_slugs.join(",")));
1726        }
1727        if let Some(toolkit_slug) = &params.toolkit_slug {
1728            query_params.push(format!("toolkit_slug={}", toolkit_slug));
1729        }
1730        if let Some(search) = &params.search {
1731            query_params.push(format!("search={}", search));
1732        }
1733        if let Some(scopes) = &params.scopes {
1734            query_params.push(format!("scopes={}", scopes.join(",")));
1735        }
1736        if let Some(tags) = &params.tags {
1737            query_params.push(format!("tags={}", tags.join(",")));
1738        }
1739        if let Some(importance) = &params.importance {
1740            query_params.push(format!("importance={}", importance));
1741        }
1742        if let Some(show_deprecated) = params.show_deprecated {
1743            query_params.push(format!("show_deprecated={}", show_deprecated));
1744        }
1745        if let Some(limit) = params.limit {
1746            query_params.push(format!("limit={}", limit));
1747        }
1748        if let Some(cursor) = &params.cursor {
1749            query_params.push(format!("cursor={}", cursor));
1750        }
1751        if let Some(toolkit_versions) = &params.toolkit_versions {
1752            query_params.push(format!("toolkit_versions={}", toolkit_versions));
1753        }
1754
1755        if !query_params.is_empty() {
1756            url.push_str("?");
1757            url.push_str(&query_params.join("&"));
1758        }
1759
1760        // Execute request with retry logic
1761        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1762            let response = self
1763                .http_client
1764                .get(&url)
1765                .header("x-api-key", &self.config.api_key)
1766                .send()
1767                .await
1768                .map_err(ComposioError::NetworkError)?;
1769
1770            // Check for errors
1771            if !response.status().is_success() {
1772                return Err(ComposioError::from_response(response).await);
1773            }
1774
1775            Ok(response)
1776        })
1777        .await?;
1778
1779        // Parse response
1780        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1781    }
1782
1783    /// Retrieve all tool slug enumeration values.
1784    pub async fn retrieve_tool_enum(
1785        &self,
1786    ) -> Result<crate::models::tools::ToolRetrieveEnumResponse, ComposioError> {
1787        let url = format!("{}/api/v3/tools/enum", self.config.base_url);
1788
1789        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1790            let response = self
1791                .http_client
1792                .get(&url)
1793                .header("x-api-key", &self.config.api_key)
1794                .send()
1795                .await
1796                .map_err(ComposioError::NetworkError)?;
1797
1798            if !response.status().is_success() {
1799                return Err(ComposioError::from_response(response).await);
1800            }
1801
1802            Ok(response)
1803        })
1804        .await?;
1805
1806        response.json().await.map_err(ComposioError::NetworkError)
1807    }
1808
1809    /// Get a specific tool by slug
1810    ///
1811    /// Retrieves detailed information about a specific tool including
1812    /// input/output schemas, scopes, and version information.
1813    ///
1814    /// # Arguments
1815    ///
1816    /// * `slug` - The tool slug (e.g., "GITHUB_CREATE_ISSUE", "GMAIL_SEND_EMAIL")
1817    ///
1818    /// # Example
1819    ///
1820    /// ```no_run
1821    /// use composio_sdk::client::ComposioClient;
1822    ///
1823    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1824    /// let client = ComposioClient::builder()
1825    ///     .api_key("your_api_key")
1826    ///     .build()?;
1827    ///
1828    /// let tool = client.get_tool("GITHUB_CREATE_ISSUE").await?;
1829    /// println!("Tool: {}", tool.name);
1830    /// println!("Description: {}", tool.description);
1831    /// println!("Version: {}", tool.version);
1832    /// # Ok(())
1833    /// # }
1834    /// ```
1835    pub async fn get_tool(
1836        &self,
1837        slug: impl Into<String>,
1838    ) -> Result<crate::models::tools::ToolInfo, ComposioError> {
1839        let slug = slug.into();
1840        let url = format!("{}/api/v3/tools/{}", self.config.base_url, slug);
1841
1842        // Execute request with retry logic
1843        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1844            let response = self
1845                .http_client
1846                .get(&url)
1847                .header("x-api-key", &self.config.api_key)
1848                .send()
1849                .await
1850                .map_err(ComposioError::NetworkError)?;
1851
1852            // Check for errors
1853            if !response.status().is_success() {
1854                return Err(ComposioError::from_response(response).await);
1855            }
1856
1857            Ok(response)
1858        })
1859        .await?;
1860
1861        // Parse response
1862        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1863    }
1864
1865    /// Execute a tool
1866    ///
1867    /// Executes a specific tool with the provided arguments. The tool must be
1868    /// available and the user must have appropriate authentication.
1869    ///
1870    /// # Arguments
1871    ///
1872    /// * `params` - Tool execution parameters including slug, arguments, and auth info
1873    ///
1874    /// # Returns
1875    ///
1876    /// Returns a `ToolExecutionResponse` containing:
1877    /// - `data`: The tool's output data
1878    /// - `error`: Optional error message if execution failed
1879    /// - `successful`: Whether the execution was successful
1880    /// - `log_id`: Unique identifier for this execution (for debugging)
1881    ///
1882    /// # Errors
1883    ///
1884    /// Returns an error if:
1885    /// - The tool is not found
1886    /// - The user doesn't have a connected account for the toolkit
1887    /// - The arguments are invalid or missing required fields
1888    /// - Network error occurs
1889    /// - API returns an error response
1890    ///
1891    /// # Example
1892    ///
1893    /// ```no_run
1894    /// use composio_sdk::client::ComposioClient;
1895    /// use composio_sdk::models::tools::ToolExecuteParams;
1896    /// use std::collections::HashMap;
1897    ///
1898    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1899    /// let client = ComposioClient::builder()
1900    ///     .api_key("your_api_key")
1901    ///     .build()?;
1902    ///
1903    /// let mut arguments = HashMap::new();
1904    /// arguments.insert("owner".to_string(), serde_json::json!("composio"));
1905    /// arguments.insert("repo".to_string(), serde_json::json!("composio"));
1906    /// arguments.insert("title".to_string(), serde_json::json!("Test issue"));
1907    ///
1908    /// let params = ToolExecuteParams {
1909    ///     slug: "GITHUB_CREATE_ISSUE".to_string(),
1910    ///     arguments,
1911    ///     user_id: Some("user_123".to_string()),
1912    ///     version: Some("1.0.0".to_string()),
1913    ///     ..Default::default()
1914    /// };
1915    ///
1916    /// let result = client.execute_tool(params).await?;
1917    /// println!("Result: {:?}", result.data);
1918    /// # Ok(())
1919    /// # }
1920    /// ```
1921    pub async fn execute_tool(
1922        &self,
1923        params: crate::models::tools::ToolExecuteParams,
1924    ) -> Result<crate::models::tools::ToolExecutionResponse, ComposioError> {
1925        use crate::utils::toolkit_version::get_toolkit_version;
1926
1927        let url = format!(
1928            "{}/api/v3/tools/execute/{}",
1929            self.config.base_url,
1930            params.slug()
1931        );
1932
1933        // Resolve version if not provided
1934        let version = if let Some(v) = params.version {
1935            v
1936        } else {
1937            // Extract toolkit from slug (e.g., "GITHUB_CREATE_ISSUE" -> "github")
1938            let toolkit = params
1939                .slug()
1940                .split('_')
1941                .next()
1942                .unwrap_or(params.slug())
1943                .to_lowercase();
1944
1945            get_toolkit_version(&toolkit, self.config.toolkit_versions.as_ref())
1946                .as_str()
1947                .to_string()
1948        };
1949
1950        // Check if version is 'latest' and skip check is not enabled
1951        if version == "latest" && !params.dangerously_skip_version_check.unwrap_or(false) {
1952            return Err(ComposioError::InvalidInput(
1953                "Tool version 'latest' requires dangerously_skip_version_check=true. \
1954                 Please specify an explicit version or enable the skip check."
1955                    .to_string(),
1956            ));
1957        }
1958
1959        // Build request body
1960        let mut body = serde_json::json!({
1961            "arguments": params.arguments,
1962            "version": version,
1963        });
1964
1965        if let Some(connected_account_id) = params.connected_account_id {
1966            body["connected_account_id"] = serde_json::json!(connected_account_id);
1967        }
1968        if let Some(custom_auth_params) = params.custom_auth_params {
1969            body["custom_auth_params"] = serde_json::to_value(custom_auth_params)
1970                .map_err(|e| ComposioError::InvalidInput(e.to_string()))?;
1971        }
1972        if let Some(custom_connection_data) = params.custom_connection_data {
1973            body["custom_connection_data"] = serde_json::to_value(custom_connection_data)
1974                .map_err(|e| ComposioError::InvalidInput(e.to_string()))?;
1975        }
1976        if let Some(user_id) = params.user_id {
1977            body["user_id"] = serde_json::json!(user_id);
1978        }
1979        if let Some(text) = params.text {
1980            body["text"] = serde_json::json!(text);
1981        }
1982
1983        // Execute request with retry logic
1984        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
1985            let response = self
1986                .http_client
1987                .post(&url)
1988                .header("x-api-key", &self.config.api_key)
1989                .json(&body)
1990                .send()
1991                .await
1992                .map_err(ComposioError::NetworkError)?;
1993
1994            // Check for errors
1995            if !response.status().is_success() {
1996                return Err(ComposioError::from_response(response).await);
1997            }
1998
1999            Ok(response)
2000        })
2001        .await?;
2002
2003        // Parse response
2004        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2005    }
2006
2007    /// Execute a proxy request to a third-party API
2008    ///
2009    /// Makes an authenticated HTTP request to a third-party API using
2010    /// a connected account's credentials. This is useful for calling
2011    /// endpoints that don't have predefined tools.
2012    ///
2013    /// # Arguments
2014    ///
2015    /// * `params` - Proxy request parameters including endpoint, method, and auth
2016    ///
2017    /// # Example
2018    ///
2019    /// ```no_run
2020    /// use composio_sdk::client::ComposioClient;
2021    /// use composio_sdk::models::tools::{ToolProxyParams, HttpMethod};
2022    ///
2023    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2024    /// let client = ComposioClient::builder()
2025    ///     .api_key("your_api_key")
2026    ///     .build()?;
2027    ///
2028    /// let params = ToolProxyParams {
2029    ///     endpoint: "/repos/composio/composio".to_string(),
2030    ///     method: HttpMethod::Get,
2031    ///     connected_account_id: Some("ca_123".to_string()),
2032    ///     body: None,
2033    ///     parameters: None,
2034    ///     custom_connection_data: None,
2035    /// };
2036    ///
2037    /// let result = client.proxy_tool(params).await?;
2038    /// println!("Status: {}", result.status);
2039    /// println!("Data: {:?}", result.data);
2040    /// # Ok(())
2041    /// # }
2042    /// ```
2043    pub async fn proxy_tool(
2044        &self,
2045        params: crate::models::tools::ToolProxyParams,
2046    ) -> Result<crate::models::tools::ToolProxyResponse, ComposioError> {
2047        let url = format!("{}/api/v3/tools/execute/proxy", self.config.base_url);
2048
2049        // Build request body
2050        let mut body = serde_json::json!({
2051            "endpoint": params.endpoint,
2052            "method": params.method,
2053        });
2054
2055        if let Some(request_body) = params.body {
2056            body["body"] = request_body;
2057        }
2058        if let Some(connected_account_id) = params.connected_account_id {
2059            body["connected_account_id"] = serde_json::json!(connected_account_id);
2060        }
2061        if let Some(parameters) = params.parameters {
2062            body["parameters"] = serde_json::to_value(parameters)
2063                .map_err(|e| ComposioError::InvalidInput(e.to_string()))?;
2064        }
2065        if let Some(custom_connection_data) = params.custom_connection_data {
2066            body["custom_connection_data"] = serde_json::to_value(custom_connection_data)
2067                .map_err(|e| ComposioError::InvalidInput(e.to_string()))?;
2068        }
2069
2070        // Execute request with retry logic
2071        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2072            let response = self
2073                .http_client
2074                .post(&url)
2075                .header("x-api-key", &self.config.api_key)
2076                .json(&body)
2077                .send()
2078                .await
2079                .map_err(ComposioError::NetworkError)?;
2080
2081            // Check for errors
2082            if !response.status().is_success() {
2083                return Err(ComposioError::from_response(response).await);
2084            }
2085
2086            Ok(response)
2087        })
2088        .await?;
2089
2090        // Parse response
2091        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2092    }
2093
2094    /// Generate tool inputs from natural language
2095    ///
2096    /// Uses AI to convert a natural language description into structured
2097    /// tool arguments. This is useful for allowing users to describe what
2098    /// they want to do in plain language.
2099    ///
2100    /// # Arguments
2101    ///
2102    /// * `params` - Input generation parameters including tool slug and text
2103    ///
2104    /// # Example
2105    ///
2106    /// ```no_run
2107    /// use composio_sdk::client::ComposioClient;
2108    /// use composio_sdk::models::tools::ToolInputGenerationParams;
2109    ///
2110    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2111    /// let client = ComposioClient::builder()
2112    ///     .api_key("your_api_key")
2113    ///     .build()?;
2114    ///
2115    /// let params = ToolInputGenerationParams {
2116    ///     tool_slug: "GITHUB_CREATE_ISSUE".to_string(),
2117    ///     text: "Create an issue about fixing the login bug in the composio repo".to_string(),
2118    ///     custom_tool_description: None,
2119    ///     custom_system_prompt: None,
2120    /// };
2121    ///
2122    /// let result = client.generate_tool_inputs(params).await?;
2123    /// if let Some(arguments) = result.arguments {
2124    ///     println!("Generated arguments: {:?}", arguments);
2125    /// }
2126    /// # Ok(())
2127    /// # }
2128    /// ```
2129    pub async fn generate_tool_inputs(
2130        &self,
2131        params: crate::models::tools::ToolInputGenerationParams,
2132    ) -> Result<crate::models::tools::ToolInputGenerationResponse, ComposioError> {
2133        let url = format!(
2134            "{}/api/v3/tools/execute/{}/input",
2135            self.config.base_url, params.tool_slug
2136        );
2137
2138        // Build request body
2139        let mut body = serde_json::json!({
2140            "text": params.text,
2141        });
2142
2143        if let Some(custom_tool_description) = params.custom_tool_description {
2144            body["custom_tool_description"] = serde_json::json!(custom_tool_description);
2145        }
2146        if let Some(custom_system_prompt) = params.custom_system_prompt {
2147            body["custom_system_prompt"] = serde_json::json!(custom_system_prompt);
2148        }
2149
2150        // Execute request with retry logic
2151        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2152            let response = self
2153                .http_client
2154                .post(&url)
2155                .header("x-api-key", &self.config.api_key)
2156                .json(&body)
2157                .send()
2158                .await
2159                .map_err(ComposioError::NetworkError)?;
2160
2161            // Check for errors
2162            if !response.status().is_success() {
2163                return Err(ComposioError::from_response(response).await);
2164            }
2165
2166            Ok(response)
2167        })
2168        .await?;
2169
2170        // Parse response
2171        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2172    }
2173
2174    // ========================================================================
2175    // Triggers Methods
2176    // ========================================================================
2177
2178    /// List trigger types
2179    ///
2180    /// Retrieves a list of available trigger types (templates) based on filters.
2181    /// Trigger types define what events can be listened for.
2182    ///
2183    /// # Arguments
2184    ///
2185    /// * `params` - Filter parameters for the query
2186    ///
2187    /// # Example
2188    ///
2189    /// ```no_run
2190    /// use composio_sdk::client::ComposioClient;
2191    /// use composio_sdk::models::triggers::TriggerTypeListParams;
2192    ///
2193    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2194    /// let client = ComposioClient::builder()
2195    ///     .api_key("your_api_key")
2196    ///     .build()?;
2197    ///
2198    /// let params = TriggerTypeListParams {
2199    ///     toolkit_slugs: Some(vec!["github".to_string()]),
2200    ///     limit: Some(20),
2201    ///     ..Default::default()
2202    /// };
2203    ///
2204    /// let triggers = client.list_trigger_types(params).await?;
2205    /// for trigger in triggers.items {
2206    ///     println!("Trigger: {} ({})", trigger.name, trigger.slug);
2207    /// }
2208    /// # Ok(())
2209    /// # }
2210    /// ```
2211    pub async fn list_trigger_types(
2212        &self,
2213        params: crate::models::triggers::TriggerTypeListParams,
2214    ) -> Result<crate::models::triggers::TriggerTypeListResponse, ComposioError> {
2215        let mut url = format!("{}/api/v3/triggers_types", self.config.base_url);
2216
2217        // Build query parameters
2218        let mut query_params = vec![];
2219
2220        if let Some(cursor) = &params.cursor {
2221            query_params.push(format!("cursor={}", cursor));
2222        }
2223        if let Some(limit) = params.limit {
2224            query_params.push(format!("limit={}", limit));
2225        }
2226        if let Some(toolkit_slugs) = &params.toolkit_slugs {
2227            query_params.push(format!("toolkit_slugs={}", toolkit_slugs.join(",")));
2228        }
2229        if let Some(toolkit_versions) = &params.toolkit_versions {
2230            query_params.push(format!("toolkit_versions={}", toolkit_versions));
2231        }
2232
2233        if !query_params.is_empty() {
2234            url.push_str("?");
2235            url.push_str(&query_params.join("&"));
2236        }
2237
2238        // Execute request with retry logic
2239        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2240            let response = self
2241                .http_client
2242                .get(&url)
2243                .header("x-api-key", &self.config.api_key)
2244                .send()
2245                .await
2246                .map_err(ComposioError::NetworkError)?;
2247
2248            // Check for errors
2249            if !response.status().is_success() {
2250                return Err(ComposioError::from_response(response).await);
2251            }
2252
2253            Ok(response)
2254        })
2255        .await?;
2256
2257        // Parse response
2258        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2259    }
2260
2261    /// Retrieve all trigger type slug enumeration values.
2262    pub async fn retrieve_trigger_type_enum(
2263        &self,
2264    ) -> Result<crate::models::triggers::TriggerTypeRetrieveEnumResponse, ComposioError> {
2265        let url = format!("{}/api/v3/triggers_types/list/enum", self.config.base_url);
2266
2267        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2268            let response = self
2269                .http_client
2270                .get(&url)
2271                .header("x-api-key", &self.config.api_key)
2272                .send()
2273                .await
2274                .map_err(ComposioError::NetworkError)?;
2275
2276            if !response.status().is_success() {
2277                return Err(ComposioError::from_response(response).await);
2278            }
2279
2280            Ok(response)
2281        })
2282        .await?;
2283
2284        response.json().await.map_err(ComposioError::NetworkError)
2285    }
2286
2287    /// Get a specific trigger type by slug
2288    ///
2289    /// Retrieves detailed information about a trigger type including
2290    /// configuration schema and payload schema.
2291    ///
2292    /// # Arguments
2293    ///
2294    /// * `slug` - The trigger type slug (e.g., "GITHUB_COMMIT_EVENT")
2295    ///
2296    /// # Example
2297    ///
2298    /// ```no_run
2299    /// use composio_sdk::client::ComposioClient;
2300    ///
2301    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2302    /// let client = ComposioClient::builder()
2303    ///     .api_key("your_api_key")
2304    ///     .build()?;
2305    ///
2306    /// let trigger = client.get_trigger_type("GITHUB_COMMIT_EVENT").await?;
2307    /// println!("Trigger: {}", trigger.name);
2308    /// println!("Type: {}", trigger.trigger_type);
2309    /// println!("Config schema: {}", trigger.config);
2310    /// # Ok(())
2311    /// # }
2312    /// ```
2313    pub async fn get_trigger_type(
2314        &self,
2315        slug: impl Into<String>,
2316    ) -> Result<crate::models::triggers::TriggerType, ComposioError> {
2317        self.get_trigger_type_with_params(
2318            slug,
2319            crate::models::triggers::TriggerTypeRetrieveParams::default(),
2320        )
2321        .await
2322    }
2323
2324    /// Get a specific trigger type by slug with optional query parameters.
2325    pub async fn get_trigger_type_with_params(
2326        &self,
2327        slug: impl Into<String>,
2328        params: crate::models::triggers::TriggerTypeRetrieveParams,
2329    ) -> Result<crate::models::triggers::TriggerType, ComposioError> {
2330        let slug = slug.into();
2331        let url = format!("{}/api/v3/triggers_types/{}", self.config.base_url, slug);
2332
2333        // Execute request with retry logic
2334        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2335            let response = self
2336                .http_client
2337                .get(&url)
2338                .header("x-api-key", &self.config.api_key)
2339                .query(&params)
2340                .send()
2341                .await
2342                .map_err(ComposioError::NetworkError)?;
2343
2344            // Check for errors
2345            if !response.status().is_success() {
2346                return Err(ComposioError::from_response(response).await);
2347            }
2348
2349            Ok(response)
2350        })
2351        .await?;
2352
2353        // Parse response
2354        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2355    }
2356
2357    /// List active trigger instances
2358    ///
2359    /// Retrieves a list of active trigger instances (listeners) based on filters.
2360    /// Trigger instances are active event listeners for specific users.
2361    ///
2362    /// # Arguments
2363    ///
2364    /// * `params` - Filter parameters for the query
2365    ///
2366    /// # Example
2367    ///
2368    /// ```no_run
2369    /// use composio_sdk::client::ComposioClient;
2370    /// use composio_sdk::models::triggers::TriggerInstanceListParams;
2371    ///
2372    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2373    /// let client = ComposioClient::builder()
2374    ///     .api_key("your_api_key")
2375    ///     .build()?;
2376    ///
2377    /// let params = TriggerInstanceListParams {
2378    ///     trigger_names: Some(vec!["GITHUB_COMMIT_EVENT".to_string()]),
2379    ///     show_disabled: Some(false),
2380    ///     ..Default::default()
2381    /// };
2382    ///
2383    /// let instances = client.list_active_triggers(params).await?;
2384    /// for instance in instances.items {
2385    ///     println!("Instance: {} for user {}", instance.trigger_name, instance.user_id.unwrap_or_default());
2386    /// }
2387    /// # Ok(())
2388    /// # }
2389    /// ```
2390    pub async fn list_active_triggers(
2391        &self,
2392        params: crate::models::triggers::TriggerInstanceListParams,
2393    ) -> Result<crate::models::triggers::TriggerInstanceListResponse, ComposioError> {
2394        let mut url = format!("{}/api/v3/trigger_instances/active", self.config.base_url);
2395
2396        // Build query parameters
2397        let mut query_params = vec![];
2398
2399        if let Some(trigger_ids) = &params.trigger_ids {
2400            query_params.push(format!("trigger_ids={}", trigger_ids.join(",")));
2401        }
2402        if let Some(trigger_names) = &params.trigger_names {
2403            query_params.push(format!("trigger_names={}", trigger_names.join(",")));
2404        }
2405        if let Some(auth_config_ids) = &params.auth_config_ids {
2406            query_params.push(format!("auth_config_ids={}", auth_config_ids.join(",")));
2407        }
2408        if let Some(connected_account_ids) = &params.connected_account_ids {
2409            query_params.push(format!(
2410                "connected_account_ids={}",
2411                connected_account_ids.join(",")
2412            ));
2413        }
2414        if let Some(show_disabled) = params.show_disabled {
2415            query_params.push(format!("show_disabled={}", show_disabled));
2416        }
2417        if let Some(limit) = params.limit {
2418            query_params.push(format!("limit={}", limit));
2419        }
2420        if let Some(cursor) = &params.cursor {
2421            query_params.push(format!("cursor={}", cursor));
2422        }
2423
2424        if !query_params.is_empty() {
2425            url.push_str("?");
2426            url.push_str(&query_params.join("&"));
2427        }
2428
2429        // Execute request with retry logic
2430        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2431            let response = self
2432                .http_client
2433                .get(&url)
2434                .header("x-api-key", &self.config.api_key)
2435                .send()
2436                .await
2437                .map_err(ComposioError::NetworkError)?;
2438
2439            // Check for errors
2440            if !response.status().is_success() {
2441                return Err(ComposioError::from_response(response).await);
2442            }
2443
2444            Ok(response)
2445        })
2446        .await?;
2447
2448        // Parse response
2449        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2450    }
2451
2452    /// Create a trigger instance
2453    ///
2454    /// Creates a new trigger instance (event listener) for a user.
2455    /// Either `connected_account_id` or `user_id` must be provided.
2456    /// If `user_id` is provided, the most recent connected account will be used.
2457    ///
2458    /// # Arguments
2459    ///
2460    /// * `params` - Trigger creation parameters
2461    ///
2462    /// # Example
2463    ///
2464    /// ```no_run
2465    /// use composio_sdk::client::ComposioClient;
2466    /// use composio_sdk::models::triggers::TriggerCreateParams;
2467    /// use std::collections::HashMap;
2468    ///
2469    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2470    /// let client = ComposioClient::builder()
2471    ///     .api_key("your_api_key")
2472    ///     .build()?;
2473    ///
2474    /// let mut config = HashMap::new();
2475    /// config.insert("repo".to_string(), serde_json::json!("composio"));
2476    /// config.insert("owner".to_string(), serde_json::json!("composio"));
2477    ///
2478    /// let params = TriggerCreateParams {
2479    ///     slug: "GITHUB_COMMIT_EVENT".to_string(),
2480    ///     user_id: Some("user_123".to_string()),
2481    ///     connected_account_id: None,
2482    ///     trigger_config: Some(config),
2483    ///     toolkit_versions: None,
2484    /// };
2485    ///
2486    /// let trigger = client.create_trigger(params).await?;
2487    /// println!("Created trigger: {}", trigger.id);
2488    /// # Ok(())
2489    /// # }
2490    /// ```
2491    pub async fn create_trigger(
2492        &self,
2493        mut params: crate::models::triggers::TriggerCreateParams,
2494    ) -> Result<crate::models::triggers::TriggerCreateResponse, ComposioError> {
2495        // If user_id is provided but not connected_account_id, find the connected account
2496        if params.user_id.is_some() && params.connected_account_id.is_none() {
2497            let user_id = params.user_id.as_ref().unwrap();
2498
2499            // Get trigger type to find toolkit
2500            let trigger_type = self.get_trigger_type(&params.slug).await?;
2501            let toolkit = trigger_type.toolkit.slug;
2502
2503            // Find connected account for this user and toolkit
2504            let account_params = crate::models::connected_accounts::ConnectedAccountListParams {
2505                user_ids: Some(vec![user_id.clone()]),
2506                toolkit_slugs: Some(vec![toolkit]),
2507                ..Default::default()
2508            };
2509
2510            let accounts = self.list_connected_accounts(account_params).await?;
2511
2512            if accounts.items.is_empty() {
2513                return Err(ComposioError::InvalidInput(format!(
2514                    "No connected accounts found for trigger {} and user {}",
2515                    params.slug, user_id
2516                )));
2517            }
2518
2519            // Use the most recent account
2520            let mut sorted_accounts = accounts.items;
2521            sorted_accounts.sort_by(|a, b| b.created_at.cmp(&a.created_at));
2522            params.connected_account_id = Some(sorted_accounts[0].id.clone());
2523        }
2524
2525        if params.connected_account_id.is_none() {
2526            return Err(ComposioError::InvalidInput(
2527                "Either connected_account_id or user_id must be provided".to_string(),
2528            ));
2529        }
2530
2531        let url = format!(
2532            "{}/api/v3/trigger_instances/{}/upsert",
2533            self.config.base_url, params.slug
2534        );
2535
2536        // Build request body
2537        let mut body = serde_json::json!({
2538            "connected_account_id": params.connected_account_id.unwrap(),
2539        });
2540
2541        if let Some(trigger_config) = params.trigger_config {
2542            body["trigger_config"] = serde_json::to_value(trigger_config)
2543                .map_err(|e| ComposioError::InvalidInput(e.to_string()))?;
2544        }
2545        if let Some(toolkit_versions) = params.toolkit_versions {
2546            body["toolkit_versions"] = serde_json::json!(toolkit_versions);
2547        }
2548
2549        // Execute request with retry logic
2550        let response = crate::retry::with_retry(&self.config.retry_policy, || async {
2551            let response = self
2552                .http_client
2553                .post(&url)
2554                .header("x-api-key", &self.config.api_key)
2555                .json(&body)
2556                .send()
2557                .await
2558                .map_err(ComposioError::NetworkError)?;
2559
2560            // Check for errors
2561            if !response.status().is_success() {
2562                return Err(ComposioError::from_response(response).await);
2563            }
2564
2565            Ok(response)
2566        })
2567        .await?;
2568
2569        // Parse response
2570        Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2571    }
2572
2573    /// Delete a trigger instance
2574    ///
2575    /// Permanently deletes a trigger instance. This cannot be undone.
2576    ///
2577    /// # Arguments
2578    ///
2579    /// * `trigger_id` - The trigger instance ID to delete
2580    ///
2581    /// # Example
2582    ///
2583    /// ```no_run
2584    /// use composio_sdk::client::ComposioClient;
2585    ///
2586    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2587    /// let client = ComposioClient::builder()
2588    ///     .api_key("your_api_key")
2589    ///     .build()?;
2590    ///
2591    /// client.delete_trigger("ti_abc123").await?;
2592    /// println!("Trigger deleted");
2593    /// # Ok(())
2594    /// # }
2595    /// ```
2596    pub async fn delete_trigger(&self, trigger_id: impl Into<String>) -> Result<(), ComposioError> {
2597        let trigger_id = trigger_id.into();
2598        let url = format!(
2599            "{}/api/v3/trigger_instances/manage/{}",
2600            self.config.base_url, trigger_id
2601        );
2602
2603        // Execute request with retry logic
2604        crate::retry::with_retry(&self.config.retry_policy, || async {
2605            let response = self
2606                .http_client
2607                .delete(&url)
2608                .header("x-api-key", &self.config.api_key)
2609                .send()
2610                .await
2611                .map_err(ComposioError::NetworkError)?;
2612
2613            // Check for errors
2614            if !response.status().is_success() {
2615                return Err(ComposioError::from_response(response).await);
2616            }
2617
2618            Ok(response)
2619        })
2620        .await?;
2621
2622        Ok(())
2623    }
2624
2625    /// Enable a trigger instance
2626    ///
2627    /// Enables a previously disabled trigger instance.
2628    ///
2629    /// # Arguments
2630    ///
2631    /// * `trigger_id` - The trigger instance ID to enable
2632    ///
2633    /// # Example
2634    ///
2635    /// ```no_run
2636    /// use composio_sdk::client::ComposioClient;
2637    ///
2638    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2639    /// let client = ComposioClient::builder()
2640    ///     .api_key("your_api_key")
2641    ///     .build()?;
2642    ///
2643    /// client.enable_trigger("ti_abc123").await?;
2644    /// println!("Trigger enabled");
2645    /// # Ok(())
2646    /// # }
2647    /// ```
2648    pub async fn enable_trigger(&self, trigger_id: impl Into<String>) -> Result<(), ComposioError> {
2649        let trigger_id = trigger_id.into();
2650        let url = format!(
2651            "{}/api/v3/trigger_instances/manage/{}",
2652            self.config.base_url, trigger_id
2653        );
2654
2655        let body = serde_json::json!({
2656            "status": "enable"
2657        });
2658
2659        // Execute request with retry logic
2660        crate::retry::with_retry(&self.config.retry_policy, || async {
2661            let response = self
2662                .http_client
2663                .patch(&url)
2664                .header("x-api-key", &self.config.api_key)
2665                .json(&body)
2666                .send()
2667                .await
2668                .map_err(ComposioError::NetworkError)?;
2669
2670            // Check for errors
2671            if !response.status().is_success() {
2672                return Err(ComposioError::from_response(response).await);
2673            }
2674
2675            Ok(response)
2676        })
2677        .await?;
2678
2679        Ok(())
2680    }
2681
2682    /// Disable a trigger instance
2683    ///
2684    /// Temporarily disables a trigger instance without deleting it.
2685    ///
2686    /// # Arguments
2687    ///
2688    /// * `trigger_id` - The trigger instance ID to disable
2689    ///
2690    /// # Example
2691    ///
2692    /// ```no_run
2693    /// use composio_sdk::client::ComposioClient;
2694    ///
2695    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2696    /// let client = ComposioClient::builder()
2697    ///     .api_key("your_api_key")
2698    ///     .build()?;
2699    ///
2700    /// client.disable_trigger("ti_abc123").await?;
2701    /// println!("Trigger disabled");
2702    /// # Ok(())
2703    /// # }
2704    /// ```
2705    pub async fn disable_trigger(
2706        &self,
2707        trigger_id: impl Into<String>,
2708    ) -> Result<(), ComposioError> {
2709        let trigger_id = trigger_id.into();
2710        let url = format!(
2711            "{}/api/v3/trigger_instances/manage/{}",
2712            self.config.base_url, trigger_id
2713        );
2714
2715        let body = serde_json::json!({
2716            "status": "disable"
2717        });
2718
2719        // Execute request with retry logic
2720        crate::retry::with_retry(&self.config.retry_policy, || async {
2721            let response = self
2722                .http_client
2723                .patch(&url)
2724                .header("x-api-key", &self.config.api_key)
2725                .json(&body)
2726                .send()
2727                .await
2728                .map_err(ComposioError::NetworkError)?;
2729
2730            // Check for errors
2731            if !response.status().is_success() {
2732                return Err(ComposioError::from_response(response).await);
2733            }
2734
2735            Ok(response)
2736        })
2737        .await?;
2738
2739        Ok(())
2740    }
2741
2742    /// Verify an incoming webhook payload and signature
2743    ///
2744    /// This method validates that the webhook request is authentic by:
2745    /// 1. Validating the webhook timestamp is within the tolerance window
2746    /// 2. Verifying the HMAC-SHA256 signature using the correct algorithm
2747    /// 3. Parsing the payload and detecting the webhook version (V1, V2, or V3)
2748    ///
2749    /// # Arguments
2750    ///
2751    /// * `params` - Webhook verification parameters including id, payload, signature, timestamp, and secret
2752    ///
2753    /// # Returns
2754    ///
2755    /// Returns a `VerifyWebhookResult` containing:
2756    /// - `version`: Detected webhook version (V1, V2, or V3)
2757    /// - `payload`: Normalized trigger event
2758    /// - `raw_payload`: Original parsed payload
2759    ///
2760    /// # Errors
2761    ///
2762    /// Returns an error if:
2763    /// - Signature verification fails
2764    /// - Timestamp is outside tolerance window
2765    /// - Payload cannot be parsed
2766    /// - Required headers are missing
2767    ///
2768    /// # Example
2769    ///
2770    /// ```no_run
2771    /// use composio_sdk::client::ComposioClient;
2772    /// use composio_sdk::models::triggers::WebhookVerifyParams;
2773    ///
2774    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
2775    /// let client = ComposioClient::builder()
2776    ///     .api_key("your_api_key")
2777    ///     .build()?;
2778    ///
2779    /// // In a webhook handler (e.g., Actix-web, Axum, etc.)
2780    /// let params = WebhookVerifyParams {
2781    ///     id: "msg_abc123".to_string(),
2782    ///     payload: r#"{"type":"composio.trigger.message","data":{}}"#.to_string(),
2783    ///     signature: "v1,base64signature".to_string(),
2784    ///     timestamp: "1234567890".to_string(),
2785    ///     secret: "whsec_abc123".to_string(),
2786    ///     tolerance: Some(300),
2787    /// };
2788    ///
2789    /// let result = client.verify_webhook(params)?;
2790    /// println!("Webhook version: {:?}", result.version);
2791    /// println!("Trigger slug: {}", result.payload.trigger_slug);
2792    /// # Ok(())
2793    /// # }
2794    /// ```
2795    pub fn verify_webhook(
2796        &self,
2797        params: crate::models::triggers::WebhookVerifyParams,
2798    ) -> Result<crate::models::triggers::VerifyWebhookResult, ComposioError> {
2799        use base64::{engine::general_purpose, Engine as _};
2800        use std::time::{SystemTime, UNIX_EPOCH};
2801
2802        let tolerance = params.tolerance.unwrap_or(300);
2803
2804        // Validate timestamp if tolerance is set
2805        if tolerance > 0 {
2806            let timestamp_seconds: i64 = params.timestamp.parse().map_err(|_| {
2807                ComposioError::InvalidInput(format!(
2808                    "Invalid webhook timestamp: {}. Expected Unix timestamp in seconds.",
2809                    params.timestamp
2810                ))
2811            })?;
2812
2813            let current_time = SystemTime::now()
2814                .duration_since(UNIX_EPOCH)
2815                .map_err(|e| ComposioError::InvalidInput(format!("System time error: {}", e)))?
2816                .as_secs() as i64;
2817
2818            let time_difference = (current_time - timestamp_seconds).abs();
2819
2820            if time_difference > tolerance as i64 {
2821                return Err(ComposioError::InvalidInput(
2822                    format!(
2823                        "The webhook timestamp is outside the allowed tolerance. \
2824                        The webhook was sent {} seconds ago, but the maximum allowed age is {} seconds.",
2825                        time_difference, tolerance
2826                    )
2827                ));
2828            }
2829        }
2830
2831        // Verify signature
2832        if params.payload.is_empty() {
2833            return Err(ComposioError::InvalidInput(
2834                "No webhook payload was provided.".to_string(),
2835            ));
2836        }
2837
2838        if params.signature.is_empty() {
2839            return Err(ComposioError::InvalidInput(
2840                "No signature header value was provided. \
2841                Please pass the value of the webhook signature header."
2842                    .to_string(),
2843            ));
2844        }
2845
2846        if params.secret.is_empty() {
2847            return Err(ComposioError::InvalidInput(
2848                "No webhook secret was provided. \
2849                You can find your webhook secret in your Composio dashboard."
2850                    .to_string(),
2851            ));
2852        }
2853
2854        if params.id.is_empty() {
2855            return Err(ComposioError::InvalidInput(
2856                "No webhook ID was provided. \
2857                Please pass the value of the 'webhook-id' header."
2858                    .to_string(),
2859            ));
2860        }
2861
2862        if params.timestamp.is_empty() {
2863            return Err(ComposioError::InvalidInput(
2864                "No webhook timestamp was provided. \
2865                Please pass the value of the 'webhook-timestamp' header."
2866                    .to_string(),
2867            ));
2868        }
2869
2870        // Parse signature header - format is "v1,base64Sig" or "v1,sig1 v1,sig2"
2871        let signature_parts: Vec<&str> = params.signature.split(' ').collect();
2872        let mut v1_signatures: Vec<&str> = Vec::new();
2873
2874        for part in signature_parts {
2875            if part.starts_with("v1,") {
2876                v1_signatures.push(&part[3..]); // Remove "v1," prefix
2877            }
2878        }
2879
2880        if v1_signatures.is_empty() {
2881            return Err(ComposioError::InvalidInput(
2882                "No valid v1 signature found in the signature header. \
2883                Expected format: 'v1,base64EncodedSignature'"
2884                    .to_string(),
2885            ));
2886        }
2887
2888        // Construct the string to sign: webhookId.webhookTimestamp.payload
2889        let to_sign = format!("{}.{}.{}", params.id, params.timestamp, params.payload);
2890
2891        // Compute expected signature using HMAC-SHA256
2892        use hmac::{Hmac, Mac};
2893        use sha2::Sha256;
2894        type HmacSha256 = Hmac<Sha256>;
2895
2896        let mut mac = HmacSha256::new_from_slice(params.secret.as_bytes())
2897            .map_err(|e| ComposioError::InvalidInput(format!("Invalid secret key: {}", e)))?;
2898        mac.update(to_sign.as_bytes());
2899        let expected_signature_bytes = mac.finalize().into_bytes();
2900        let expected_signature_b64 = general_purpose::STANDARD.encode(&expected_signature_bytes);
2901
2902        // Check if any of the provided signatures match (timing-safe)
2903        let mut signature_valid = false;
2904        for provided_sig in v1_signatures {
2905            // Use constant-time comparison
2906            if expected_signature_b64.len() == provided_sig.len() {
2907                let mut matches = true;
2908                for (a, b) in expected_signature_b64.bytes().zip(provided_sig.bytes()) {
2909                    if a != b {
2910                        matches = false;
2911                    }
2912                }
2913                if matches {
2914                    signature_valid = true;
2915                    break;
2916                }
2917            }
2918        }
2919
2920        if !signature_valid {
2921            return Err(ComposioError::InvalidInput(
2922                "The signature provided is invalid.".to_string(),
2923            ));
2924        }
2925
2926        // Parse and detect version
2927        let raw_payload: serde_json::Value =
2928            serde_json::from_str(&params.payload).map_err(|e| {
2929                ComposioError::InvalidInput(format!(
2930                    "Failed to parse webhook payload as JSON: {}",
2931                    e
2932                ))
2933            })?;
2934
2935        // Detect version and normalize payload
2936        let (version, normalized_payload) = self.parse_webhook_payload(&raw_payload)?;
2937
2938        Ok(crate::models::triggers::VerifyWebhookResult {
2939            version,
2940            payload: normalized_payload,
2941            raw_payload,
2942        })
2943    }
2944
2945    /// Parse webhook payload and detect version (internal helper)
2946    fn parse_webhook_payload(
2947        &self,
2948        data: &serde_json::Value,
2949    ) -> Result<
2950        (
2951            crate::models::triggers::WebhookVersion,
2952            crate::models::triggers::TriggerEvent,
2953        ),
2954        ComposioError,
2955    > {
2956        use crate::models::triggers::WebhookVersion;
2957
2958        // Try V3 first (has 'type' starting with 'composio.' and 'metadata' as dict)
2959        if let Some(obj) = data.as_object() {
2960            if let Some(event_type) = obj.get("type").and_then(|v| v.as_str()) {
2961                if event_type.starts_with("composio.")
2962                    && obj.contains_key("metadata")
2963                    && obj.get("metadata").and_then(|v| v.as_object()).is_some()
2964                    && obj.contains_key("id")
2965                    && obj.contains_key("data")
2966                {
2967                    return Ok((WebhookVersion::V3, self.normalize_v3_payload(data)?));
2968                }
2969            }
2970
2971            // Try V2 (has 'type', 'timestamp', 'data' with nested fields)
2972            if obj.contains_key("type") && obj.contains_key("timestamp") && obj.contains_key("data")
2973            {
2974                if let Some(data_obj) = obj.get("data").and_then(|v| v.as_object()) {
2975                    if data_obj.contains_key("connection_id") {
2976                        return Ok((WebhookVersion::V2, self.normalize_v2_payload(data)?));
2977                    }
2978                }
2979            }
2980
2981            // Try V1 (has 'trigger_name', 'connection_id', 'trigger_id', 'payload')
2982            if obj.contains_key("trigger_name")
2983                && obj.contains_key("connection_id")
2984                && obj.contains_key("trigger_id")
2985                && obj.contains_key("payload")
2986            {
2987                return Ok((WebhookVersion::V1, self.normalize_v1_payload(data)?));
2988            }
2989        }
2990
2991        Err(ComposioError::InvalidInput(
2992            "Webhook payload does not match any known version (V1, V2, or V3). \
2993            Please ensure the payload structure is correct."
2994                .to_string(),
2995        ))
2996    }
2997
2998    /// Normalize V1 payload to TriggerEvent format (internal helper)
2999    fn normalize_v1_payload(
3000        &self,
3001        data: &serde_json::Value,
3002    ) -> Result<crate::models::triggers::TriggerEvent, ComposioError> {
3003        use crate::models::triggers::{TriggerConnectedAccount, TriggerEvent, TriggerMetadata};
3004
3005        let obj = data.as_object().ok_or_else(|| {
3006            ComposioError::InvalidInput("V1 payload must be an object".to_string())
3007        })?;
3008
3009        let trigger_id = obj
3010            .get("trigger_id")
3011            .and_then(|v| v.as_str())
3012            .unwrap_or("")
3013            .to_string();
3014        let trigger_name = obj
3015            .get("trigger_name")
3016            .and_then(|v| v.as_str())
3017            .unwrap_or("")
3018            .to_string();
3019        let connection_id = obj
3020            .get("connection_id")
3021            .and_then(|v| v.as_str())
3022            .unwrap_or("")
3023            .to_string();
3024        let payload = obj.get("payload").cloned();
3025
3026        Ok(TriggerEvent {
3027            id: trigger_id.clone(),
3028            uuid: trigger_id.clone(),
3029            user_id: String::new(),      // V1 doesn't have user_id
3030            toolkit_slug: String::new(), // V1 doesn't have toolkit_slug
3031            trigger_slug: trigger_name.clone(),
3032            metadata: TriggerMetadata {
3033                id: trigger_id.clone(),
3034                uuid: trigger_id.clone(),
3035                toolkit_slug: String::new(),
3036                trigger_slug: trigger_name,
3037                trigger_data: None,
3038                trigger_config: serde_json::json!({}),
3039                connected_account: TriggerConnectedAccount {
3040                    id: connection_id.clone(),
3041                    uuid: connection_id,
3042                    auth_config_id: String::new(),
3043                    auth_config_uuid: String::new(),
3044                    user_id: String::new(),
3045                    status: "ACTIVE".to_string(),
3046                },
3047            },
3048            payload,
3049            original_payload: None,
3050        })
3051    }
3052
3053    /// Normalize V2 payload to TriggerEvent format (internal helper)
3054    fn normalize_v2_payload(
3055        &self,
3056        data: &serde_json::Value,
3057    ) -> Result<crate::models::triggers::TriggerEvent, ComposioError> {
3058        use crate::models::triggers::{TriggerConnectedAccount, TriggerEvent, TriggerMetadata};
3059
3060        let obj = data.as_object().ok_or_else(|| {
3061            ComposioError::InvalidInput("V2 payload must be an object".to_string())
3062        })?;
3063
3064        let event_type = obj
3065            .get("type")
3066            .and_then(|v| v.as_str())
3067            .unwrap_or("")
3068            .to_uppercase();
3069
3070        let payload_data = obj.get("data").and_then(|v| v.as_object()).ok_or_else(|| {
3071            ComposioError::InvalidInput("V2 payload missing 'data' object".to_string())
3072        })?;
3073
3074        let trigger_id = payload_data
3075            .get("trigger_id")
3076            .and_then(|v| v.as_str())
3077            .unwrap_or("")
3078            .to_string();
3079        let trigger_nano_id = payload_data
3080            .get("trigger_nano_id")
3081            .and_then(|v| v.as_str())
3082            .unwrap_or(&trigger_id)
3083            .to_string();
3084        let user_id = payload_data
3085            .get("user_id")
3086            .and_then(|v| v.as_str())
3087            .unwrap_or("")
3088            .to_string();
3089        let connection_id = payload_data
3090            .get("connection_id")
3091            .and_then(|v| v.as_str())
3092            .unwrap_or("")
3093            .to_string();
3094        let connection_nano_id = payload_data
3095            .get("connection_nano_id")
3096            .and_then(|v| v.as_str())
3097            .unwrap_or(&connection_id)
3098            .to_string();
3099
3100        // Extract payload fields, excluding metadata fields
3101        let excluded_keys = [
3102            "connection_id",
3103            "connection_nano_id",
3104            "trigger_nano_id",
3105            "trigger_id",
3106            "user_id",
3107        ];
3108        let mut filtered_payload = serde_json::Map::new();
3109        for (k, v) in payload_data.iter() {
3110            if !excluded_keys.contains(&k.as_str()) {
3111                filtered_payload.insert(k.clone(), v.clone());
3112            }
3113        }
3114
3115        Ok(TriggerEvent {
3116            id: trigger_nano_id.clone(),
3117            uuid: trigger_id.clone(),
3118            user_id: user_id.clone(),
3119            toolkit_slug: event_type.clone(),
3120            trigger_slug: event_type.clone(),
3121            metadata: TriggerMetadata {
3122                id: trigger_nano_id,
3123                uuid: trigger_id,
3124                toolkit_slug: event_type.clone(),
3125                trigger_slug: event_type,
3126                trigger_data: None,
3127                trigger_config: serde_json::json!({}),
3128                connected_account: TriggerConnectedAccount {
3129                    id: connection_nano_id,
3130                    uuid: connection_id,
3131                    auth_config_id: String::new(),
3132                    auth_config_uuid: String::new(),
3133                    user_id,
3134                    status: "ACTIVE".to_string(),
3135                },
3136            },
3137            payload: Some(serde_json::Value::Object(filtered_payload)),
3138            original_payload: None,
3139        })
3140    }
3141
3142    /// Normalize V3 payload to TriggerEvent format (internal helper)
3143    fn normalize_v3_payload(
3144        &self,
3145        data: &serde_json::Value,
3146    ) -> Result<crate::models::triggers::TriggerEvent, ComposioError> {
3147        use crate::models::triggers::{TriggerConnectedAccount, TriggerEvent, TriggerMetadata};
3148
3149        let obj = data.as_object().ok_or_else(|| {
3150            ComposioError::InvalidInput("V3 payload must be an object".to_string())
3151        })?;
3152
3153        let metadata = obj
3154            .get("metadata")
3155            .and_then(|v| v.as_object())
3156            .ok_or_else(|| {
3157                ComposioError::InvalidInput("V3 payload missing 'metadata' object".to_string())
3158            })?;
3159
3160        // Check if this is a trigger event (has trigger-specific metadata fields)
3161        let is_trigger_event = metadata.contains_key("trigger_id")
3162            && metadata.contains_key("trigger_slug")
3163            && metadata.contains_key("user_id")
3164            && metadata.contains_key("connected_account_id")
3165            && metadata.contains_key("auth_config_id")
3166            && metadata.contains_key("log_id");
3167
3168        if is_trigger_event {
3169            let trigger_id = metadata
3170                .get("trigger_id")
3171                .and_then(|v| v.as_str())
3172                .unwrap_or("")
3173                .to_string();
3174            let trigger_slug = metadata
3175                .get("trigger_slug")
3176                .and_then(|v| v.as_str())
3177                .unwrap_or("")
3178                .to_string();
3179            let user_id = metadata
3180                .get("user_id")
3181                .and_then(|v| v.as_str())
3182                .unwrap_or("")
3183                .to_string();
3184            let connected_account_id = metadata
3185                .get("connected_account_id")
3186                .and_then(|v| v.as_str())
3187                .unwrap_or("")
3188                .to_string();
3189            let auth_config_id = metadata
3190                .get("auth_config_id")
3191                .and_then(|v| v.as_str())
3192                .unwrap_or("")
3193                .to_string();
3194
3195            // Extract toolkit slug from trigger slug (e.g., "GITHUB_COMMIT_EVENT" -> "GITHUB")
3196            let toolkit_slug = if trigger_slug.contains('_') {
3197                trigger_slug
3198                    .split('_')
3199                    .next()
3200                    .unwrap_or("UNKNOWN")
3201                    .to_uppercase()
3202            } else {
3203                "UNKNOWN".to_string()
3204            };
3205
3206            let event_data = obj.get("data").cloned();
3207
3208            Ok(TriggerEvent {
3209                id: trigger_id.clone(),
3210                uuid: trigger_id.clone(),
3211                user_id: user_id.clone(),
3212                toolkit_slug: toolkit_slug.clone(),
3213                trigger_slug: trigger_slug.clone(),
3214                metadata: TriggerMetadata {
3215                    id: trigger_id.clone(),
3216                    uuid: trigger_id,
3217                    toolkit_slug,
3218                    trigger_slug,
3219                    trigger_data: None,
3220                    trigger_config: serde_json::json!({}),
3221                    connected_account: TriggerConnectedAccount {
3222                        id: connected_account_id.clone(),
3223                        uuid: connected_account_id.clone(),
3224                        auth_config_id: auth_config_id.clone(),
3225                        auth_config_uuid: auth_config_id,
3226                        user_id,
3227                        status: "ACTIVE".to_string(),
3228                    },
3229                },
3230                payload: event_data,
3231                original_payload: None,
3232            })
3233        } else {
3234            // Non-trigger V3 event (e.g., connection expired)
3235            let event_type = obj
3236                .get("type")
3237                .and_then(|v| v.as_str())
3238                .unwrap_or("")
3239                .to_string();
3240            let event_id = obj
3241                .get("id")
3242                .and_then(|v| v.as_str())
3243                .unwrap_or("")
3244                .to_string();
3245
3246            Ok(TriggerEvent {
3247                id: event_id.clone(),
3248                uuid: event_id.clone(),
3249                user_id: String::new(),
3250                toolkit_slug: "COMPOSIO".to_string(),
3251                trigger_slug: event_type.clone(),
3252                metadata: TriggerMetadata {
3253                    id: event_id.clone(),
3254                    uuid: event_id,
3255                    toolkit_slug: "COMPOSIO".to_string(),
3256                    trigger_slug: event_type,
3257                    trigger_data: None,
3258                    trigger_config: serde_json::json!({}),
3259                    connected_account: TriggerConnectedAccount {
3260                        id: String::new(),
3261                        uuid: String::new(),
3262                        auth_config_id: String::new(),
3263                        auth_config_uuid: String::new(),
3264                        user_id: String::new(),
3265                        status: "ACTIVE".to_string(),
3266                    },
3267                },
3268                payload: obj.get("data").cloned(),
3269                original_payload: None,
3270            })
3271        }
3272    }
3273}
3274
3275impl ComposioClientBuilder {
3276    /// Set the API key
3277    ///
3278    /// The API key is required for authenticating with the Composio API.
3279    /// You can obtain your API key from the Composio dashboard.
3280    ///
3281    /// # Arguments
3282    ///
3283    /// * `key` - The Composio API key (can be `String` or `&str`)
3284    ///
3285    /// # Example
3286    ///
3287    /// ```no_run
3288    /// use composio_sdk::client::ComposioClient;
3289    ///
3290    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3291    /// let client = ComposioClient::builder()
3292    ///     .api_key("your_api_key")
3293    ///     .build()?;
3294    /// # Ok(())
3295    /// # }
3296    /// ```
3297    pub fn api_key(mut self, key: impl Into<String>) -> Self {
3298        self.api_key = Some(key.into());
3299        self
3300    }
3301
3302    /// Set the base URL
3303    ///
3304    /// Override the default Composio API base URL. This is useful for testing
3305    /// or when using a custom Composio deployment.
3306    ///
3307    /// # Arguments
3308    ///
3309    /// * `url` - The base URL (must start with http:// or https://)
3310    ///
3311    /// # Default
3312    ///
3313    /// `https://backend.composio.dev/api/v3`
3314    ///
3315    /// # Example
3316    ///
3317    /// ```no_run
3318    /// use composio_sdk::client::ComposioClient;
3319    ///
3320    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3321    /// let client = ComposioClient::builder()
3322    ///     .api_key("your_api_key")
3323    ///     .base_url("https://custom.api.com")
3324    ///     .build()?;
3325    /// # Ok(())
3326    /// # }
3327    /// ```
3328    pub fn base_url(mut self, url: impl Into<String>) -> Self {
3329        self.base_url = Some(url.into());
3330        self
3331    }
3332
3333    /// Set the request timeout
3334    ///
3335    /// Configure how long to wait for API requests to complete before timing out.
3336    ///
3337    /// # Arguments
3338    ///
3339    /// * `timeout` - The timeout duration
3340    ///
3341    /// # Default
3342    ///
3343    /// 30 seconds
3344    ///
3345    /// # Example
3346    ///
3347    /// ```no_run
3348    /// use composio_sdk::client::ComposioClient;
3349    /// use std::time::Duration;
3350    ///
3351    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3352    /// let client = ComposioClient::builder()
3353    ///     .api_key("your_api_key")
3354    ///     .timeout(Duration::from_secs(60))
3355    ///     .build()?;
3356    /// # Ok(())
3357    /// # }
3358    /// ```
3359    pub fn timeout(mut self, timeout: Duration) -> Self {
3360        self.timeout = Some(timeout);
3361        self
3362    }
3363
3364    /// Set the maximum number of retries
3365    ///
3366    /// Configure how many times to retry failed requests for transient errors
3367    /// (rate limits, server errors, network issues).
3368    ///
3369    /// # Arguments
3370    ///
3371    /// * `retries` - Maximum number of retry attempts
3372    ///
3373    /// # Default
3374    ///
3375    /// 3 retries
3376    ///
3377    /// # Example
3378    ///
3379    /// ```no_run
3380    /// use composio_sdk::client::ComposioClient;
3381    ///
3382    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3383    /// let client = ComposioClient::builder()
3384    ///     .api_key("your_api_key")
3385    ///     .max_retries(5)
3386    ///     .build()?;
3387    /// # Ok(())
3388    /// # }
3389    /// ```
3390    pub fn max_retries(mut self, retries: u32) -> Self {
3391        self.max_retries = Some(retries);
3392        self
3393    }
3394
3395    /// Set the initial retry delay
3396    ///
3397    /// Configure the delay before the first retry attempt. Subsequent retries
3398    /// use exponential backoff based on this initial delay.
3399    ///
3400    /// # Arguments
3401    ///
3402    /// * `delay` - Initial delay duration
3403    ///
3404    /// # Default
3405    ///
3406    /// 1 second
3407    ///
3408    /// # Example
3409    ///
3410    /// ```no_run
3411    /// use composio_sdk::client::ComposioClient;
3412    /// use std::time::Duration;
3413    ///
3414    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3415    /// let client = ComposioClient::builder()
3416    ///     .api_key("your_api_key")
3417    ///     .initial_retry_delay(Duration::from_secs(2))
3418    ///     .build()?;
3419    /// # Ok(())
3420    /// # }
3421    /// ```
3422    pub fn initial_retry_delay(mut self, delay: Duration) -> Self {
3423        self.initial_retry_delay = Some(delay);
3424        self
3425    }
3426
3427    /// Set the maximum retry delay
3428    ///
3429    /// Configure the maximum delay between retry attempts. This caps the
3430    /// exponential backoff to prevent excessively long waits.
3431    ///
3432    /// # Arguments
3433    ///
3434    /// * `delay` - Maximum delay duration
3435    ///
3436    /// # Default
3437    ///
3438    /// 10 seconds
3439    ///
3440    /// # Example
3441    ///
3442    /// ```no_run
3443    /// use composio_sdk::client::ComposioClient;
3444    /// use std::time::Duration;
3445    ///
3446    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3447    /// let client = ComposioClient::builder()
3448    ///     .api_key("your_api_key")
3449    ///     .max_retry_delay(Duration::from_secs(30))
3450    ///     .build()?;
3451    /// # Ok(())
3452    /// # }
3453    /// ```
3454    pub fn max_retry_delay(mut self, delay: Duration) -> Self {
3455        self.max_retry_delay = Some(delay);
3456        self
3457    }
3458
3459    /// Set toolkit version configuration
3460    ///
3461    /// Configure which versions of toolkits to use. This allows you to:
3462    /// - Use "latest" for all toolkits (default behavior)
3463    /// - Specify different versions for different toolkits
3464    /// - Pin specific toolkits to specific versions for stability
3465    ///
3466    /// Version resolution follows this precedence:
3467    /// 1. `COMPOSIO_TOOLKIT_VERSION_{TOOLKIT}` environment variable (highest priority)
3468    /// 2. User-provided configuration (this method)
3469    /// 3. `COMPOSIO_TOOLKIT_VERSION` global environment variable
3470    /// 4. Default to "latest"
3471    ///
3472    /// # Arguments
3473    ///
3474    /// * `versions` - Toolkit version configuration
3475    ///
3476    /// # Default
3477    ///
3478    /// None (uses "latest" for all toolkits)
3479    ///
3480    /// # Example
3481    ///
3482    /// ```no_run
3483    /// use composio_sdk::client::ComposioClient;
3484    /// use composio_sdk::models::versioning::{ToolkitVersion, ToolkitVersionParam};
3485    /// use std::collections::HashMap;
3486    ///
3487    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3488    /// // Use latest for all toolkits
3489    /// let client = ComposioClient::builder()
3490    ///     .api_key("your_api_key")
3491    ///     .toolkit_versions(ToolkitVersionParam::Latest)
3492    ///     .build()?;
3493    ///
3494    /// // Use specific versions for specific toolkits
3495    /// let mut versions = HashMap::new();
3496    /// versions.insert("github".to_string(), ToolkitVersion::Specific("20250906_01".to_string()));
3497    /// versions.insert("gmail".to_string(), ToolkitVersion::Latest);
3498    ///
3499    /// let client = ComposioClient::builder()
3500    ///     .api_key("your_api_key")
3501    ///     .toolkit_versions(ToolkitVersionParam::Versions(versions))
3502    ///     .build()?;
3503    /// # Ok(())
3504    /// # }
3505    /// ```
3506    pub fn toolkit_versions(
3507        mut self,
3508        versions: crate::models::versioning::ToolkitVersionParam,
3509    ) -> Self {
3510        self.toolkit_versions = Some(versions);
3511        self
3512    }
3513
3514    /// Set the file download directory
3515    ///
3516    /// Configure the directory where downloaded files will be saved.
3517    /// If not set, files will be downloaded to the current working directory.
3518    ///
3519    /// # Arguments
3520    ///
3521    /// * `dir` - Path to the download directory
3522    ///
3523    /// # Example
3524    ///
3525    /// ```no_run
3526    /// use composio_sdk::ComposioClient;
3527    /// use std::path::PathBuf;
3528    ///
3529    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3530    /// let client = ComposioClient::builder()
3531    ///     .api_key("your_api_key")
3532    ///     .file_download_dir(PathBuf::from("./downloads"))
3533    ///     .build()?;
3534    /// # Ok(())
3535    /// # }
3536    /// ```
3537    pub fn file_download_dir(mut self, dir: impl Into<std::path::PathBuf>) -> Self {
3538        self.file_download_dir = Some(dir.into());
3539        self
3540    }
3541
3542    /// Enable or disable automatic file upload/download
3543    ///
3544    /// When enabled (default), the SDK will automatically:
3545    /// - Upload local file paths to S3 before tool execution
3546    /// - Download file URLs returned by tools to local paths
3547    ///
3548    /// # Arguments
3549    ///
3550    /// * `enabled` - Whether to enable automatic file handling
3551    ///
3552    /// # Default
3553    ///
3554    /// true
3555    ///
3556    /// # Example
3557    ///
3558    /// ```no_run
3559    /// use composio_sdk::ComposioClient;
3560    ///
3561    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3562    /// let client = ComposioClient::builder()
3563    ///     .api_key("your_api_key")
3564    ///     .auto_upload_download_files(false)
3565    ///     .build()?;
3566    /// # Ok(())
3567    /// # }
3568    /// ```
3569    pub fn auto_upload_download_files(mut self, enabled: bool) -> Self {
3570        self.auto_upload_download_files = Some(enabled);
3571        self
3572    }
3573
3574    /// Enable or disable telemetry tracking
3575    ///
3576    /// When enabled, the SDK will send anonymous usage telemetry to Composio.
3577    /// This helps improve the SDK but is disabled by default for privacy.
3578    ///
3579    /// Telemetry is sent asynchronously and does not block operations.
3580    ///
3581    /// # Arguments
3582    ///
3583    /// * `enabled` - Whether to enable telemetry
3584    ///
3585    /// # Default
3586    ///
3587    /// false (opt-in for privacy)
3588    ///
3589    /// # Example
3590    ///
3591    /// ```no_run
3592    /// use composio_sdk::ComposioClient;
3593    ///
3594    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3595    /// let client = ComposioClient::builder()
3596    ///     .api_key("your_api_key")
3597    ///     .telemetry_enabled(true)
3598    ///     .build()?;
3599    /// # Ok(())
3600    /// # }
3601    /// ```
3602    pub fn telemetry_enabled(mut self, enabled: bool) -> Self {
3603        self.telemetry_enabled = Some(enabled);
3604        self
3605    }
3606
3607    /// Build the client
3608    ///
3609    /// Validates the configuration and constructs a `ComposioClient` instance.
3610    /// The reqwest HTTP client is configured with the specified timeout and
3611    /// default headers (including the API key).
3612    ///
3613    /// # Errors
3614    ///
3615    /// Returns an error if:
3616    /// - API key is not provided or is empty
3617    /// - Base URL is invalid (doesn't start with http:// or https://)
3618    /// - HTTP client construction fails
3619    ///
3620    /// # Example
3621    ///
3622    /// ```no_run
3623    /// use composio_sdk::client::ComposioClient;
3624    ///
3625    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
3626    /// let client = ComposioClient::builder()
3627    ///     .api_key("your_api_key")
3628    ///     .build()?;
3629    /// # Ok(())
3630    /// # }
3631    /// ```
3632    pub fn build(self) -> Result<ComposioClient, ComposioError> {
3633        // Try explicit API key first, then environment variable
3634        let api_key = self
3635            .api_key
3636            .or_else(|| std::env::var("COMPOSIO_API_KEY").ok())
3637            .ok_or_else(|| {
3638                ComposioError::ConfigError(
3639                    "API key not provided. Set COMPOSIO_API_KEY environment variable or use .api_key()".to_string()
3640                )
3641            })?;
3642
3643        // Build configuration with defaults
3644        let mut config = ComposioConfig::new(api_key);
3645
3646        if let Some(base_url) = self.base_url {
3647            config.base_url = base_url;
3648        }
3649
3650        if let Some(timeout) = self.timeout {
3651            config.timeout = timeout;
3652        }
3653
3654        // Build retry policy
3655        let mut retry_policy = RetryPolicy::default();
3656        if let Some(max_retries) = self.max_retries {
3657            retry_policy.max_retries = max_retries;
3658        }
3659        if let Some(initial_delay) = self.initial_retry_delay {
3660            retry_policy.initial_delay = initial_delay;
3661        }
3662        if let Some(max_delay) = self.max_retry_delay {
3663            retry_policy.max_delay = max_delay;
3664        }
3665        config.retry_policy = retry_policy;
3666
3667        // Set toolkit versions if provided
3668        if let Some(toolkit_versions) = self.toolkit_versions {
3669            config.toolkit_versions = Some(toolkit_versions);
3670        }
3671
3672        // Set file management options
3673        if let Some(file_download_dir) = self.file_download_dir {
3674            config.file_download_dir = Some(file_download_dir);
3675        }
3676        if let Some(auto_upload_download_files) = self.auto_upload_download_files {
3677            config.auto_upload_download_files = auto_upload_download_files;
3678        }
3679
3680        // Set telemetry option (opt-in, disabled by default)
3681        if let Some(telemetry_enabled) = self.telemetry_enabled {
3682            config.telemetry_enabled = telemetry_enabled;
3683        }
3684
3685        // Validate configuration
3686        config.validate()?;
3687
3688        // Build HTTP client with timeout and default headers
3689        let mut headers = reqwest::header::HeaderMap::new();
3690        headers.insert(
3691            "x-api-key",
3692            reqwest::header::HeaderValue::from_str(&config.api_key)
3693                .map_err(|_| ComposioError::InvalidInput("Invalid API key format".to_string()))?,
3694        );
3695
3696        let http_client = reqwest::Client::builder()
3697            .timeout(config.timeout)
3698            .default_headers(headers)
3699            .build()
3700            .map_err(|e| ComposioError::NetworkError(e))?;
3701
3702        Ok(ComposioClient {
3703            http_client,
3704            config,
3705        })
3706    }
3707}
3708
3709#[cfg(test)]
3710mod tests {
3711    use super::*;
3712
3713    #[test]
3714    fn test_builder_with_api_key_only() {
3715        let client = ComposioClient::builder()
3716            .api_key("test_key")
3717            .build()
3718            .unwrap();
3719
3720        assert_eq!(client.config().api_key, "test_key");
3721        assert_eq!(
3722            client.config().base_url,
3723            "https://backend.composio.dev/api/v3"
3724        );
3725        assert_eq!(client.config().timeout, Duration::from_secs(30));
3726        assert_eq!(client.config().retry_policy.max_retries, 3);
3727    }
3728
3729    #[test]
3730    fn test_builder_with_all_options() {
3731        let client = ComposioClient::builder()
3732            .api_key("test_key")
3733            .base_url("https://custom.api.com")
3734            .timeout(Duration::from_secs(60))
3735            .max_retries(5)
3736            .initial_retry_delay(Duration::from_secs(2))
3737            .max_retry_delay(Duration::from_secs(30))
3738            .build()
3739            .unwrap();
3740
3741        assert_eq!(client.config().api_key, "test_key");
3742        assert_eq!(client.config().base_url, "https://custom.api.com");
3743        assert_eq!(client.config().timeout, Duration::from_secs(60));
3744        assert_eq!(client.config().retry_policy.max_retries, 5);
3745        assert_eq!(
3746            client.config().retry_policy.initial_delay,
3747            Duration::from_secs(2)
3748        );
3749        assert_eq!(
3750            client.config().retry_policy.max_delay,
3751            Duration::from_secs(30)
3752        );
3753    }
3754
3755    #[test]
3756    fn test_builder_without_api_key_fails() {
3757        let result = ComposioClient::builder().build();
3758
3759        assert!(result.is_err());
3760        match result {
3761            Err(ComposioError::InvalidInput(msg)) => {
3762                assert_eq!(msg, "API key is required");
3763            }
3764            _ => panic!("Expected InvalidInput error"),
3765        }
3766    }
3767
3768    #[test]
3769    fn test_builder_with_empty_api_key_fails() {
3770        let result = ComposioClient::builder().api_key("").build();
3771
3772        assert!(result.is_err());
3773        match result {
3774            Err(ComposioError::InvalidInput(msg)) => {
3775                assert_eq!(msg, "API key cannot be empty");
3776            }
3777            _ => panic!("Expected InvalidInput error"),
3778        }
3779    }
3780
3781    #[test]
3782    fn test_builder_with_invalid_base_url_fails() {
3783        let result = ComposioClient::builder()
3784            .api_key("test_key")
3785            .base_url("invalid-url")
3786            .build();
3787
3788        assert!(result.is_err());
3789        match result {
3790            Err(ComposioError::ConfigError(msg)) => {
3791                assert_eq!(msg, "Base URL must start with http:// or https://");
3792            }
3793            _ => panic!("Expected ConfigError"),
3794        }
3795    }
3796
3797    #[test]
3798    fn test_builder_accepts_string_api_key() {
3799        let client = ComposioClient::builder()
3800            .api_key("test_key".to_string())
3801            .build()
3802            .unwrap();
3803
3804        assert_eq!(client.config().api_key, "test_key");
3805    }
3806
3807    #[test]
3808    fn test_builder_accepts_str_api_key() {
3809        let client = ComposioClient::builder()
3810            .api_key("test_key")
3811            .build()
3812            .unwrap();
3813
3814        assert_eq!(client.config().api_key, "test_key");
3815    }
3816
3817    #[test]
3818    fn test_client_is_cloneable() {
3819        let client = ComposioClient::builder()
3820            .api_key("test_key")
3821            .build()
3822            .unwrap();
3823
3824        let cloned = client.clone();
3825        assert_eq!(client.config().api_key, cloned.config().api_key);
3826    }
3827
3828    #[test]
3829    fn test_client_is_debuggable() {
3830        let client = ComposioClient::builder()
3831            .api_key("test_key")
3832            .build()
3833            .unwrap();
3834
3835        let debug_str = format!("{:?}", client);
3836        assert!(debug_str.contains("ComposioClient"));
3837    }
3838
3839    #[test]
3840    fn test_builder_is_debuggable() {
3841        let builder = ComposioClient::builder().api_key("test_key");
3842
3843        let debug_str = format!("{:?}", builder);
3844        assert!(debug_str.contains("ComposioClientBuilder"));
3845    }
3846
3847    #[test]
3848    fn test_http_client_has_correct_timeout() {
3849        let client = ComposioClient::builder()
3850            .api_key("test_key")
3851            .timeout(Duration::from_secs(45))
3852            .build()
3853            .unwrap();
3854
3855        assert_eq!(client.config().timeout, Duration::from_secs(45));
3856    }
3857
3858    #[test]
3859    fn test_config_accessor() {
3860        let client = ComposioClient::builder()
3861            .api_key("test_key")
3862            .build()
3863            .unwrap();
3864
3865        let config = client.config();
3866        assert_eq!(config.api_key, "test_key");
3867    }
3868
3869    #[test]
3870    fn test_http_client_accessor() {
3871        let client = ComposioClient::builder()
3872            .api_key("test_key")
3873            .build()
3874            .unwrap();
3875
3876        let _http_client = client.http_client();
3877        // Just verify we can access it without panic
3878    }
3879
3880    #[test]
3881    fn test_builder_method_chaining() {
3882        let client = ComposioClient::builder()
3883            .api_key("test_key")
3884            .base_url("https://test.com")
3885            .timeout(Duration::from_secs(60))
3886            .max_retries(5)
3887            .initial_retry_delay(Duration::from_secs(2))
3888            .max_retry_delay(Duration::from_secs(30))
3889            .build()
3890            .unwrap();
3891
3892        assert_eq!(client.config().api_key, "test_key");
3893        assert_eq!(client.config().base_url, "https://test.com");
3894    }
3895
3896    #[test]
3897    fn test_default_retry_policy() {
3898        let client = ComposioClient::builder()
3899            .api_key("test_key")
3900            .build()
3901            .unwrap();
3902
3903        assert_eq!(client.config().retry_policy.max_retries, 3);
3904        assert_eq!(
3905            client.config().retry_policy.initial_delay,
3906            Duration::from_secs(1)
3907        );
3908        assert_eq!(
3909            client.config().retry_policy.max_delay,
3910            Duration::from_secs(10)
3911        );
3912    }
3913
3914    #[test]
3915    fn test_custom_retry_policy() {
3916        let client = ComposioClient::builder()
3917            .api_key("test_key")
3918            .max_retries(7)
3919            .initial_retry_delay(Duration::from_millis(500))
3920            .max_retry_delay(Duration::from_secs(20))
3921            .build()
3922            .unwrap();
3923
3924        assert_eq!(client.config().retry_policy.max_retries, 7);
3925        assert_eq!(
3926            client.config().retry_policy.initial_delay,
3927            Duration::from_millis(500)
3928        );
3929        assert_eq!(
3930            client.config().retry_policy.max_delay,
3931            Duration::from_secs(20)
3932        );
3933    }
3934
3935    #[test]
3936    fn test_partial_retry_policy_customization() {
3937        let client = ComposioClient::builder()
3938            .api_key("test_key")
3939            .max_retries(5)
3940            .build()
3941            .unwrap();
3942
3943        assert_eq!(client.config().retry_policy.max_retries, 5);
3944        assert_eq!(
3945            client.config().retry_policy.initial_delay,
3946            Duration::from_secs(1)
3947        );
3948        assert_eq!(
3949            client.config().retry_policy.max_delay,
3950            Duration::from_secs(10)
3951        );
3952    }
3953}