Skip to main content

bee_tui/api/
mod.rs

1#![allow(dead_code)] // wired up in the next commit (api → app → watch).
2
3//! Thin wrapper around [`bee::Client`] that binds it to a configured
4//! [`NodeConfig`]. The cockpit talks to Bee through one [`ApiClient`]
5//! per active profile.
6//!
7//! Construction:
8//!
9//! ```ignore
10//! use crate::api::ApiClient;
11//! let client = ApiClient::from_node(&node_config)?;
12//! let rtt = client.bee().ping().await?;
13//! ```
14//!
15//! Multi-node UX (v0.4) will hold a `HashMap<String, ApiClient>`
16//! keyed by node name; the screen-level `:context` command swaps the
17//! active key.
18
19use color_eyre::eyre::{WrapErr, eyre};
20
21use crate::config::NodeConfig;
22
23/// Active connection to one Bee node. Cheap to clone (`bee::Client`
24/// is `Arc<Inner>` under the hood).
25#[derive(Clone, Debug)]
26pub struct ApiClient {
27    /// Friendly profile name shown in the header bar.
28    pub name: String,
29    /// Resolved base URL (the scheme has been validated to be `http`
30    /// or `https`).
31    pub url: String,
32    /// Whether the node was configured with a bearer token. We don't
33    /// store the token itself — `bee::Client` keeps it.
34    pub authenticated: bool,
35    inner: bee::Client,
36}
37
38impl ApiClient {
39    /// Build an [`ApiClient`] from a [`NodeConfig`]. Resolves the
40    /// optional `@env:VAR` indirection and wires the bearer token via
41    /// [`bee::Client::with_token`] when present.
42    pub fn from_node(node: &NodeConfig) -> color_eyre::Result<Self> {
43        let url = node.url.trim_end_matches('/').to_string();
44        let token = node.resolved_token();
45        let authenticated = token.is_some();
46        let inner = match token.as_deref() {
47            Some(t) => bee::Client::with_token(&url, t)
48                .map_err(|e| eyre!("invalid bee endpoint or token: {e}"))?,
49            None => bee::Client::new(&url).map_err(|e| eyre!("invalid bee endpoint: {e}"))?,
50        };
51        Ok(Self {
52            name: node.name.clone(),
53            url,
54            authenticated,
55            inner,
56        })
57    }
58
59    /// Borrow the underlying [`bee::Client`] for direct API calls.
60    pub fn bee(&self) -> &bee::Client {
61        &self.inner
62    }
63
64    /// Health round-trip latency. Convenience over `self.bee().ping()`
65    /// for the connection-status indicator.
66    pub async fn ping(&self) -> color_eyre::Result<std::time::Duration> {
67        self.inner
68            .ping()
69            .await
70            .wrap_err_with(|| format!("ping {} ({}) failed", self.name, self.url))
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    fn node(url: &str, token: Option<&str>) -> NodeConfig {
79        NodeConfig {
80            name: "test".into(),
81            url: url.into(),
82            token: token.map(String::from),
83            default: false,
84        }
85    }
86
87    #[test]
88    fn from_node_strips_trailing_slash() {
89        let c = ApiClient::from_node(&node("http://localhost:1633/", None)).unwrap();
90        assert_eq!(c.url, "http://localhost:1633");
91        assert!(!c.authenticated);
92    }
93
94    #[test]
95    fn from_node_with_token_marks_authenticated() {
96        let c = ApiClient::from_node(&node("http://localhost:1633", Some("dummy-jwt"))).unwrap();
97        assert!(c.authenticated);
98    }
99
100    #[test]
101    fn from_node_rejects_bogus_url() {
102        let err = ApiClient::from_node(&node("not a url", None)).unwrap_err();
103        assert!(format!("{err}").contains("invalid"));
104    }
105}