wasmstore_client/
lib.rs

1pub use anyhow::Error;
2
3mod hash;
4mod path;
5
6pub use hash::{Commit, Hash};
7pub use path::Path;
8
9#[derive(Clone)]
10pub struct Client {
11    url: reqwest::Url,
12    client: reqwest::Client,
13    version: Version,
14    auth: Option<String>,
15    branch: Option<String>,
16}
17
18#[derive(serde::Deserialize)]
19pub struct CommitInfo {
20    pub hash: Commit,
21    pub parents: Option<Vec<Commit>>,
22    pub date: i64,
23    pub author: String,
24    pub message: String,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Version {
29    V1,
30}
31
32impl Client {
33    pub fn new(url: impl reqwest::IntoUrl, version: Version) -> Result<Client, Error> {
34        let url = url.into_url()?;
35
36        let client = reqwest::Client::new();
37
38        Ok(Client {
39            url,
40            client,
41            version,
42            auth: None,
43            branch: None,
44        })
45    }
46
47    pub fn with_auth(mut self, auth: String) -> Client {
48        self.auth = Some(auth);
49        self
50    }
51
52    pub fn with_branch(mut self, branch: String) -> Client {
53        self.branch = Some(branch);
54        self
55    }
56
57    pub async fn request(
58        &self,
59        method: reqwest::Method,
60        endpoint: impl AsRef<str>,
61        body: Option<Vec<u8>>,
62    ) -> Result<reqwest::Response, Error> {
63        let url = self.url.join(endpoint.as_ref())?;
64        let mut builder = self.client.request(method, url);
65
66        if let Some(auth) = &self.auth {
67            builder = builder.header("Wasmstore-Auth", auth);
68        }
69
70        if let Some(branch) = &self.branch {
71            builder = builder.header("Wasmstore-Branch", branch);
72        }
73
74        if let Some(body) = body {
75            builder = builder.body(body);
76        }
77
78        let res = builder.send().await?;
79        Ok(res)
80    }
81
82    fn endpoint(&self, endpoint: &str) -> String {
83        let v = match self.version {
84            Version::V1 => "v1",
85        };
86        format!("/api/{v}{endpoint}")
87    }
88
89    pub async fn find(&self, path: impl Into<Path>) -> Result<Option<(Vec<u8>, Hash)>, Error> {
90        let path = path.into();
91        let p = format!("/module/{}", path.to_string());
92        let res = self
93            .request(reqwest::Method::GET, self.endpoint(&p), None)
94            .await?;
95        if res.status() == reqwest::StatusCode::NOT_FOUND {
96            return Ok(None);
97        }
98
99        if !res.status().is_success() {
100            return Err(Error::msg(res.text().await?));
101        }
102        let hash = res
103            .headers()
104            .get("Wasmstore-Hash")
105            .expect("Wasmstore-Hash header is unset in find response")
106            .to_str()?
107            .to_string();
108        let b = res.bytes().await?;
109        Ok(Some((b.to_vec(), Hash(hash))))
110    }
111
112    pub async fn hash(&self, path: impl Into<Path>) -> Result<Option<Hash>, Error> {
113        let path = path.into();
114        let p = format!("/hash/{}", path.to_string());
115        let res = self
116            .request(reqwest::Method::GET, self.endpoint(&p), None)
117            .await?;
118        if res.status() == reqwest::StatusCode::NOT_FOUND {
119            return Ok(None);
120        }
121
122        if !res.status().is_success() {
123            return Err(Error::msg(res.text().await?));
124        }
125
126        let b = res.text().await?;
127        Ok(Some(Hash(b)))
128    }
129
130    pub async fn add(&self, path: impl Into<Path>, data: Vec<u8>) -> Result<Hash, Error> {
131        let path = path.into();
132        let p = format!("/module/{}", path.to_string());
133        let res = self
134            .request(reqwest::Method::POST, self.endpoint(&p), Some(data))
135            .await?;
136
137        if !res.status().is_success() {
138            return Err(Error::msg(res.text().await?));
139        }
140
141        let b = res.text().await?;
142        Ok(Hash(b))
143    }
144
145    pub async fn remove(&self, path: impl Into<Path>) -> Result<(), Error> {
146        let path = path.into();
147        let p = format!("/module/{}", path.to_string());
148        let res = self
149            .request(reqwest::Method::DELETE, self.endpoint(&p), None)
150            .await?;
151        if !res.status().is_success() {
152            return Err(Error::msg(res.text().await?));
153        }
154        Ok(())
155    }
156
157    pub async fn gc(&self) -> Result<(), Error> {
158        let res = self
159            .request(reqwest::Method::POST, self.endpoint("/gc"), None)
160            .await?;
161        if !res.status().is_success() {
162            return Err(Error::msg(res.text().await?));
163        }
164        Ok(())
165    }
166
167    pub async fn list(
168        &self,
169        path: impl Into<Path>,
170    ) -> Result<std::collections::BTreeMap<Path, Hash>, Error> {
171        let path = path.into();
172        let p = format!("/modules/{}", path.to_string());
173        let res = self
174            .request(reqwest::Method::GET, self.endpoint(&p), None)
175            .await?;
176        if !res.status().is_success() {
177            return Err(Error::msg(res.text().await?));
178        }
179        let res: std::collections::BTreeMap<String, String> = res.json().await?;
180        Ok(res
181            .into_iter()
182            .map(|(k, v)| (Path::String(k), Hash(v)))
183            .collect())
184    }
185
186    pub async fn versions(&self, path: impl Into<Path>) -> Result<Vec<(Hash, Commit)>, Error> {
187        let url = format!("/versions/{}", path.into().to_string());
188        let res = self
189            .request(reqwest::Method::GET, self.endpoint(&url), None)
190            .await?;
191        if !res.status().is_success() {
192            return Err(Error::msg(res.text().await?));
193        }
194        Ok(res.json().await?)
195    }
196
197    pub async fn branches(&self) -> Result<Vec<String>, Error> {
198        let res = self
199            .request(reqwest::Method::GET, self.endpoint("/branches"), None)
200            .await?;
201        if !res.status().is_success() {
202            return Err(Error::msg(res.text().await?));
203        }
204        Ok(res.json().await?)
205    }
206
207    pub async fn create_branch(&self, name: impl AsRef<str>) -> Result<(), Error> {
208        let p = format!("/branch/{}", name.as_ref());
209        let res = self
210            .request(reqwest::Method::POST, self.endpoint(&p), None)
211            .await?;
212        if !res.status().is_success() {
213            return Err(Error::msg(res.text().await?));
214        }
215        Ok(())
216    }
217
218    pub async fn delete_branch(&self, name: impl AsRef<str>) -> Result<(), Error> {
219        let p = format!("/branch/{}", name.as_ref());
220        let res = self
221            .request(reqwest::Method::DELETE, self.endpoint(&p), None)
222            .await?;
223        if !res.status().is_success() {
224            return Err(Error::msg(res.text().await?));
225        }
226        Ok(())
227    }
228
229    pub async fn snapshot(&self) -> Result<Commit, Error> {
230        let res = self
231            .request(reqwest::Method::GET, self.endpoint("/snapshot"), None)
232            .await?;
233        if !res.status().is_success() {
234            return Err(Error::msg(res.text().await?));
235        }
236        let hash = res.text().await?;
237        Ok(Commit(hash))
238    }
239
240    pub async fn restore(&self, hash: &Commit) -> Result<(), Error> {
241        let res = self
242            .request(
243                reqwest::Method::POST,
244                self.endpoint(&format!("/restore/{}", hash.0)),
245                None,
246            )
247            .await?;
248        if !res.status().is_success() {
249            return Err(Error::msg(res.text().await?));
250        }
251        Ok(())
252    }
253
254    pub async fn restore_path(&self, hash: &Commit, path: impl Into<Path>) -> Result<(), Error> {
255        let res = self
256            .request(
257                reqwest::Method::POST,
258                self.endpoint(&format!("/restore/{}/{}", hash.0, path.into().to_string())),
259                None,
260            )
261            .await?;
262        if !res.status().is_success() {
263            return Err(Error::msg(res.text().await?));
264        }
265        Ok(())
266    }
267
268    pub async fn rollback(&self, path: impl Into<Path>) -> Result<(), Error> {
269        let res = self
270            .request(
271                reqwest::Method::POST,
272                self.endpoint(&format!("/rollback/{}", path.into().to_string())),
273                None,
274            )
275            .await?;
276        if !res.status().is_success() {
277            return Err(Error::msg(res.text().await?));
278        }
279        Ok(())
280    }
281
282    pub async fn contains(&self, path: impl Into<Path>) -> Result<bool, Error> {
283        let res = self
284            .request(
285                reqwest::Method::HEAD,
286                self.endpoint(&format!("/module/{}", path.into().to_string())),
287                None,
288            )
289            .await?;
290        Ok(res.status().is_success())
291    }
292
293    pub async fn set(&self, path: impl Into<Path>, hash: &Hash) -> Result<Hash, Error> {
294        let path = path.into();
295        let p = format!("/hash/{}/{}", hash.0, path.to_string());
296        let res = self
297            .request(reqwest::Method::POST, self.endpoint(&p), None)
298            .await?;
299
300        if !res.status().is_success() {
301            return Err(Error::msg(res.text().await?));
302        }
303
304        let b = res.text().await?;
305        Ok(Hash(b))
306    }
307
308    pub async fn commit_info(&self, commit: &Commit) -> Result<CommitInfo, Error> {
309        let p = format!("/commit/{}", commit.0);
310        let res = self
311            .request(reqwest::Method::GET, self.endpoint(&p), None)
312            .await?;
313        if !res.status().is_success() {
314            return Err(Error::msg(res.text().await?));
315        }
316        let res: CommitInfo = res.json().await?;
317        Ok(res)
318    }
319
320    pub async fn auth(&self, method: reqwest::Method) -> Result<bool, Error> {
321        let res = self.request(method, self.endpoint("/auth"), None).await?;
322        Ok(res.status() == 200)
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use crate::*;
329
330    #[tokio::test]
331    async fn basic_test() {
332        let client = Client::new("http://127.0.0.1:6384", Version::V1).unwrap();
333
334        let data = std::fs::read("../../test/a.wasm").unwrap();
335        let hash = client.add("test.wasm", data).await;
336        println!("HASH: {hash:?}");
337
338        let data = client.find(hash.unwrap()).await.unwrap();
339        let data1 = client.find("test.wasm").await.unwrap();
340
341        let hash = client.snapshot().await.unwrap();
342        client.commit_info(&hash).await.unwrap();
343
344        client.remove("test.wasm").await.unwrap();
345
346        client.restore(&hash).await.unwrap();
347
348        assert!(data.is_some());
349        assert!(data1.is_some());
350        assert_eq!(data, data1);
351    }
352}