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