pinpayments/client/
pinpayments.rs

1use http_types::{Body, Method, Request, Url};
2use serde::{de::DeserializeOwned, Serialize};
3
4use surf::http::auth::BasicAuth;
5
6use crate::{
7    client::{BaseClient, Response, StatusOnlyResponse},
8    config::err,
9    params::AppInfo,
10    Headers, PinError,
11};
12
13const USER_AGENT: &str = concat!("PinPayments/1 RustClient/", env!("CARGO_PKG_VERSION"));
14
15pub const DEFAULT_API_BASE_URL: &str = "https://api.pinpayments.com/1/";
16pub const DEFAULT_TEST_API_BASE_URL: &str = "https://test-api.pinpayments.com/1/";
17
18#[derive(Clone, Debug)]
19pub struct Client {
20    client: crate::client::BaseClient,
21    secret_key: String,
22    headers: Headers,
23    app_info: Option<AppInfo>,
24    api_base: Url
25}
26
27impl Client {
28    /// Create a new client using the presented secret key.
29    pub fn new(secret_key: impl Into<String>) -> Self {
30        Self::from_url(DEFAULT_API_BASE_URL, secret_key)
31    }
32
33    /// Create a new client making use of the specified URL. Typically used in sandbox and test
34    /// scenarios.
35    pub fn from_url<'a>(url: impl Into<&'a str>, secret_key: impl Into<String>) -> Self {
36        Client {
37            client: BaseClient::new(),
38            secret_key: secret_key.into(),
39            headers: Headers {
40                user_agent: USER_AGENT.to_string()
41            },
42            app_info: None,
43            api_base: Url::parse(url.into()).expect("invalid url"),
44        }
45    }
46
47    /// Set the application info of the client.
48    pub fn with_app_info(
49        mut self,
50        name: String,
51        version: Option<String>,
52        url: Option<String>,
53    ) -> Self {
54        let app_info = AppInfo { name, version, url };
55        self.headers.user_agent = format!("{} {}", USER_AGENT, app_info.to_string());
56        self.app_info = Some(app_info);
57        self
58    }
59
60    /// Make a http `GET` request using presented path
61    pub fn get<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
62        let url = self.url(path);
63        self.client.execute::<T>(self.create_request(Method::Get, url))
64    }
65
66    /// Make a http `GET` request appending presented query parameters
67    pub fn get_query<T: DeserializeOwned + Send + 'static, P: Serialize>(
68        &self,
69        path: &str,
70        params: P,
71    ) -> Response<T> {
72        let url = match self.url_with_params(path, params) {
73            Err(e) => return err(e),
74            Ok(ok) => ok,
75        };
76        self.client.execute::<T>(self.create_request(Method::Get, url))
77    }
78
79    /// Make a http `DELETE` request using presented path
80    pub fn delete<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
81        let url = self.url(path);
82        self.client.execute::<T>(self.create_request(Method::Delete, url))
83    }
84
85    pub fn delete_status_only(&self, path: &str) -> StatusOnlyResponse {
86        let url = self.url(path);
87        self.client.execute_status_only(self.create_request(Method::Delete, url))
88    }
89
90    /// Make a http `DELETE` request using presented query parameters
91    pub fn delete_query<T: DeserializeOwned + Send + 'static, P: Serialize>(
92        &self,
93        path: &str,
94        params: P,
95    ) -> Response<T> {
96        let url = match self.url_with_params(path, params) {
97            Err(e) => return err(e),
98            Ok(ok) => ok,
99        };
100        self.client.execute::<T>(self.create_request(Method::Delete, url))
101    }
102
103    /// Make a http `PUT` request using presented path
104    pub fn put<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
105        let url = self.url(path);
106        self.client.execute::<T>(self.create_request(Method::Put, url))
107    }
108
109    /// Make a http `POST` request using presented path
110    pub fn post<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
111        let url = self.url(path);
112        self.client.execute::<T>(self.create_request(Method::Post, url))
113    }
114
115    /// Make a http `POST` request using presented path returning only the status
116    pub fn post_status_only(&self, path: &str) -> StatusOnlyResponse {
117        let url = self.url(path);
118        self.client.execute_status_only(self.create_request(Method::Post, url))
119    }
120
121    /// Make a http `POST` request urlencoding the body
122    pub fn post_form<T: DeserializeOwned + Send + 'static, F: Serialize>(
123        &self,
124        path: &str,
125        form: F,
126    ) -> Response<T> {
127        let url = self.url(path);
128        let mut req = self.create_request(Method::Post, url);
129
130        let mut params_buffer = Vec::new();
131        let qs_ser = &mut serde_qs::Serializer::new(&mut params_buffer);
132        if let Err(qs_ser_err) = serde_path_to_error::serialize(&form, qs_ser) {
133            return err(PinError::QueryStringSerialize(qs_ser_err));
134        }
135
136        let body = std::str::from_utf8(params_buffer.as_slice())
137            .expect("Unable to extract string from params_buffer")
138            .to_string();
139
140        req.set_body(Body::from_string(body));
141
142        req.insert_header("content-type", "application/x-www-form-urlencoded");
143        self.client.execute::<T>(req)
144    }
145
146    fn url(&self, path: &str) -> Url {
147        let base = self.api_base.clone();
148        let url = base.join(path.trim_start_matches('/')).unwrap();
149        url
150    }
151
152    fn url_with_params<P: Serialize>(&self, path: &str, params: P) -> Result<Url, PinError> {
153        let mut url = self.url(path);
154
155        let mut params_buffer = Vec::new();
156        let qs_ser = &mut serde_qs::Serializer::new(&mut params_buffer);
157        serde_path_to_error::serialize(&params, qs_ser).map_err(PinError::from)?;
158
159        let params = std::str::from_utf8(params_buffer.as_slice())
160            .expect("Unable to extract string from params_buffer")
161            .to_string();
162
163        url.set_query(Some(&params));
164        Ok(url)
165    }
166
167    fn create_request(&self, method: Method, url: Url) -> Request {
168        let mut req = Request::new(method, url);
169        let auth = BasicAuth::new(&self.secret_key, "");
170        req.insert_header(auth.name(), auth.value());
171
172        for (key, value) in self.headers.to_array().iter().filter_map(|(k, v)| v.map(|v| (*k, v))) {
173            req.insert_header(key, value);
174        }
175
176        req
177    }
178}
179
180
181#[cfg(test)]
182mod test {
183    use super::Client;
184
185    #[test]
186    fn user_agent_base() {
187        let client = Client::new("sk_test_12345");
188
189        assert_eq!(
190            client.headers.user_agent,
191            format!("PinPayments/1 RustClient/{}", env!("CARGO_PKG_VERSION"))
192        );
193    }
194
195    #[test]
196    fn user_agent_minimal_app_info() {
197        let client =
198            Client::new("sk_test_12345").with_app_info("fusillade".to_string(), None, None);
199
200        assert_eq!(
201            client.headers.user_agent,
202            format!("PinPayments/1 RustClient/{} fusillade", env!("CARGO_PKG_VERSION"))
203        );
204    }
205
206    #[test]
207    fn user_agent_all() {
208        let client = Client::new("sk_test_12345").with_app_info(
209            "fusillade".to_string(),
210            Some("0.1.0".to_string()),
211            Some("https://fusillade.app".to_string()),
212        );
213
214        assert_eq!(
215            client.headers.user_agent,
216            format!(
217                "PinPayments/1 RustClient/{} fusillade/0.1.0 (https://fusillade.app)",
218                env!("CARGO_PKG_VERSION")
219            )
220        );
221    }
222}