stripe/client/
stripe.rs

1use http_types::{Body, Method, Request, Url};
2use serde::{de::DeserializeOwned, Serialize};
3
4use crate::{
5    client::{request_strategy::RequestStrategy, BaseClient, Response},
6    config::err,
7    generated::core::version::VERSION,
8    params::AppInfo,
9    AccountId, ApplicationId, Headers, StripeError,
10};
11
12static USER_AGENT: &str = concat!("Stripe/v1 RustBindings/", env!("CARGO_PKG_VERSION"));
13
14#[derive(Clone)]
15pub struct Client {
16    client: crate::client::BaseClient,
17    secret_key: String,
18    headers: Headers,
19    strategy: RequestStrategy,
20    app_info: Option<AppInfo>,
21    api_base: Url,
22    api_root: String,
23}
24
25impl Client {
26    /// Create a new account with the given secret key.
27    pub fn new(secret_key: impl Into<String>) -> Self {
28        Self::from_url("https://api.stripe.com/", secret_key)
29    }
30
31    /// Create a new account pointed at a specific URL. This is useful for testing.
32    ///
33    /// # Panics
34    /// If the url can't be parsed
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                stripe_version: VERSION,
41                user_agent: USER_AGENT.to_string(),
42                client_id: None,
43                stripe_account: None,
44            },
45            strategy: RequestStrategy::Once,
46            app_info: None,
47            api_base: Url::parse(url.into()).expect("invalid url"),
48            api_root: "v1".to_string(),
49        }
50    }
51
52    /// Set the client id for the client.
53    pub fn with_client_id(mut self, id: ApplicationId) -> Self {
54        self.headers.client_id = Some(id);
55        self
56    }
57
58    /// Set the stripe account for the client.
59    pub fn with_stripe_account(mut self, id: AccountId) -> Self {
60        self.headers.stripe_account = Some(id);
61        self
62    }
63
64    /// Set the request strategy for the client.
65    ///
66    /// Note: the client is cheap to clone so if you require a new client
67    ///       temporarily with a new strategy you can simply clone it
68    ///       and keep going.
69    ///
70    /// ```no_run
71    /// use stripe::RequestStrategy;
72    /// let client = stripe::Client::new("sk_test_123");
73    /// let idempotent_client = client
74    ///     .clone()
75    ///     .with_strategy(RequestStrategy::Idempotent("my-key".to_string()));
76    /// ```
77    pub fn with_strategy(mut self, strategy: RequestStrategy) -> Self {
78        self.strategy = strategy;
79        self
80    }
81
82    /// Set the application info for the client.
83    ///
84    /// It is recommended that applications set this so that
85    /// stripe is able to undestand usage patterns from your
86    /// user agent.
87    pub fn with_app_info(
88        mut self,
89        name: String,
90        version: Option<String>,
91        url: Option<String>,
92    ) -> Self {
93        let app_info = AppInfo { name, version, url };
94        self.headers.user_agent = format!("{} {}", USER_AGENT, app_info);
95        self.app_info = Some(app_info);
96        self
97    }
98
99    /// Make a `GET` http request with just a path
100    pub fn get<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
101        let url = self.url(path);
102        self.client.execute::<T>(self.create_request(Method::Get, url), &self.strategy)
103    }
104
105    /// Make a `GET` http request with url query parameters
106    pub fn get_query<T: DeserializeOwned + Send + 'static, P: Serialize>(
107        &self,
108        path: &str,
109        params: P,
110    ) -> Response<T> {
111        let url = match self.url_with_params(path, params) {
112            Err(e) => return err(e),
113            Ok(ok) => ok,
114        };
115        self.client.execute::<T>(self.create_request(Method::Get, url), &self.strategy)
116    }
117
118    /// Make a `DELETE` http request with just a path
119    pub fn delete<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
120        let url = self.url(path);
121        self.client.execute::<T>(self.create_request(Method::Delete, url), &self.strategy)
122    }
123
124    /// Make a `DELETE` http request with url query parameters
125    pub fn delete_query<T: DeserializeOwned + Send + 'static, P: Serialize>(
126        &self,
127        path: &str,
128        params: P,
129    ) -> Response<T> {
130        let url = match self.url_with_params(path, params) {
131            Err(e) => return err(e),
132            Ok(ok) => ok,
133        };
134        self.client.execute::<T>(self.create_request(Method::Delete, url), &self.strategy)
135    }
136
137    /// Make a `POST` http request with just a path
138    pub fn post<T: DeserializeOwned + Send + 'static>(&self, path: &str) -> Response<T> {
139        let url = self.url(path);
140        self.client.execute::<T>(self.create_request(Method::Post, url), &self.strategy)
141    }
142
143    /// Make a `POST` http request with urlencoded body
144    ///
145    /// # Panics
146    /// If the form is not serialized to an utf8 string.
147    pub fn post_form<T: DeserializeOwned + Send + 'static, F: Serialize>(
148        &self,
149        path: &str,
150        form: F,
151    ) -> Response<T> {
152        let url = self.url(path);
153        let mut req = self.create_request(Method::Post, url);
154
155        let mut params_buffer = Vec::new();
156        let qs_ser = &mut serde_qs::Serializer::new(&mut params_buffer);
157        if let Err(qs_ser_err) = serde_path_to_error::serialize(&form, qs_ser) {
158            return err(StripeError::QueryStringSerialize(qs_ser_err));
159        }
160
161        let body = std::str::from_utf8(params_buffer.as_slice())
162            .expect("Unable to extract string from params_buffer")
163            .to_string();
164
165        req.set_body(Body::from_string(body));
166
167        req.insert_header("content-type", "application/x-www-form-urlencoded");
168        self.client.execute::<T>(req, &self.strategy)
169    }
170
171    fn url(&self, path: &str) -> Url {
172        let mut url = self.api_base.clone();
173        url.set_path(&format!("{}/{}", self.api_root, path.trim_start_matches('/')));
174        url
175    }
176
177    fn url_with_params<P: Serialize>(&self, path: &str, params: P) -> Result<Url, StripeError> {
178        let mut url = self.url(path);
179
180        let mut params_buffer = Vec::new();
181        let qs_ser = &mut serde_qs::Serializer::new(&mut params_buffer);
182        serde_path_to_error::serialize(&params, qs_ser).map_err(StripeError::from)?;
183
184        let params = std::str::from_utf8(params_buffer.as_slice())
185            .expect("Unable to extract string from params_buffer")
186            .to_string();
187
188        url.set_query(Some(&params));
189        Ok(url)
190    }
191
192    fn create_request(&self, method: Method, url: Url) -> Request {
193        let mut req = Request::new(method, url);
194        req.insert_header("authorization", format!("Bearer {}", self.secret_key));
195
196        for (key, value) in self.headers.to_array().iter().filter_map(|(k, v)| v.map(|v| (*k, v))) {
197            req.insert_header(key, value);
198        }
199
200        req
201    }
202}
203
204#[cfg(test)]
205mod test {
206    //! Ensures our user agent matches the format of the other stripe clients.
207    //!
208    //! See: <https://github.com/stripe/stripe-python/blob/3b917dc4cec6a3cccfd46961e05fe7b55c6bee87/stripe/api_requestor.py#L241>
209
210    use super::Client;
211
212    #[test]
213    fn user_agent_base() {
214        let client = Client::new("sk_test_12345");
215
216        assert_eq!(
217            client.headers.user_agent,
218            format!("Stripe/v1 RustBindings/{}", env!("CARGO_PKG_VERSION"))
219        );
220    }
221
222    #[test]
223    fn user_agent_minimal_app_info() {
224        let client =
225            Client::new("sk_test_12345").with_app_info("sick-new-startup".to_string(), None, None);
226
227        assert_eq!(
228            client.headers.user_agent,
229            format!("Stripe/v1 RustBindings/{} sick-new-startup", env!("CARGO_PKG_VERSION"))
230        );
231    }
232
233    #[test]
234    fn user_agent_all() {
235        let client = Client::new("sk_test_12345").with_app_info(
236            "sick-new-startup".to_string(),
237            Some("0.1.0".to_string()),
238            Some("https://sick-startup.io".to_string()),
239        );
240
241        assert_eq!(
242            client.headers.user_agent,
243            format!(
244                "Stripe/v1 RustBindings/{} sick-new-startup/0.1.0 (https://sick-startup.io)",
245                env!("CARGO_PKG_VERSION")
246            )
247        );
248    }
249}