1use hyper::{Body, Client as HyperClient, Method, Request};
2use hyper_tls::HttpsConnector;
3use serde::Serialize;
4
5#[derive(Serialize)]
6#[serde(rename_all = "camelCase")]
7pub struct AuthRequest {
8 client_id: String,
9 client_secret: String,
10 non_expiring: bool,
11}
12
13pub struct Client {
14 client_id: String,
15 client_secret: String,
16 url: String,
17 client: HyperClient<HttpsConnector<hyper::client::connect::HttpConnector>>,
18}
19
20impl Client {
21 pub fn new(client_id: String, client_secret: String) -> Self {
22 let client = HyperClient::builder().build::<_, hyper::Body>(HttpsConnector::new());
23
24 Self {
25 client_id,
26 client_secret,
27 url: "https://api.pluggy.ai".to_string(),
28 client,
29 }
30 }
31
32 pub fn new_from_env() -> Result<Self, Box<dyn std::error::Error>> {
33 let client_id = dotenv::var("PLUGGY_CLIENT_ID")?;
34 let client_secret = dotenv::var("PLUGGY_CLIENT_SECRET")?;
35 let client = Self::new(client_id, client_secret);
36 Ok(client)
37 }
38
39 pub async fn new_from_env_with_api_key() -> Result<(Self, String), Box<dyn std::error::Error>> {
40 let client = Self::new_from_env()?;
41 let connect_token = client.create_api_key().await?;
42 Ok((client, connect_token))
43 }
44
45 async fn create_api_key(&self) -> Result<String, Box<dyn std::error::Error>> {
46 let url = format!("{}/auth", self.url);
47
48 let payload = AuthRequest {
49 client_id: self.client_id.clone(),
50 client_secret: self.client_secret.clone(),
51 non_expiring: false,
52 };
53
54 let json_payload = serde_json::to_string(&payload)?;
56
57 let request = Request::builder()
59 .method(Method::POST)
60 .uri(url)
61 .header("Content-Type", "application/json")
62 .body(Body::from(json_payload))?;
63
64 let response = self.client.request(request).await?;
65 let body = hyper::body::to_bytes(response.into_body()).await?;
66 let body = String::from_utf8(body.to_vec())?;
67 let json: serde_json::Value = serde_json::from_str(&body)?;
68 let api_key = json["apiKey"].as_str();
69
70 match api_key {
71 Some(api_key) => Ok(api_key.to_string()),
72 None => Err("No api key found".into()),
73 }
74 }
75
76 pub async fn create_connect_token(
77 &self,
78 api_key: &str,
79 ) -> Result<String, Box<dyn std::error::Error>> {
80 let url = format!("{}/connect_token", self.url);
81
82 let request = Request::builder()
83 .method(Method::POST)
84 .uri(url)
85 .header("Content-Type", "application/json")
86 .header("Accept", "application/json")
87 .header("X-API-KEY", api_key)
88 .body(Body::empty())?;
89
90 let response = self.client.request(request).await?;
91 let body = hyper::body::to_bytes(response.into_body()).await?;
92 let body = String::from_utf8(body.to_vec())?;
93 let json: serde_json::Value = serde_json::from_str(&body)?;
94 let connect_token = json["accessToken"].as_str();
95
96 match connect_token {
97 Some(connect_token) => Ok(connect_token.to_string()),
98 None => Err("No connect token found".into()),
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 #[test]
108 fn can_instantiate_from_env() {
109 Client::new_from_env().unwrap();
110 }
111
112 #[tokio::test]
113 async fn can_instantiate_from_env_with_api_key() {
114 Client::new_from_env_with_api_key().await.unwrap();
115 }
116
117 #[tokio::test]
118 async fn can_create_connect_token() {
119 let (client, api_key) = Client::new_from_env_with_api_key().await.unwrap();
120 let connect_token = client.create_connect_token(&api_key).await.unwrap();
121 assert_eq!(connect_token.len(), 892);
122 }
123}