#![doc = include_str!("../README.md")]
use std::{fmt::Debug, sync::Arc};
use url::Url;
#[cfg(feature = "serde")]
mod ser;
#[derive(Debug, Clone)]
pub struct Environment(Arc<dyn Env>);
impl std::fmt::Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.0.as_ref().as_ref())
}
}
impl Default for Environment {
fn default() -> Self {
Self::test()
}
}
pub trait IntoEnv {
fn into(self) -> Environment;
}
impl<T: Env> IntoEnv for T {
fn into(self) -> Environment {
Environment::new(self)
}
}
impl IntoEnv for Environment {
fn into(self) -> Environment {
self
}
}
impl std::ops::Deref for Environment {
type Target = dyn Env;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl Environment {
pub fn new<E: Env>(env: E) -> Self {
Self(Arc::new(env))
}
pub fn test() -> Self {
Self::new(Test)
}
pub fn prod() -> Self {
Self::new(Prod)
}
}
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Secret<T>(pub T);
impl<T> From<T> for Secret<T> {
fn from(value: T) -> Self {
Secret(value)
}
}
impl<T> Secret<T> {
pub fn expose(&self) -> &T {
&self.0
}
}
impl<T> std::fmt::Display for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as std::fmt::Debug>::fmt(self, f)
}
}
impl<T> std::fmt::Debug for Secret<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("Secret").field(&"*****").finish()
}
}
pub trait Env: 'static + AsRef<str> + Debug + Send + Sync + Unpin {
fn from_str(val: &str) -> Option<Self>
where
Self: Sized;
fn fps_host(&self) -> &str;
fn freedom_entrypoint(&self) -> Url;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Test;
impl AsRef<str> for Test {
fn as_ref(&self) -> &str {
"test"
}
}
impl Env for Test {
fn from_str(val: &str) -> Option<Self>
where
Self: Sized,
{
val.to_ascii_lowercase().eq("test").then_some(Self)
}
fn fps_host(&self) -> &str {
"fps.test.atlasground.com"
}
fn freedom_entrypoint(&self) -> Url {
Url::parse("https://test-api.atlasground.com/api/").unwrap()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Prod;
impl AsRef<str> for Prod {
fn as_ref(&self) -> &str {
"prod"
}
}
impl Env for Prod {
fn from_str(val: &str) -> Option<Self>
where
Self: Sized,
{
val.to_ascii_lowercase().eq("prod").then_some(Self)
}
fn fps_host(&self) -> &str {
"fps.atlasground.com"
}
fn freedom_entrypoint(&self) -> Url {
Url::parse("https://api.atlasground.com/api/").unwrap()
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Config {
environment: Environment,
key: String,
secret: Secret<String>,
}
impl PartialEq for Config {
fn eq(&self, other: &Self) -> bool {
self.environment_str() == other.environment_str()
&& self.key == other.key
&& self.secret == other.secret
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Copy, Hash, PartialOrd, Ord)]
pub enum Error {
ParseEnvironment,
MissingSecret,
MissingKey,
MissingEnvironment,
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
<Self as std::fmt::Debug>::fmt(self, f)
}
}
impl std::error::Error for Error {}
#[derive(Default)]
pub struct ConfigBuilder {
environment: Option<Environment>,
key: Option<String>,
secret: Option<Secret<String>>,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn environment_from_env(&mut self) -> Result<&mut Self, Error> {
let var = std::env::var(Config::ATLAS_ENV_VAR).map_err(|_| Error::ParseEnvironment)?;
if let Some(env) = Test::from_str(&var) {
return Ok(self.environment(env));
}
if let Some(env) = Prod::from_str(&var) {
return Ok(self.environment(env));
}
Err(Error::ParseEnvironment)
}
pub fn secret_from_env(&mut self) -> Result<&mut Self, Error> {
let var = std::env::var(Config::ATLAS_SECRET_VAR).map_err(|_| Error::ParseEnvironment)?;
self.secret(var);
Ok(self)
}
pub fn key_from_env(&mut self) -> Result<&mut Self, Error> {
let var = std::env::var(Config::ATLAS_KEY_VAR).map_err(|_| Error::ParseEnvironment)?;
self.key(var);
Ok(self)
}
pub fn environment(&mut self, environment: impl IntoEnv) -> &mut Self {
self.environment = Some(environment.into());
self
}
pub fn secret(&mut self, secret: impl Into<String>) -> &mut Self {
self.secret = Some(Secret(secret.into()));
self
}
pub fn key(&mut self, key: impl Into<String>) -> &mut Self {
self.key = Some(key.into());
self
}
pub fn build(&mut self) -> Result<Config, Error> {
let Some(environment) = self.environment.take() else {
return Err(Error::MissingEnvironment);
};
let Some(key) = self.key.take() else {
return Err(Error::MissingKey);
};
let Some(secret) = self.secret.take() else {
return Err(Error::MissingSecret);
};
Ok(Config {
environment,
key,
secret,
})
}
}
impl Config {
pub const ATLAS_ENV_VAR: &'static str = "ATLAS_ENV";
pub const ATLAS_KEY_VAR: &'static str = "ATLAS_KEY";
pub const ATLAS_SECRET_VAR: &'static str = "ATLAS_SECRET";
pub fn builder() -> ConfigBuilder {
ConfigBuilder::new()
}
pub fn from_env() -> Result<Self, Error> {
Self::builder()
.environment_from_env()?
.key_from_env()?
.secret_from_env()?
.build()
}
pub fn new(environment: impl Env, key: impl Into<String>, secret: impl Into<String>) -> Self {
let environment = Environment::new(environment);
Self {
environment,
key: key.into(),
secret: Secret(secret.into()),
}
}
pub fn set_environment(&mut self, environment: impl Env) {
self.environment = Environment::new(environment);
}
pub fn environment(&self) -> &Environment {
&self.environment
}
pub fn environment_str(&self) -> &str {
self.environment.as_ref()
}
pub fn expose_secret(&self) -> &str {
self.secret.expose()
}
pub fn key(&self) -> &str {
&self.key
}
pub fn set_key(&mut self, key: impl Into<String>) {
self.key = key.into();
}
pub fn set_secret(&mut self, secret: impl Into<String>) {
self.secret = Secret(secret.into());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused)]
fn config_is_send() {
fn is_send<T: Send>(_foo: T) {}
let config = Config::from_env().unwrap();
is_send(config);
}
#[cfg(feature = "serde")]
mod serde {
use super::*;
#[test]
fn deserialize_config() {
let json = r#"{"key": "foo", "secret": "bar", "environment": "tEsT"}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.key(), "foo");
assert_eq!(config.expose_secret(), "bar");
assert_eq!(config.environment_str(), "test");
}
#[test]
fn serialize_config() {
let config = Config::builder()
.key("foo")
.secret("bar")
.environment(Test)
.build()
.unwrap();
let value = serde_json::to_value(&config).unwrap();
assert_eq!(value.get("key").unwrap().as_str().unwrap(), "foo");
assert_eq!(value.get("secret").unwrap().as_str().unwrap(), "bar");
assert_eq!(value.get("environment").unwrap().as_str().unwrap(), "test");
}
#[test]
fn deserialize_config_prod() {
let json = r#"{"key": "foo", "secret": "bar", "environment": "prod"}"#;
let config: Config = serde_json::from_str(json).unwrap();
assert_eq!(config.key(), "foo");
assert_eq!(config.expose_secret(), "bar");
assert_eq!(config.environment_str(), "prod");
}
#[test]
fn serialize_config_prod() {
let config = Config::builder()
.key("foo")
.secret("bar")
.environment(Prod)
.build()
.unwrap();
let value = serde_json::to_value(&config).unwrap();
assert_eq!(value.get("key").unwrap().as_str().unwrap(), "foo");
assert_eq!(value.get("secret").unwrap().as_str().unwrap(), "bar");
assert_eq!(value.get("environment").unwrap().as_str().unwrap(), "prod");
}
}
}