1use crate::config::ComposioConfig;
24use crate::error::ComposioError;
25use crate::retry::RetryPolicy;
26use serde::Deserialize;
27use std::time::Duration;
28
29#[derive(Debug, Clone)]
47pub struct ComposioClient {
48 http_client: reqwest::Client,
49 config: ComposioConfig,
50}
51
52#[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 pub fn builder() -> ComposioClientBuilder {
114 ComposioClientBuilder::default()
115 }
116
117 pub fn http_client(&self) -> &reqwest::Client {
122 &self.http_client
123 }
124
125 pub fn config(&self) -> &ComposioConfig {
129 &self.config
130 }
131
132 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 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 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 if !response.status().is_success() {
215 return Err(ComposioError::from_response(response).await);
216 }
217
218 Ok(response)
219 })
220 .await?;
221
222 let session_response: crate::models::SessionResponse =
224 response.json().await.map_err(ComposioError::NetworkError)?;
225
226 Ok(crate::session::Session::from_response(
228 self.clone(),
229 session_response,
230 ))
231 }
232
233 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(¶ms)
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 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 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(¶ms)
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 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 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(¶ms)
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 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(¶ms)
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 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(¶ms)
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 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(¶ms)
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 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(¶ms)
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 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 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(¶ms)
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 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 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(¶ms)
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 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 let mut query_params = vec![];
633
634 if let Some(user_ids) = ¶ms.user_ids {
635 query_params.push(format!("user_ids={}", user_ids.join(",")));
636 }
637 if let Some(auth_config_ids) = ¶ms.auth_config_ids {
638 query_params.push(format!("auth_config_ids={}", auth_config_ids.join(",")));
639 }
640 if let Some(toolkit_slugs) = ¶ms.toolkit_slugs {
641 query_params.push(format!("toolkit_slugs={}", toolkit_slugs.join(",")));
642 }
643 if let Some(connected_account_ids) = ¶ms.connected_account_ids {
644 query_params.push(format!(
645 "connected_account_ids={}",
646 connected_account_ids.join(",")
647 ));
648 }
649 if let Some(statuses) = ¶ms.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) = ¶ms.cursor {
668 query_params.push(format!("cursor={}", cursor));
669 }
670 if let Some(order_by) = ¶ms.order_by {
671 query_params.push(format!("order_by={}", order_by));
672 }
673 if let Some(order_direction) = ¶ms.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 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 if !response.status().is_success() {
694 return Err(ComposioError::from_response(response).await);
695 }
696
697 Ok(response)
698 })
699 .await?;
700
701 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
703 }
704
705 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 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 if !response.status().is_success() {
747 return Err(ComposioError::from_response(response).await);
748 }
749
750 Ok(response)
751 })
752 .await?;
753
754 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
756 }
757
758 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(¶ms)
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 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(¶ms)
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 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(¶ms)
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 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 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(¶ms)
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 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(¶ms)
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 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 let mut query_params = vec![];
993
994 if let Some(category) = ¶ms.category {
995 query_params.push(format!("category={}", category));
996 }
997 if let Some(cursor) = ¶ms.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) = ¶ms.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) = ¶ms.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) = ¶ms.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 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 if !response.status().is_success() {
1042 return Err(ComposioError::from_response(response).await);
1043 }
1044
1045 Ok(response)
1046 })
1047 .await?;
1048
1049 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1051 }
1052
1053 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 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 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(¶ms)
1105 .send()
1106 .await
1107 .map_err(ComposioError::NetworkError)?;
1108
1109 if !response.status().is_success() {
1111 return Err(ComposioError::from_response(response).await);
1112 }
1113
1114 Ok(response)
1115 })
1116 .await?;
1117
1118 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1120 }
1121
1122 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 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 if !response.status().is_success() {
1161 return Err(ComposioError::from_response(response).await);
1162 }
1163
1164 Ok(response)
1165 })
1166 .await?;
1167
1168 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1170 }
1171
1172 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 let auth_config_id = self.get_or_create_auth_config(&toolkit).await?;
1217
1218 self.initiate_connection(user_id, auth_config_id, None)
1220 .await
1221 }
1222
1223 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 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 !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 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 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 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 if !response.status().is_success() {
1296 return Err(ComposioError::from_response(response).await);
1297 }
1298
1299 Ok(response)
1300 })
1301 .await?;
1302
1303 #[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 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 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(¶ms)
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 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 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 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 let mut query_params = vec![];
1461
1462 if let Some(toolkit_slug) = ¶ms.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) = ¶ms.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) = ¶ms.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 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 if !response.status().is_success() {
1498 return Err(ComposioError::from_response(response).await);
1499 }
1500
1501 Ok(response)
1502 })
1503 .await?;
1504
1505 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1507 }
1508
1509 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 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 if !response.status().is_success() {
1529 return Err(ComposioError::from_response(response).await);
1530 }
1531
1532 Ok(response)
1533 })
1534 .await?;
1535
1536 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1538 }
1539
1540 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 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 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 let mut query_params = vec![];
1723
1724 if let Some(tool_slugs) = ¶ms.tool_slugs {
1725 query_params.push(format!("tool_slugs={}", tool_slugs.join(",")));
1726 }
1727 if let Some(toolkit_slug) = ¶ms.toolkit_slug {
1728 query_params.push(format!("toolkit_slug={}", toolkit_slug));
1729 }
1730 if let Some(search) = ¶ms.search {
1731 query_params.push(format!("search={}", search));
1732 }
1733 if let Some(scopes) = ¶ms.scopes {
1734 query_params.push(format!("scopes={}", scopes.join(",")));
1735 }
1736 if let Some(tags) = ¶ms.tags {
1737 query_params.push(format!("tags={}", tags.join(",")));
1738 }
1739 if let Some(importance) = ¶ms.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) = ¶ms.cursor {
1749 query_params.push(format!("cursor={}", cursor));
1750 }
1751 if let Some(toolkit_versions) = ¶ms.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 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 if !response.status().is_success() {
1772 return Err(ComposioError::from_response(response).await);
1773 }
1774
1775 Ok(response)
1776 })
1777 .await?;
1778
1779 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1781 }
1782
1783 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 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 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 if !response.status().is_success() {
1854 return Err(ComposioError::from_response(response).await);
1855 }
1856
1857 Ok(response)
1858 })
1859 .await?;
1860
1861 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
1863 }
1864
1865 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 let version = if let Some(v) = params.version {
1935 v
1936 } else {
1937 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 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 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 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 if !response.status().is_success() {
1996 return Err(ComposioError::from_response(response).await);
1997 }
1998
1999 Ok(response)
2000 })
2001 .await?;
2002
2003 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2005 }
2006
2007 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 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 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 if !response.status().is_success() {
2083 return Err(ComposioError::from_response(response).await);
2084 }
2085
2086 Ok(response)
2087 })
2088 .await?;
2089
2090 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2092 }
2093
2094 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 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 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 if !response.status().is_success() {
2163 return Err(ComposioError::from_response(response).await);
2164 }
2165
2166 Ok(response)
2167 })
2168 .await?;
2169
2170 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2172 }
2173
2174 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 let mut query_params = vec![];
2219
2220 if let Some(cursor) = ¶ms.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) = ¶ms.toolkit_slugs {
2227 query_params.push(format!("toolkit_slugs={}", toolkit_slugs.join(",")));
2228 }
2229 if let Some(toolkit_versions) = ¶ms.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 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 if !response.status().is_success() {
2250 return Err(ComposioError::from_response(response).await);
2251 }
2252
2253 Ok(response)
2254 })
2255 .await?;
2256
2257 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2259 }
2260
2261 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 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 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 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(¶ms)
2340 .send()
2341 .await
2342 .map_err(ComposioError::NetworkError)?;
2343
2344 if !response.status().is_success() {
2346 return Err(ComposioError::from_response(response).await);
2347 }
2348
2349 Ok(response)
2350 })
2351 .await?;
2352
2353 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2355 }
2356
2357 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 let mut query_params = vec![];
2398
2399 if let Some(trigger_ids) = ¶ms.trigger_ids {
2400 query_params.push(format!("trigger_ids={}", trigger_ids.join(",")));
2401 }
2402 if let Some(trigger_names) = ¶ms.trigger_names {
2403 query_params.push(format!("trigger_names={}", trigger_names.join(",")));
2404 }
2405 if let Some(auth_config_ids) = ¶ms.auth_config_ids {
2406 query_params.push(format!("auth_config_ids={}", auth_config_ids.join(",")));
2407 }
2408 if let Some(connected_account_ids) = ¶ms.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) = ¶ms.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 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 if !response.status().is_success() {
2441 return Err(ComposioError::from_response(response).await);
2442 }
2443
2444 Ok(response)
2445 })
2446 .await?;
2447
2448 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2450 }
2451
2452 pub async fn create_trigger(
2492 &self,
2493 mut params: crate::models::triggers::TriggerCreateParams,
2494 ) -> Result<crate::models::triggers::TriggerCreateResponse, ComposioError> {
2495 if params.user_id.is_some() && params.connected_account_id.is_none() {
2497 let user_id = params.user_id.as_ref().unwrap();
2498
2499 let trigger_type = self.get_trigger_type(¶ms.slug).await?;
2501 let toolkit = trigger_type.toolkit.slug;
2502
2503 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 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 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 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 if !response.status().is_success() {
2562 return Err(ComposioError::from_response(response).await);
2563 }
2564
2565 Ok(response)
2566 })
2567 .await?;
2568
2569 Ok(response.json().await.map_err(ComposioError::NetworkError)?)
2571 }
2572
2573 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 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 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 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 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 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 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 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 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 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 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 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 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..]); }
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 let to_sign = format!("{}.{}.{}", params.id, params.timestamp, params.payload);
2890
2891 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 let mut signature_valid = false;
2904 for provided_sig in v1_signatures {
2905 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 let raw_payload: serde_json::Value =
2928 serde_json::from_str(¶ms.payload).map_err(|e| {
2929 ComposioError::InvalidInput(format!(
2930 "Failed to parse webhook payload as JSON: {}",
2931 e
2932 ))
2933 })?;
2934
2935 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 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 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 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 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 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(), toolkit_slug: String::new(), 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 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 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 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 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 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 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 pub fn api_key(mut self, key: impl Into<String>) -> Self {
3298 self.api_key = Some(key.into());
3299 self
3300 }
3301
3302 pub fn base_url(mut self, url: impl Into<String>) -> Self {
3329 self.base_url = Some(url.into());
3330 self
3331 }
3332
3333 pub fn timeout(mut self, timeout: Duration) -> Self {
3360 self.timeout = Some(timeout);
3361 self
3362 }
3363
3364 pub fn max_retries(mut self, retries: u32) -> Self {
3391 self.max_retries = Some(retries);
3392 self
3393 }
3394
3395 pub fn initial_retry_delay(mut self, delay: Duration) -> Self {
3423 self.initial_retry_delay = Some(delay);
3424 self
3425 }
3426
3427 pub fn max_retry_delay(mut self, delay: Duration) -> Self {
3455 self.max_retry_delay = Some(delay);
3456 self
3457 }
3458
3459 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 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 pub fn auto_upload_download_files(mut self, enabled: bool) -> Self {
3570 self.auto_upload_download_files = Some(enabled);
3571 self
3572 }
3573
3574 pub fn telemetry_enabled(mut self, enabled: bool) -> Self {
3603 self.telemetry_enabled = Some(enabled);
3604 self
3605 }
3606
3607 pub fn build(self) -> Result<ComposioClient, ComposioError> {
3633 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 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 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 if let Some(toolkit_versions) = self.toolkit_versions {
3669 config.toolkit_versions = Some(toolkit_versions);
3670 }
3671
3672 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 if let Some(telemetry_enabled) = self.telemetry_enabled {
3682 config.telemetry_enabled = telemetry_enabled;
3683 }
3684
3685 config.validate()?;
3687
3688 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 }
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}