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 pub fn new(secret_key: impl Into<String>) -> Self {
30 Self::from_url(DEFAULT_API_BASE_URL, secret_key)
31 }
32
33 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 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 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 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 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 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 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 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 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 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(¶ms, 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(¶ms));
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}