freedom_config/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::{fmt::Debug, sync::Arc};
4
5use url::Url;
6
7#[cfg(feature = "serde")]
8mod ser;
9
10/// The ATLAS Environment
11#[derive(Debug, Clone)]
12pub struct Environment(Arc<dyn Env>);
13
14impl std::fmt::Display for Environment {
15    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16        f.write_str(self.0.as_ref().as_ref())
17    }
18}
19
20impl Default for Environment {
21    fn default() -> Self {
22        Self::test()
23    }
24}
25
26pub trait IntoEnv {
27    fn into(self) -> Environment;
28}
29
30impl<T: Env> IntoEnv for T {
31    fn into(self) -> Environment {
32        Environment::new(self)
33    }
34}
35
36impl IntoEnv for Environment {
37    fn into(self) -> Environment {
38        self
39    }
40}
41
42// NOTE: I can't really think of a reason we'd need DerefMut. Once we construct an environment
43// It shouldn't change during runtime.
44impl std::ops::Deref for Environment {
45    type Target = dyn Env;
46
47    fn deref(&self) -> &Self::Target {
48        self.0.as_ref()
49    }
50}
51
52impl Environment {
53    /// Construct an ATLAS environment
54    pub fn new<E: Env>(env: E) -> Self {
55        Self(Arc::new(env))
56    }
57
58    /// Construct an ATLAS environment object for the test environment
59    pub fn test() -> Self {
60        Self::new(Test)
61    }
62
63    /// Construct an ATLAS environment object for the production environment
64    pub fn prod() -> Self {
65        Self::new(Prod)
66    }
67}
68
69/// A wrapper around T which implements debug and display, without showing the underlying value.
70///
71/// This is intended to wrap sensitive information, and prevent it from being accidentally logged,
72/// or otherwise exposed
73#[cfg_attr(
74    feature = "serde",
75    derive(serde::Serialize, serde::Deserialize),
76    serde(transparent)
77)]
78#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
79pub struct Secret<T>(pub T);
80
81impl<T> From<T> for Secret<T> {
82    fn from(value: T) -> Self {
83        Secret(value)
84    }
85}
86
87impl<T> Secret<T> {
88    pub fn expose(&self) -> &T {
89        &self.0
90    }
91}
92
93impl<T> std::fmt::Display for Secret<T> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        <Self as std::fmt::Debug>::fmt(self, f)
96    }
97}
98
99impl<T> std::fmt::Debug for Secret<T> {
100    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101        f.debug_tuple("Secret").field(&"*****").finish()
102    }
103}
104
105/// Shared behavior for atlas environments
106pub trait Env: 'static + AsRef<str> + Debug + Send + Sync + Unpin {
107    fn from_str(val: &str) -> Option<Self>
108    where
109        Self: Sized;
110
111    /// The hostname of the FPS for the given environment
112    fn fps_host(&self) -> &str;
113
114    /// The entrypoint for the freedom API for the given environment
115    ///
116    /// # Note
117    ///
118    /// Each environment contains the path "/api" as all requests initiate from this point
119    fn freedom_entrypoint(&self) -> Url;
120}
121
122/// Type state for the test environment
123#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
124pub struct Test;
125
126impl AsRef<str> for Test {
127    fn as_ref(&self) -> &str {
128        "test"
129    }
130}
131
132impl Env for Test {
133    fn from_str(val: &str) -> Option<Self>
134    where
135        Self: Sized,
136    {
137        val.to_ascii_lowercase().eq("test").then_some(Self)
138    }
139
140    fn fps_host(&self) -> &str {
141        "fps.test.atlasground.com"
142    }
143
144    fn freedom_entrypoint(&self) -> Url {
145        Url::parse("https://test-api.atlasground.com/api/").unwrap()
146    }
147}
148
149/// Type state for the production environment
150#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
151pub struct Prod;
152
153impl AsRef<str> for Prod {
154    fn as_ref(&self) -> &str {
155        "prod"
156    }
157}
158
159impl Env for Prod {
160    fn from_str(val: &str) -> Option<Self>
161    where
162        Self: Sized,
163    {
164        val.to_ascii_lowercase().eq("prod").then_some(Self)
165    }
166
167    fn fps_host(&self) -> &str {
168        "fps.atlasground.com"
169    }
170
171    fn freedom_entrypoint(&self) -> Url {
172        Url::parse("https://api.atlasground.com/api/").unwrap()
173    }
174}
175
176/// The configuration object for Freedom.
177///
178/// Used when creating a Freedom API client
179#[derive(Clone, Debug)]
180#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
181pub struct Config {
182    environment: Environment,
183    key: String,
184    secret: Secret<String>,
185}
186
187impl PartialEq for Config {
188    fn eq(&self, other: &Self) -> bool {
189        self.environment_str() == other.environment_str()
190            && self.key == other.key
191            && self.secret == other.secret
192    }
193}
194
195/// Error enumeration for creating a Freedom Config
196#[non_exhaustive]
197#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
198pub enum Error {
199    /// Failed to parse the variable from the environment
200    ParseEnvironment,
201    /// Missing secret from builder
202    MissingSecret,
203    /// Missing key from builder
204    MissingKey,
205    /// Missing environment from builder
206    MissingEnvironment,
207}
208
209impl std::fmt::Display for Error {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        <Self as std::fmt::Debug>::fmt(self, f)
212    }
213}
214
215impl std::error::Error for Error {}
216
217/// Builder for the Freedom Config object
218#[derive(Default)]
219pub struct ConfigBuilder {
220    environment: Option<Environment>,
221    key: Option<String>,
222    secret: Option<Secret<String>>,
223}
224
225impl ConfigBuilder {
226    /// Construct an empty Config builder
227    pub fn new() -> Self {
228        Self::default()
229    }
230
231    /// Attempt to load the ATLAS environment from the environment
232    pub fn environment_from_env(&mut self) -> Result<&mut Self, Error> {
233        let var = std::env::var(Config::ATLAS_ENV_VAR).map_err(|_| Error::ParseEnvironment)?;
234
235        if let Some(env) = Test::from_str(&var) {
236            return Ok(self.environment(env));
237        }
238        if let Some(env) = Prod::from_str(&var) {
239            return Ok(self.environment(env));
240        }
241
242        Err(Error::ParseEnvironment)
243    }
244
245    /// Attempt to load the ATLAS secret from the environment
246    pub fn secret_from_env(&mut self) -> Result<&mut Self, Error> {
247        let var = std::env::var(Config::ATLAS_SECRET_VAR).map_err(|_| Error::ParseEnvironment)?;
248
249        self.secret(var);
250        Ok(self)
251    }
252
253    /// Attempt to load the ATLAS key from the environment
254    pub fn key_from_env(&mut self) -> Result<&mut Self, Error> {
255        let var = std::env::var(Config::ATLAS_KEY_VAR).map_err(|_| Error::ParseEnvironment)?;
256
257        self.key(var);
258        Ok(self)
259    }
260
261    /// Set the environment
262    pub fn environment(&mut self, environment: impl IntoEnv) -> &mut Self {
263        self.environment = Some(environment.into());
264        self
265    }
266
267    /// Set the secret
268    pub fn secret(&mut self, secret: impl Into<String>) -> &mut Self {
269        self.secret = Some(Secret(secret.into()));
270        self
271    }
272
273    /// Set the key
274    pub fn key(&mut self, key: impl Into<String>) -> &mut Self {
275        self.key = Some(key.into());
276        self
277    }
278
279    /// Build the Config from the current builder
280    pub fn build(&mut self) -> Result<Config, Error> {
281        let Some(environment) = self.environment.take() else {
282            return Err(Error::MissingEnvironment);
283        };
284        let Some(key) = self.key.take() else {
285            return Err(Error::MissingKey);
286        };
287        let Some(secret) = self.secret.take() else {
288            return Err(Error::MissingSecret);
289        };
290
291        Ok(Config {
292            environment,
293            key,
294            secret,
295        })
296    }
297}
298
299impl Config {
300    /// The environment variable name for the atlas environment
301    pub const ATLAS_ENV_VAR: &'static str = "ATLAS_ENV";
302
303    /// The environment variable name for the atlas key
304    pub const ATLAS_KEY_VAR: &'static str = "ATLAS_KEY";
305
306    /// The environment variable name for the atlas secret
307    pub const ATLAS_SECRET_VAR: &'static str = "ATLAS_SECRET";
308
309    /// Construct a new config builder
310    ///
311    /// # Example
312    ///
313    /// ```
314    /// # use freedom_config::{Config, Test};
315    /// let config_result = Config::builder()
316    ///     .environment(Test)
317    ///     .key("my_key")
318    ///     .secret("my_secret")
319    ///     .build();
320    ///
321    /// assert!(config_result.is_ok());
322    /// ```
323    pub fn builder() -> ConfigBuilder {
324        ConfigBuilder::new()
325    }
326
327    /// Build the entire configuration from environment variables
328    pub fn from_env() -> Result<Self, Error> {
329        Self::builder()
330            .environment_from_env()?
331            .key_from_env()?
332            .secret_from_env()?
333            .build()
334    }
335
336    /// Construct the Config from the environment, key, and secret
337    ///
338    /// # Example
339    ///
340    /// ```
341    /// # use freedom_config::{Config, Test};
342    /// let config = Config::new(Test, "my_key", "my_secret");
343    /// ```
344    pub fn new(environment: impl Env, key: impl Into<String>, secret: impl Into<String>) -> Self {
345        let environment = Environment::new(environment);
346
347        Self {
348            environment,
349            key: key.into(),
350            secret: Secret(secret.into()),
351        }
352    }
353
354    /// Set the environment
355    ///
356    /// # Example
357    ///
358    /// ```
359    /// # let mut config = freedom_config::Config::new(freedom_config::Test, "key", "password");
360    /// # use freedom_config::Prod;
361    /// config.set_environment(Prod);
362    /// assert_eq!(config.environment_str(), "prod");
363    /// ```
364    pub fn set_environment(&mut self, environment: impl Env) {
365        self.environment = Environment::new(environment);
366    }
367
368    /// Return the trait object representing an ATLAS environment
369    pub fn environment(&self) -> &Environment {
370        &self.environment
371    }
372
373    /// Return the string representation of the environment
374    pub fn environment_str(&self) -> &str {
375        self.environment.as_ref()
376    }
377
378    /// Exposes the secret as a string slice.
379    ///
380    /// # Warning
381    ///
382    /// Use this with extreme care to avoid accidentally leaking your key
383    pub fn expose_secret(&self) -> &str {
384        self.secret.expose()
385    }
386
387    /// Return the ATLAS key
388    pub fn key(&self) -> &str {
389        &self.key
390    }
391
392    /// Set the ATLAS key
393    ///
394    /// # Example
395    ///
396    /// ```
397    /// # let mut config = freedom_config::Config::new(freedom_config::Test, "key", "password");
398    /// config.set_key("top secret");
399    /// assert_eq!(config.key(), "top secret");
400    /// ```
401    pub fn set_key(&mut self, key: impl Into<String>) {
402        self.key = key.into();
403    }
404
405    /// Set the value of the ATLAS secret
406    ///
407    /// # Example
408    ///
409    /// ```
410    /// # let mut config = freedom_config::Config::new(freedom_config::Test, "key", "password");
411    /// config.set_secret("top secret");
412    /// assert_eq!(config.expose_secret(), "top secret");
413    /// ```
414    pub fn set_secret(&mut self, secret: impl Into<String>) {
415        self.secret = Secret(secret.into());
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[allow(unused)]
424    fn config_is_send() {
425        fn is_send<T: Send>(_foo: T) {}
426
427        let config = Config::from_env().unwrap();
428        is_send(config);
429    }
430
431    #[cfg(feature = "serde")]
432    mod serde {
433        use super::*;
434
435        #[test]
436        fn deserialize_config() {
437            let json = r#"{"key": "foo", "secret": "bar", "environment": "tEsT"}"#;
438            let config: Config = serde_json::from_str(json).unwrap();
439            assert_eq!(config.key(), "foo");
440            assert_eq!(config.expose_secret(), "bar");
441            assert_eq!(config.environment_str(), "test");
442        }
443
444        #[test]
445        fn serialize_config() {
446            let config = Config::builder()
447                .key("foo")
448                .secret("bar")
449                .environment(Test)
450                .build()
451                .unwrap();
452            let value = serde_json::to_value(&config).unwrap();
453            assert_eq!(value.get("key").unwrap().as_str().unwrap(), "foo");
454            assert_eq!(value.get("secret").unwrap().as_str().unwrap(), "bar");
455            assert_eq!(value.get("environment").unwrap().as_str().unwrap(), "test");
456        }
457
458        #[test]
459        fn deserialize_config_prod() {
460            let json = r#"{"key": "foo", "secret": "bar", "environment": "prod"}"#;
461            let config: Config = serde_json::from_str(json).unwrap();
462            assert_eq!(config.key(), "foo");
463            assert_eq!(config.expose_secret(), "bar");
464            assert_eq!(config.environment_str(), "prod");
465        }
466
467        #[test]
468        fn serialize_config_prod() {
469            let config = Config::builder()
470                .key("foo")
471                .secret("bar")
472                .environment(Prod)
473                .build()
474                .unwrap();
475            let value = serde_json::to_value(&config).unwrap();
476            assert_eq!(value.get("key").unwrap().as_str().unwrap(), "foo");
477            assert_eq!(value.get("secret").unwrap().as_str().unwrap(), "bar");
478            assert_eq!(value.get("environment").unwrap().as_str().unwrap(), "prod");
479        }
480    }
481}