Skip to main content

agentcat_sdk/config/
env.rs

1//! The environment the SDK reads: the names of its variables, where it reads them (the process,
2//! or a map in tests), the API base URL they give and the switch of `AGENTCAT_ENABLED`. Both
3//! [`AgentCat::new`](crate::AgentCat::new) and [`AgentCat::from_env`](crate::AgentCat::from_env)
4//! read it; `from_env` also takes its project ID from it.
5use std::fmt;
6use std::path::PathBuf;
7
8use super::Options;
9use crate::wire;
10
11/// The environment variable that holds the project ID when none is passed to the SDK.
12pub const AGENTCAT_PROJECT_ID: &str = "AGENTCAT_PROJECT_ID";
13
14/// The environment variable that holds the base URL of the `AgentCat` API.
15pub const AGENTCAT_API_URL: &str = "AGENTCAT_API_URL";
16
17/// The environment variable under the former name of [`AGENTCAT_API_URL`], read when that one is
18/// unset or empty.
19pub const MCPCAT_API_URL: &str = "MCPCAT_API_URL";
20
21/// The environment variable that turns the SDK off: `false`, `0`, `no` or `off` (any case,
22/// surrounding white space ignored) disables it.
23pub const AGENTCAT_ENABLED: &str = "AGENTCAT_ENABLED";
24
25/// The base URL of the `AgentCat` API when neither the options nor the environment name one.
26pub(crate) const DEFAULT_API_BASE_URL: &str = "https://api.agentcat.com";
27
28/// Where the SDK reads its environment: the process, or a map in tests (the one place that
29/// touches `std::env`).
30pub(crate) trait Env: Send + Sync + fmt::Debug {
31    /// The value of the variable `name`, `None` when it is unset or not text.
32    fn var(&self, name: &str) -> Option<String>;
33
34    /// The user's home directory.
35    fn home_dir(&self) -> Option<PathBuf>;
36}
37
38/// The environment of the running process.
39#[derive(Debug, Clone, Copy)]
40pub(crate) struct ProcessEnv;
41
42impl Env for ProcessEnv {
43    #[expect(
44        clippy::disallowed_methods,
45        reason = "the one place that reads the process environment"
46    )]
47    fn var(&self, name: &str) -> Option<String> {
48        std::env::var(name).ok()
49    }
50
51    fn home_dir(&self) -> Option<PathBuf> {
52        std::env::home_dir()
53    }
54}
55
56/// A fixed environment for tests.
57#[cfg(test)]
58#[derive(Debug, Clone, Default)]
59pub(crate) struct MapEnv {
60    vars: std::collections::HashMap<String, String>,
61    home: Option<PathBuf>,
62}
63
64#[cfg(test)]
65impl MapEnv {
66    /// An environment with the variables `vars` and no home directory.
67    pub(crate) fn new(vars: &[(&str, &str)]) -> Self {
68        Self {
69            vars: vars
70                .iter()
71                .map(|&(name, value)| (name.to_owned(), value.to_owned()))
72                .collect(),
73            home: None,
74        }
75    }
76
77    /// The same environment with the home directory `home`.
78    pub(crate) fn with_home(mut self, home: PathBuf) -> Self {
79        self.home = Some(home);
80        self
81    }
82}
83
84#[cfg(test)]
85impl Env for MapEnv {
86    fn var(&self, name: &str) -> Option<String> {
87        self.vars.get(name).cloned()
88    }
89
90    fn home_dir(&self) -> Option<PathBuf> {
91        self.home.clone()
92    }
93}
94
95/// The base URL events go to: the first non-empty of the option, `AGENTCAT_API_URL`,
96/// `MCPCAT_API_URL`, else the default. The value is used as given, with no trailing slash
97/// trimmed.
98pub(crate) fn resolve_api_base_url(options: &Options, env: &dyn Env) -> String {
99    options
100        .api_base_url_option()
101        .map(str::to_owned)
102        .or_else(|| env.var(AGENTCAT_API_URL).filter(|url| !url.is_empty()))
103        .or_else(|| env.var(MCPCAT_API_URL).filter(|url| !url.is_empty()))
104        .unwrap_or_else(|| DEFAULT_API_BASE_URL.to_owned())
105}
106
107/// Whether the SDK is on: off when `AGENTCAT_ENABLED`, trimmed ([`wire::trim`]) and lower-cased, is
108/// `false`, `0`, `no` or `off`; on otherwise, also when unset.
109pub(crate) fn sdk_enabled(env: &dyn Env) -> bool {
110    let Some(value) = env.var(AGENTCAT_ENABLED) else {
111        return true;
112    };
113    let value = wire::trim(&value).to_lowercase();
114    !matches!(value.as_str(), "false" | "0" | "no" | "off")
115}