1use crate::config::{load_chipzen_config, resolve_token, resolve_url, ChipzenConfig};
31use crate::error::Error;
32use crate::retry::RetryPolicy;
33
34pub const ENV_NAMES: &[&str] = &["prod", "staging", "local"];
37
38pub const CHIPZEN_ENV_VAR: &str = "CHIPZEN_ENV";
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum EnvName {
45 Prod,
46 Staging,
47 Local,
48}
49
50impl EnvName {
51 pub fn parse(s: &str) -> Option<Self> {
54 match s {
55 "prod" => Some(EnvName::Prod),
56 "staging" => Some(EnvName::Staging),
57 "local" => Some(EnvName::Local),
58 _ => None,
59 }
60 }
61
62 pub fn as_str(&self) -> &'static str {
63 match self {
64 EnvName::Prod => "prod",
65 EnvName::Staging => "staging",
66 EnvName::Local => "local",
67 }
68 }
69
70 fn lobby_url(&self, bot_id: &str) -> String {
72 match self {
73 EnvName::Prod => format!("wss://chipzen.ai/ws/external/bot/{bot_id}"),
74 EnvName::Staging => format!("wss://staging.chipzen.ai/ws/external/bot/{bot_id}"),
75 EnvName::Local => format!("ws://localhost:8001/ws/external/bot/{bot_id}"),
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
82pub struct ConnectionConfig {
83 pub url: String,
86 pub token: Option<String>,
90 pub retry_policy: RetryPolicy,
92 pub env: Option<EnvName>,
95 pub config: Option<ChipzenConfig>,
98}
99
100fn resolve_env_name(explicit: Option<EnvName>, env_var: Option<&str>) -> Result<EnvName, Error> {
105 if let Some(env) = explicit {
106 return Ok(env);
107 }
108 if let Some(val) = env_var.filter(|s| !s.is_empty()) {
111 return EnvName::parse(val).ok_or_else(|| {
112 Error::Protocol(format!(
113 "{CHIPZEN_ENV_VAR}={val:?} is not a recognized environment. Valid values: {}.",
114 ENV_NAMES.join(", ")
115 ))
116 });
117 }
118 Ok(EnvName::Prod)
119}
120
121pub fn connect_to_chipzen(
140 bot_id: &str,
141 env: Option<EnvName>,
142 retry_policy: Option<RetryPolicy>,
143 config: Option<ChipzenConfig>,
144) -> Result<ConnectionConfig, Error> {
145 if bot_id.is_empty() {
146 return Err(Error::Protocol(
147 "connect_to_chipzen() requires a non-empty bot_id. Pass the external-API \
148 bot UUID issued by the Chipzen platform."
149 .to_string(),
150 ));
151 }
152
153 let env_var = std::env::var(CHIPZEN_ENV_VAR).ok();
156 let resolved_env = resolve_env_name(env, env_var.as_deref())?;
157 let env_derived_url = resolved_env.lobby_url(bot_id);
158
159 let config = match config {
161 Some(c) => Some(c),
162 None => load_chipzen_config(None)?,
163 };
164
165 let (url, env_for_return) = match resolve_url(None, config.as_ref()) {
166 Some(config_url) => (config_url, None),
167 None => (env_derived_url, Some(resolved_env)),
168 };
169
170 let token = resolve_token(None, config.as_ref());
171 let policy = retry_policy.unwrap_or_default();
172
173 Ok(ConnectionConfig {
174 url,
175 token,
176 retry_policy: policy,
177 env: env_for_return,
178 config,
179 })
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn env_name_parse_round_trips() {
188 for name in ENV_NAMES {
189 assert_eq!(EnvName::parse(name).unwrap().as_str(), *name);
190 }
191 assert!(EnvName::parse("prd").is_none());
192 }
193
194 #[test]
195 fn lobby_url_templates_match_python() {
196 assert_eq!(
197 EnvName::Prod.lobby_url("b"),
198 "wss://chipzen.ai/ws/external/bot/b"
199 );
200 assert_eq!(
201 EnvName::Staging.lobby_url("b"),
202 "wss://staging.chipzen.ai/ws/external/bot/b"
203 );
204 assert_eq!(
205 EnvName::Local.lobby_url("b"),
206 "ws://localhost:8001/ws/external/bot/b"
207 );
208 }
209
210 #[test]
211 fn resolve_env_explicit_wins() {
212 let env = resolve_env_name(Some(EnvName::Local), Some("staging")).unwrap();
214 assert_eq!(env, EnvName::Local);
215 }
216
217 #[test]
218 fn resolve_env_uses_env_var_then_default() {
219 assert_eq!(
220 resolve_env_name(None, Some("staging")).unwrap(),
221 EnvName::Staging
222 );
223 assert_eq!(resolve_env_name(None, Some("")).unwrap(), EnvName::Prod);
225 assert_eq!(resolve_env_name(None, None).unwrap(), EnvName::Prod);
227 }
228
229 #[test]
230 fn resolve_env_rejects_unknown_env_var() {
231 let err = resolve_env_name(None, Some("prd")).unwrap_err();
232 assert!(format!("{err}").contains("not a recognized environment"));
233 }
234
235 #[test]
236 fn connect_requires_bot_id() {
237 let err = connect_to_chipzen("", Some(EnvName::Prod), None, None).unwrap_err();
238 assert!(format!("{err}").contains("non-empty bot_id"));
239 }
240
241 #[test]
242 fn connect_builds_env_derived_url() {
243 let conn = connect_to_chipzen(
244 "abc",
245 Some(EnvName::Staging),
246 None,
247 Some(ChipzenConfig::default()),
248 )
249 .unwrap();
250 assert_eq!(conn.url, "wss://staging.chipzen.ai/ws/external/bot/abc");
251 assert_eq!(conn.env, Some(EnvName::Staging));
252 }
253
254 #[test]
255 fn connect_config_url_overrides_env_derived() {
256 let cfg = ChipzenConfig {
257 url: Some("wss://verbatim/url".into()),
258 token: Some("cz_extbot_t".into()),
259 ..Default::default()
260 };
261 let conn = connect_to_chipzen("abc", Some(EnvName::Prod), None, Some(cfg)).unwrap();
262 assert_eq!(conn.url, "wss://verbatim/url");
263 assert_eq!(conn.env, None);
265 assert_eq!(conn.token.as_deref(), Some("cz_extbot_t"));
266 }
267}