1use error_stack::Result;
16use error_stack::ResultExt;
17use error_stack::bail;
18use reqwest::IntoUrl;
19use reqwest::StatusCode;
20use reqwest::Url;
21
22#[derive(Debug, thiserror::Error)]
23#[error("{0}")]
24pub enum Error {
25 #[from(std::io::Error)]
26 IO(std::io::Error),
27 #[from(reqwest::Error)]
28 Http(reqwest::Error),
29
30 Other(String),
31}
32
33pub struct ClientBuilder {
34 endpoint: String,
35}
36
37impl ClientBuilder {
38 pub fn new(endpoint: String) -> Self {
39 Self { endpoint }
40 }
41
42 pub fn build(self) -> Result<Client, Error> {
43 let builder = reqwest::ClientBuilder::new().no_proxy();
44 Client::new(self.endpoint, builder)
45 }
46}
47
48pub struct Client {
49 client: reqwest::Client,
50 base_url: Url,
51}
52
53impl Client {
54 pub async fn get(&self, key: &str) -> Result<Option<Vec<u8>>, Error> {
55 do_get(self, key).await
56 }
57
58 pub async fn put(&self, key: &str, value: &[u8]) -> Result<(), Error> {
59 do_put(self, key, value).await
60 }
61
62 pub async fn delete(&self, key: &str) -> Result<(), Error> {
63 do_delete(self, key).await
64 }
65
66 fn new(base_url: impl IntoUrl, builder: reqwest::ClientBuilder) -> Result<Self, Error> {
67 let client = builder.build().map_err(Error::Http)?;
68 let base_url = base_url.into_url().map_err(Error::Http)?;
69 Ok(Client { client, base_url })
70 }
71}
72
73async fn do_get(client: &Client, key: &str) -> Result<Option<Vec<u8>>, Error> {
74 let make_error = || Error::Other("failed to get".to_string());
75
76 let url = client.base_url.join(key).change_context_lazy(make_error)?;
77 let resp = client
78 .client
79 .get(url)
80 .send()
81 .await
82 .change_context_lazy(make_error)?;
83
84 match resp.status() {
85 StatusCode::NOT_FOUND => Ok(None),
86 StatusCode::OK => {
87 let body = resp.bytes().await.change_context_lazy(make_error)?;
88 Ok(Some(body.to_vec()))
89 }
90 _ => bail!(Error::Other(resp.status().to_string())),
91 }
92}
93
94async fn do_put(client: &Client, key: &str, value: &[u8]) -> Result<(), Error> {
95 let make_error = || Error::Other("failed to put".to_string());
96
97 let url = client.base_url.join(key).change_context_lazy(make_error)?;
98 let resp = client
99 .client
100 .put(url)
101 .body(value.to_vec())
102 .send()
103 .await
104 .change_context_lazy(make_error)?;
105
106 match resp.status() {
107 StatusCode::OK | StatusCode::CREATED => Ok(()),
108 _ => bail!(Error::Other(resp.status().to_string())),
109 }
110}
111
112async fn do_delete(client: &Client, key: &str) -> Result<(), Error> {
113 let make_error = || Error::Other("failed to delete".to_string());
114
115 let url = client.base_url.join(key).change_context_lazy(make_error)?;
116 let resp = client
117 .client
118 .delete(url)
119 .send()
120 .await
121 .change_context_lazy(make_error)?;
122
123 match resp.status() {
124 StatusCode::OK | StatusCode::NO_CONTENT => Ok(()),
125 _ => bail!(Error::Other(resp.status().to_string())),
126 }
127}