Skip to main content

hyphae_pliegors/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Optional `PliegoRS` application-state boundary for the public Hyphae client.
4//!
5//! This crate deliberately does not depend on or import `PliegoRS` internals.
6//! A `PliegoRS` application may add [`PliegoHyphae`] to its own public state
7//! mechanism. Omitting this crate or both environment values leaves the host
8//! completely independent of Hyphae.
9
10use std::{env, fmt};
11
12use hyphae_client::{ClientConfigError, HyphaeClient};
13use thiserror::Error;
14
15/// Environment variable containing the root public Hyphae HTTP(S) origin.
16pub const BASE_URL_ENV: &str = "HYPHAE_BASE_URL";
17/// Environment variable containing the optional bearer secret.
18pub const BEARER_TOKEN_ENV: &str = "HYPHAE_BEARER_TOKEN";
19
20/// Optional adapter configuration containing only public client inputs.
21#[derive(Clone, Eq, PartialEq)]
22pub struct PliegoHyphaeConfig {
23    base_url: String,
24    bearer_token: Option<String>,
25}
26
27impl PliegoHyphaeConfig {
28    /// Resolves optional integration state from process environment.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error when a bearer secret exists without a Hyphae origin,
33    /// or when either environment value is not valid Unicode.
34    pub fn from_env() -> Result<Option<Self>, PliegoHyphaeConfigError> {
35        let base_url = read_optional_env(BASE_URL_ENV)?;
36        let bearer_token = read_optional_env(BEARER_TOKEN_ENV)?;
37        Self::from_optional_values(base_url, bearer_token)
38    }
39
40    /// Resolves optional integration state from explicitly supplied values.
41    ///
42    /// `None, None` is the normal disabled state. This constructor makes that
43    /// behavior testable without mutating process-global environment.
44    ///
45    /// # Errors
46    ///
47    /// Rejects a bearer secret without a corresponding base URL.
48    pub fn from_optional_values(
49        base_url: Option<String>,
50        bearer_token: Option<String>,
51    ) -> Result<Option<Self>, PliegoHyphaeConfigError> {
52        match (base_url, bearer_token) {
53            (None, None) => Ok(None),
54            (None, Some(_)) => Err(PliegoHyphaeConfigError::TokenWithoutBaseUrl),
55            (Some(base_url), bearer_token) => Ok(Some(Self {
56                base_url,
57                bearer_token,
58            })),
59        }
60    }
61
62    /// Builds a reusable adapter without opening a listener or data directory.
63    ///
64    /// # Errors
65    ///
66    /// Returns public-client configuration errors for malformed origins,
67    /// secrets, timeouts, or response limits.
68    pub fn build(self) -> Result<PliegoHyphae, ClientConfigError> {
69        let mut builder = HyphaeClient::builder(&self.base_url)?;
70        if let Some(token) = self.bearer_token {
71            builder = builder.bearer_token(&token)?;
72        }
73        Ok(PliegoHyphae {
74            client: builder.build()?,
75        })
76    }
77}
78
79impl fmt::Debug for PliegoHyphaeConfig {
80    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
81        formatter
82            .debug_struct("PliegoHyphaeConfig")
83            .field("base_url", &self.base_url)
84            .field(
85                "bearer_token",
86                &self.bearer_token.as_ref().map(|_| "[REDACTED]"),
87            )
88            .finish()
89    }
90}
91
92/// Cloneable public client state for opt-in `PliegoRS` applications.
93#[derive(Clone, Debug)]
94pub struct PliegoHyphae {
95    client: HyphaeClient,
96}
97
98impl PliegoHyphae {
99    /// Borrows the versioned public Hyphae HTTP client.
100    pub fn client(&self) -> &HyphaeClient {
101        &self.client
102    }
103}
104
105/// Errors while deciding whether the optional integration is enabled.
106#[derive(Debug, Error, Eq, PartialEq)]
107pub enum PliegoHyphaeConfigError {
108    /// One environment value was not valid Unicode.
109    #[error("{name} is not valid Unicode")]
110    InvalidEnvironmentEncoding {
111        /// Environment variable name.
112        name: &'static str,
113    },
114    /// A secret alone is ambiguous and must never imply a default endpoint.
115    #[error("HYPHAE_BEARER_TOKEN requires HYPHAE_BASE_URL")]
116    TokenWithoutBaseUrl,
117}
118
119fn read_optional_env(name: &'static str) -> Result<Option<String>, PliegoHyphaeConfigError> {
120    let Some(value) = env::var_os(name) else {
121        return Ok(None);
122    };
123    value
124        .into_string()
125        .map(Some)
126        .map_err(|_| PliegoHyphaeConfigError::InvalidEnvironmentEncoding { name })
127}
128
129#[cfg(test)]
130mod tests {
131    use super::{PliegoHyphaeConfig, PliegoHyphaeConfigError};
132
133    #[test]
134    fn absent_configuration_is_a_normal_disabled_state() {
135        assert_eq!(
136            PliegoHyphaeConfig::from_optional_values(None, None),
137            Ok(None)
138        );
139    }
140
141    #[test]
142    fn a_secret_never_selects_an_implicit_endpoint() {
143        assert_eq!(
144            PliegoHyphaeConfig::from_optional_values(None, Some("secret".to_owned())),
145            Err(PliegoHyphaeConfigError::TokenWithoutBaseUrl)
146        );
147    }
148
149    #[test]
150    fn enabled_adapter_uses_only_a_public_origin() -> Result<(), Box<dyn std::error::Error>> {
151        let config = PliegoHyphaeConfig::from_optional_values(
152            Some("http://127.0.0.1:8787".to_owned()),
153            None,
154        )?
155        .ok_or("adapter should be enabled")?;
156        let adapter = config.build()?;
157        let _public_client = adapter.client();
158        Ok(())
159    }
160
161    #[test]
162    fn debug_output_redacts_bearer_secret() -> Result<(), Box<dyn std::error::Error>> {
163        let config = PliegoHyphaeConfig::from_optional_values(
164            Some("https://example.test".to_owned()),
165            Some("do-not-print".to_owned()),
166        )?
167        .ok_or("adapter should be enabled")?;
168        let debug = format!("{config:?}");
169        assert!(debug.contains("[REDACTED]"));
170        assert!(!debug.contains("do-not-print"));
171        Ok(())
172    }
173}