percas_client/
client.rs

1// Copyright 2025 ScopeDB <contact@scopedb.io>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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}