use serde::{Deserialize, Serialize};
use std::time::{Duration, SystemTime};
pub const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Debug, Clone)]
pub struct ConnectionRequest {
pub id: String,
pub status: ConnectionStatus,
pub redirect_url: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ConnectionStatus {
Initializing,
Initiated,
Active,
Expired,
Failed,
Inactive,
}
impl ConnectionRequest {
pub fn new(id: String, status: ConnectionStatus, redirect_url: Option<String>) -> Self {
Self {
id,
status,
redirect_url,
}
}
pub async fn wait_for_connection(
&mut self,
timeout: Option<Duration>,
) -> Result<(), ConnectionError> {
let timeout = timeout.unwrap_or(DEFAULT_WAIT_TIMEOUT);
let deadline = SystemTime::now() + timeout;
while SystemTime::now() < deadline {
if self.status == ConnectionStatus::Active {
return Ok(());
}
if self.status == ConnectionStatus::Failed {
return Err(ConnectionError::Failed(format!(
"Connection {} failed",
self.id
)));
}
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(ConnectionError::Timeout(format!(
"Timeout while waiting for connection {} to be active",
self.id
)))
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConnectionError {
#[error("Connection timeout: {0}")]
Timeout(String),
#[error("Connection failed: {0}")]
Failed(String),
#[error("Multiple connected accounts found: {0}")]
MultipleAccounts(String),
#[error("API error: {0}")]
ApiError(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AuthScheme {
Oauth1,
Oauth2,
ComposioLink,
ApiKey,
Basic,
BearerToken,
GoogleServiceAccount,
NoAuth,
CalcomAuth,
BillcomAuth,
BasicWithJwt,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionState {
pub auth_scheme: AuthScheme,
pub status: ConnectionStatus,
#[serde(flatten)]
pub config: serde_json::Value,
}
pub struct AuthSchemeHelper;
impl AuthSchemeHelper {
pub fn oauth1(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::Oauth1,
status: ConnectionStatus::Initializing,
config,
}
}
pub fn oauth2(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::Oauth2,
status: ConnectionStatus::Initializing,
config,
}
}
pub fn composio_link(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::ComposioLink,
status: ConnectionStatus::Initializing,
config,
}
}
pub fn api_key(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::ApiKey,
status: ConnectionStatus::Active,
config,
}
}
pub fn basic(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::Basic,
status: ConnectionStatus::Active,
config,
}
}
pub fn bearer_token(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::BearerToken,
status: ConnectionStatus::Active,
config,
}
}
pub fn google_service_account(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::GoogleServiceAccount,
status: ConnectionStatus::Active,
config,
}
}
pub fn no_auth(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::NoAuth,
status: ConnectionStatus::Active,
config,
}
}
pub fn calcom_auth(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::CalcomAuth,
status: ConnectionStatus::Active,
config,
}
}
pub fn billcom_auth(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::BillcomAuth,
status: ConnectionStatus::Active,
config,
}
}
pub fn basic_with_jwt(config: serde_json::Value) -> ConnectionState {
ConnectionState {
auth_scheme: AuthScheme::BasicWithJwt,
status: ConnectionStatus::Active,
config,
}
}
}
pub static AUTH_SCHEME: AuthSchemeHelper = AuthSchemeHelper;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InitiateConnectionParams {
pub user_id: String,
pub auth_config_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub allow_multiple: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub config: Option<ConnectionState>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LinkConnectionParams {
pub user_id: String,
pub auth_config_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub callback_url: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConnectedAccountListParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub user_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auth_config_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub statuses: Option<Vec<ConnectionStatus>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolkit_slugs: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connected_account_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_by: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub order_direction: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectedAccountInfo {
pub id: String,
pub user_id: String,
pub auth_config_id: String,
pub toolkit: String,
pub status: ConnectionStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_disabled: Option<bool>,
pub created_at: String,
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub state: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectedAccountListResponse {
pub items: Vec<ConnectedAccountInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_pages: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub current_page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_items: Option<u32>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConnectedAccountRefreshParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub validate_credentials: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectedAccountRefreshResponse {
pub id: String,
pub status: ConnectionStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectedAccountUpdateStatusParams {
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectedAccountDeleteResponse {
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectedAccountUpdateStatusResponse {
pub success: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_connection_status_serialization() {
let status = ConnectionStatus::Active;
let json = serde_json::to_string(&status).unwrap();
assert_eq!(json, "\"ACTIVE\"");
}
#[test]
fn test_connection_status_deserialization() {
let json = "\"ACTIVE\"";
let status: ConnectionStatus = serde_json::from_str(json).unwrap();
assert_eq!(status, ConnectionStatus::Active);
}
#[test]
fn test_auth_scheme_serialization() {
let scheme = AuthScheme::Oauth2;
let json = serde_json::to_string(&scheme).unwrap();
assert_eq!(json, "\"OAUTH2\"");
}
#[test]
fn test_auth_scheme_helper_oauth2() {
let config = serde_json::json!({"client_id": "test"});
let state = AuthSchemeHelper::oauth2(config);
assert_eq!(state.auth_scheme, AuthScheme::Oauth2);
assert_eq!(state.status, ConnectionStatus::Initializing);
}
#[test]
fn test_auth_scheme_helper_api_key() {
let config = serde_json::json!({"api_key": "test_key"});
let state = AuthSchemeHelper::api_key(config);
assert_eq!(state.auth_scheme, AuthScheme::ApiKey);
assert_eq!(state.status, ConnectionStatus::Active);
}
#[test]
fn test_connection_request_new() {
let request = ConnectionRequest::new(
"ca_test123".to_string(),
ConnectionStatus::Initiated,
Some("https://auth.example.com".to_string()),
);
assert_eq!(request.id, "ca_test123");
assert_eq!(request.status, ConnectionStatus::Initiated);
assert!(request.redirect_url.is_some());
}
#[test]
fn test_connected_account_list_params_default() {
let params = ConnectedAccountListParams::default();
assert!(params.user_ids.is_none());
assert!(params.auth_config_ids.is_none());
assert!(params.statuses.is_none());
}
#[test]
fn test_initiate_connection_params_serialization() {
let params = InitiateConnectionParams {
user_id: "user_123".to_string(),
auth_config_id: "ac_abc".to_string(),
callback_url: Some("https://callback.example.com".to_string()),
allow_multiple: Some(false),
config: None,
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("user_123"));
assert!(json.contains("ac_abc"));
assert!(json.contains("callback"));
}
}