1use std::{env, fmt};
11
12use hyphae_client::{ClientConfigError, HyphaeClient};
13use thiserror::Error;
14
15pub const BASE_URL_ENV: &str = "HYPHAE_BASE_URL";
17pub const BEARER_TOKEN_ENV: &str = "HYPHAE_BEARER_TOKEN";
19
20#[derive(Clone, Eq, PartialEq)]
22pub struct PliegoHyphaeConfig {
23 base_url: String,
24 bearer_token: Option<String>,
25}
26
27impl PliegoHyphaeConfig {
28 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 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 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#[derive(Clone, Debug)]
94pub struct PliegoHyphae {
95 client: HyphaeClient,
96}
97
98impl PliegoHyphae {
99 pub fn client(&self) -> &HyphaeClient {
101 &self.client
102 }
103}
104
105#[derive(Debug, Error, Eq, PartialEq)]
107pub enum PliegoHyphaeConfigError {
108 #[error("{name} is not valid Unicode")]
110 InvalidEnvironmentEncoding {
111 name: &'static str,
113 },
114 #[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}