Skip to main content

bee/
client.rs

1//! Top-level [`Client`] and the shared [`Inner`] HTTP plumbing.
2//!
3//! Mirrors bee-go's `bee.Client`. A `Client` is cheaply cloneable
4//! (`Arc<Inner>`) and yields per-domain handles via accessors:
5//!
6//! ```no_run
7//! # use bee::Client;
8//! # async fn run() -> Result<(), bee::Error> {
9//! let client = Client::new("http://localhost:1633")?;
10//! let health = client.debug().health().await?;
11//! # Ok(()) }
12//! ```
13
14use std::sync::Arc;
15
16use reqwest::{Method, RequestBuilder};
17use serde::de::DeserializeOwned;
18use url::Url;
19
20use crate::api::HeaderPairs;
21use crate::swarm::{Error, RESPONSE_BODY_CAP};
22
23/// Shared HTTP/state used by every sub-service.
24#[derive(Debug)]
25pub(crate) struct Inner {
26    pub(crate) base_url: Url,
27    pub(crate) http: reqwest::Client,
28}
29
30impl Inner {
31    /// Resolve a path against the base URL. The base URL is normalized
32    /// to end with `/`, so `path` is treated as relative.
33    pub(crate) fn url(&self, path: &str) -> Result<Url, Error> {
34        self.base_url
35            .join(path)
36            .map_err(|e| Error::argument(format!("invalid url: {e}")))
37    }
38
39    /// Build a request, send it, and translate non-2xx responses into
40    /// [`Error::Response`] with method / URL / capped body captured.
41    pub(crate) async fn send(&self, builder: RequestBuilder) -> Result<reqwest::Response, Error> {
42        let request = builder.build()?;
43        let method = request.method().to_string();
44        let url = request.url().to_string();
45        let resp = self.http.execute(request).await?;
46        if resp.status().is_success() {
47            return Ok(resp);
48        }
49        let status = resp.status().as_u16();
50        let status_text = format!(
51            "{status} {}",
52            resp.status().canonical_reason().unwrap_or("")
53        )
54        .trim_end()
55        .to_string();
56        let body = resp.bytes().await.map(|b| b.to_vec()).unwrap_or_default();
57        let n = body.len().min(RESPONSE_BODY_CAP);
58        Err(Error::Response {
59            method,
60            url,
61            status,
62            status_text,
63            body: body[..n].to_vec(),
64        })
65    }
66
67    /// Send and parse the response body as JSON.
68    pub(crate) async fn send_json<T: DeserializeOwned>(
69        &self,
70        builder: RequestBuilder,
71    ) -> Result<T, Error> {
72        let resp = self.send(builder).await?;
73        let bytes = resp.bytes().await?;
74        Ok(serde_json::from_slice(&bytes)?)
75    }
76
77    /// Apply a list of header pairs to a request builder.
78    pub(crate) fn apply_headers(builder: RequestBuilder, headers: HeaderPairs) -> RequestBuilder {
79        let mut b = builder;
80        for (name, value) in headers {
81            b = b.header(name, value);
82        }
83        b
84    }
85}
86
87/// Top-level Bee API client.
88#[derive(Clone, Debug)]
89pub struct Client {
90    pub(crate) inner: Arc<Inner>,
91}
92
93impl Client {
94    /// Construct a client from a base URL (e.g. `"http://localhost:1633"`).
95    /// A trailing slash is appended if missing so relative paths resolve
96    /// correctly.
97    pub fn new(url: &str) -> Result<Self, Error> {
98        let mut owned = url.to_owned();
99        if !owned.ends_with('/') {
100            owned.push('/');
101        }
102        let base_url =
103            Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
104        let http = reqwest::Client::builder()
105            .build()
106            .map_err(Error::Transport)?;
107        Ok(Self {
108            inner: Arc::new(Inner { base_url, http }),
109        })
110    }
111
112    /// Construct a client with a caller-provided [`reqwest::Client`].
113    /// Use this to share a connection pool with other code or to set
114    /// custom timeouts / TLS roots.
115    pub fn with_http_client(url: &str, http: reqwest::Client) -> Result<Self, Error> {
116        let mut owned = url.to_owned();
117        if !owned.ends_with('/') {
118            owned.push('/');
119        }
120        let base_url =
121            Url::parse(&owned).map_err(|e| Error::argument(format!("invalid url: {e}")))?;
122        Ok(Self {
123            inner: Arc::new(Inner { base_url, http }),
124        })
125    }
126
127    /// Borrow the configured base URL.
128    pub fn base_url(&self) -> &Url {
129        &self.inner.base_url
130    }
131
132    /// Sub-service: file / data / chunk / SOC / feed / collection
133    /// uploads and downloads.
134    pub fn file(&self) -> crate::file::FileApi {
135        crate::file::FileApi::new(self.inner.clone())
136    }
137
138    /// Sub-service: postage batch CRUD + stamp metadata. Stamp math
139    /// helpers live as free functions in [`crate::postage`].
140    pub fn postage(&self) -> crate::postage::PostageApi {
141        crate::postage::PostageApi::new(self.inner.clone())
142    }
143
144    /// Sub-service: debug / operator endpoints (health, versions,
145    /// peers, accounting, chequebook, stake).
146    pub fn debug(&self) -> crate::debug::DebugApi {
147        crate::debug::DebugApi::new(self.inner.clone())
148    }
149
150    /// Sub-service: generic `/api/*` endpoints (pin, tag, stewardship,
151    /// grantee, envelope).
152    pub fn api(&self) -> crate::api::ApiService {
153        crate::api::ApiService::new(self.inner.clone())
154    }
155
156    /// Sub-service: PSS send + websocket subscribe / receive.
157    pub fn pss(&self) -> crate::pss::PssApi {
158        crate::pss::PssApi::new(self.inner.clone())
159    }
160
161    /// Sub-service: GSOC send + websocket subscribe.
162    pub fn gsoc(&self) -> crate::gsoc::GsocApi {
163        crate::gsoc::GsocApi::new(self.inner.clone())
164    }
165}
166
167/// Shorthand: build a `RequestBuilder` for `(method, path)` against
168/// the inner HTTP client.
169pub(crate) fn request(inner: &Inner, method: Method, path: &str) -> Result<RequestBuilder, Error> {
170    let url = inner.url(path)?;
171    Ok(inner.http.request(method, url))
172}