1use base64::Engine;
2use reqwest::header::{HeaderMap, HeaderValue};
3use reqwest::Url;
4
5use crate::data::Failure;
6use crate::endpoint::Endpoint;
7use crate::TosspaymentsError;
8
9static USER_AGENT: &str = concat!("Tosspayments/v1 RustBindings/", env!("CARGO_PKG_VERSION"));
10
11#[derive(Debug, Clone)]
12pub struct Client {
13 client: reqwest::Client,
14 api_base: Url,
15}
16
17impl Client {
18 pub fn new(secret: impl Into<String>) -> Self {
19 Self::from_url("https://api.tosspayments.com", secret)
20 }
21
22 pub fn from_url(url: impl Into<String>, secret: impl Into<String>) -> Self {
23 let auth_str = format!("{}:", secret.into());
24 let auth = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth_str.as_bytes());
25 let mut headers = HeaderMap::new();
26 headers.insert("user-agent", HeaderValue::from_static(USER_AGENT));
27 headers.insert(
28 "accept",
29 HeaderValue::from_static("application/json; charset=utf-8"),
30 );
31 headers.insert(
32 "authorization",
33 HeaderValue::from_str(&format!("Basic {}", auth)).expect("wrong secret key"),
34 );
35 let client = reqwest::Client::builder()
36 .default_headers(headers)
37 .build()
38 .expect("fail to create client");
39 Self {
40 client,
41 api_base: Url::parse(&url.into()).expect("invalid url"),
42 }
43 }
44
45 pub async fn execute<E>(&self, endpoint: &E) -> Result<E::Response, crate::Error>
46 where
47 E: Endpoint,
48 {
49 let mut url = self.api_base.clone();
50 url.set_path(&endpoint.relative_path());
51 if let Some(query) = endpoint.query() {
52 let query_str = serde_qs::to_string(query)?;
53 url.set_query(Some(&query_str));
54 }
55 let mut request = self.client.request(endpoint.method(), url);
56 if let Some(body) = endpoint.body() {
57 request = request.header("content-type", HeaderValue::from_static("application/json"));
58 request = request.json(body);
59 }
60 if let Some(ref idempotency_key) = endpoint.idempotency_key() {
61 request = request.header("idempotency-key", idempotency_key);
62 }
63 let resp = request.send().await?;
64 let status = resp.status();
65 if !status.is_success() {
66 let failure = resp.json::<Failure>().await?;
67 return Err(crate::Error::Tosspayments(TosspaymentsError {
68 http_status: status.as_u16(),
69 code: failure.code,
70 message: failure.message,
71 }));
72 }
73 let result = resp.json::<E::Response>().await?;
74 Ok(result)
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use crate::api::GetPayment;
82 use crate::data::ErrorCode;
83 use crate::Error;
84 use httpmock::prelude::*;
85 use serde_json::json;
86
87 #[tokio::test]
88 async fn authorization() {
89 let server = MockServer::start();
90 let client = Client::from_url(server.base_url(), "my_secret_key");
91 let mock = server.mock(|when, then| {
92 when
93 .method(GET)
94 .path("/v1/payments/my_payment_key")
95 .header("authorization", "Basic bXlfc2VjcmV0X2tleTo");
96 then
97 .status(404)
98 .header("content-type", "application/json")
99 .json_body(json!({
100 "code": "NOT_FOUND_PAYMENT",
101 "message": "결제 정보를 찾을 수 없습니다"
102 }));
103 });
104 let err = client
105 .execute(&GetPayment::PaymentKey("my_payment_key".to_string()))
106 .await
107 .unwrap_err();
108 mock.assert();
109 assert!(matches!(
110 err,
111 Error::Tosspayments(TosspaymentsError {
112 http_status: 404,
113 code: ErrorCode::NotFoundPayment,
114 ..
115 })
116 ));
117 }
118}