tosspayments/
client.rs

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}