Skip to main content

fizzy_sdk/
raw.rs

1//! The verbs for the parts of Fizzy the model does not describe. They take a path rather
2//! than a route and hand back the answer undecoded, but everything else is what a modelled
3//! call gets: the credentials, the hooks and the retries.
4//!
5//! A path here goes out as it was written; an [`AccountClient`] puts its account in front.
6
7use serde::Serialize;
8use url::Url;
9
10use crate::client::{AccountClient, Client, RequestOptions, Response};
11use crate::error::Error;
12use crate::http::Method;
13use crate::operation::Operation;
14use crate::security::is_same_origin;
15
16impl Client {
17    /// Reads a path.
18    pub async fn get(&self, path: &str) -> Result<Response, Error> {
19        self.execute(self.raw(Method::GET, path)?).await
20    }
21
22    /// Reads a path under options of the caller's own.
23    pub async fn get_with(&self, path: &str, options: &RequestOptions) -> Result<Response, Error> {
24        self.execute(options.apply(self.raw(Method::GET, path)?))
25            .await
26    }
27
28    /// Posts a JSON body. Sent once unless the options say the call is idempotent.
29    pub async fn post(&self, path: &str, body: &impl Serialize) -> Result<Response, Error> {
30        self.send_json(Method::POST, path, body, &RequestOptions::default())
31            .await
32    }
33
34    /// Posts a JSON body under options of the caller's own.
35    pub async fn post_with(
36        &self,
37        path: &str,
38        body: &impl Serialize,
39        options: &RequestOptions,
40    ) -> Result<Response, Error> {
41        self.send_json(Method::POST, path, body, options).await
42    }
43
44    /// Puts a JSON body.
45    pub async fn put(&self, path: &str, body: &impl Serialize) -> Result<Response, Error> {
46        self.send_json(Method::PUT, path, body, &RequestOptions::default())
47            .await
48    }
49
50    /// Puts a JSON body under options of the caller's own.
51    pub async fn put_with(
52        &self,
53        path: &str,
54        body: &impl Serialize,
55        options: &RequestOptions,
56    ) -> Result<Response, Error> {
57        self.send_json(Method::PUT, path, body, options).await
58    }
59
60    /// Patches with a JSON body.
61    pub async fn patch(&self, path: &str, body: &impl Serialize) -> Result<Response, Error> {
62        self.send_json(Method::PATCH, path, body, &RequestOptions::default())
63            .await
64    }
65
66    /// Patches with a JSON body under options of the caller's own.
67    pub async fn patch_with(
68        &self,
69        path: &str,
70        body: &impl Serialize,
71        options: &RequestOptions,
72    ) -> Result<Response, Error> {
73        self.send_json(Method::PATCH, path, body, options).await
74    }
75
76    /// Deletes a path.
77    pub async fn delete(&self, path: &str) -> Result<Response, Error> {
78        self.execute(self.raw(Method::DELETE, path)?).await
79    }
80
81    /// Deletes a path under options of the caller's own.
82    pub async fn delete_with(
83        &self,
84        path: &str,
85        options: &RequestOptions,
86    ) -> Result<Response, Error> {
87        self.execute(options.apply(self.raw(Method::DELETE, path)?))
88            .await
89    }
90
91    /// An operation for a path the model does not cover. An absolute URL is sent as it
92    /// stands, provided the credentials may travel to it; anything else is resolved
93    /// against the base URL.
94    pub(crate) fn raw(&self, method: Method, path: &str) -> Result<Operation, Error> {
95        Ok(match self.absolute(path)? {
96            Some(url) => Operation::at(method, url),
97            None => Operation::raw(method, path.to_string()),
98        })
99    }
100
101    /// The URL a path names when it already is one. Only the Fizzy origin itself is
102    /// accepted — a `Link` header's absolute URL, say — since every request carries the
103    /// credentials, and they go nowhere else.
104    fn absolute(&self, path: &str) -> Result<Option<Url>, Error> {
105        if path.starts_with("https://") || path.starts_with("http://") {
106            let url = Url::parse(path)?;
107            if !url.username().is_empty() || url.password().is_some() {
108                Err(Error::usage(
109                    "URL must not carry credentials; use an access token or a session token",
110                ))
111            } else if is_same_origin(&url, self.base_url()) {
112                Ok(Some(url))
113            } else {
114                Err(Error::usage(format!(
115                    "URL must be on the Fizzy origin {}, got one on {}",
116                    self.base_url().origin().ascii_serialization(),
117                    url.origin().ascii_serialization()
118                )))
119            }
120        } else {
121            Ok(None)
122        }
123    }
124
125    async fn send_json(
126        &self,
127        method: Method,
128        path: &str,
129        body: &impl Serialize,
130        options: &RequestOptions,
131    ) -> Result<Response, Error> {
132        let mut operation = self.raw(method, path)?;
133        operation.json(body)?;
134        self.execute(options.apply(operation)).await
135    }
136}
137
138impl AccountClient {
139    /// Reads a path under the account.
140    pub async fn get(&self, path: &str) -> Result<Response, Error> {
141        self.client().get(&self.scoped(path)).await
142    }
143
144    /// Reads a path under the account, with options of the caller's own.
145    pub async fn get_with(&self, path: &str, options: &RequestOptions) -> Result<Response, Error> {
146        self.client().get_with(&self.scoped(path), options).await
147    }
148
149    /// Posts a JSON body under the account.
150    pub async fn post(&self, path: &str, body: &impl Serialize) -> Result<Response, Error> {
151        self.client().post(&self.scoped(path), body).await
152    }
153
154    /// Posts a JSON body under the account, with options of the caller's own.
155    pub async fn post_with(
156        &self,
157        path: &str,
158        body: &impl Serialize,
159        options: &RequestOptions,
160    ) -> Result<Response, Error> {
161        self.client()
162            .post_with(&self.scoped(path), body, options)
163            .await
164    }
165
166    /// Puts a JSON body under the account.
167    pub async fn put(&self, path: &str, body: &impl Serialize) -> Result<Response, Error> {
168        self.client().put(&self.scoped(path), body).await
169    }
170
171    /// Puts a JSON body under the account, with options of the caller's own.
172    pub async fn put_with(
173        &self,
174        path: &str,
175        body: &impl Serialize,
176        options: &RequestOptions,
177    ) -> Result<Response, Error> {
178        self.client()
179            .put_with(&self.scoped(path), body, options)
180            .await
181    }
182
183    /// Patches with a JSON body under the account.
184    pub async fn patch(&self, path: &str, body: &impl Serialize) -> Result<Response, Error> {
185        self.client().patch(&self.scoped(path), body).await
186    }
187
188    /// Patches with a JSON body under the account, with options of the caller's own.
189    pub async fn patch_with(
190        &self,
191        path: &str,
192        body: &impl Serialize,
193        options: &RequestOptions,
194    ) -> Result<Response, Error> {
195        self.client()
196            .patch_with(&self.scoped(path), body, options)
197            .await
198    }
199
200    /// Deletes a path under the account.
201    pub async fn delete(&self, path: &str) -> Result<Response, Error> {
202        self.client().delete(&self.scoped(path)).await
203    }
204
205    /// Deletes a path under the account, with options of the caller's own.
206    pub async fn delete_with(
207        &self,
208        path: &str,
209        options: &RequestOptions,
210    ) -> Result<Response, Error> {
211        self.client().delete_with(&self.scoped(path), options).await
212    }
213
214    /// Reads a paginated path under the account to its end. See [`Client::get_all`].
215    pub async fn get_all(&self, path: &str) -> Result<Vec<serde_json::Value>, Error> {
216        self.client().get_all(&self.scoped(path)).await
217    }
218
219    /// Reads a paginated path under the account until `limit` items are in hand. See
220    /// [`Client::get_all_with_limit`].
221    pub async fn get_all_with_limit(
222        &self,
223        path: &str,
224        limit: usize,
225    ) -> Result<Vec<serde_json::Value>, Error> {
226        self.client()
227            .get_all_with_limit(&self.scoped(path), limit)
228            .await
229    }
230
231    /// The path with the account in front. An absolute URL is left alone: it names where
232    /// it goes already.
233    fn scoped(&self, path: &str) -> String {
234        if path.starts_with("https://") || path.starts_with("http://") {
235            path.to_string()
236        } else {
237            format!(
238                "/{}/{}",
239                crate::route::encode(self.account_id()),
240                path.trim_start_matches('/')
241            )
242        }
243    }
244}