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