freedom_config/
lib.rs

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