Skip to content

Commit

Permalink
Feature: Better Figment Config (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
bkonkle committed Jan 26, 2024
1 parent 6cab1a1 commit bbf0792
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 69 deletions.
4 changes: 3 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
!target/release/nakago-examples-async-graphql

# Configs
!examples/async-graphql/config/
!examples/async-graphql/config.*
examples/async-graphql/config.test.toml
examples/async-graphql/config.local.toml
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- `nakago-example-async-graphql`: Cleaned up some imports that weren't being used.
- Removed the 'config' directories in the example projects and moved the config files up to the root folder of each project.
- Updated config loaders to act on Figments, making it easier to take full advantage of the Figment library.

## [0.19.0]

Expand Down
2 changes: 1 addition & 1 deletion Makefile.toml
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ args = [
script = '''
cargo clean
cargo build --release --quiet --timings
xdg-open /target/cargo-timings/cargo-timing.html
xdg-open target/cargo-timings/cargo-timing.html
'''

[tasks.pre-commit]
Expand Down
2 changes: 1 addition & 1 deletion examples/async-graphql/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ RUN apt update && \
chown async-graphql:async-graphql /usr/src/app

COPY --chown=async-graphql:async-graphql ../../target/release/nakago-examples-async-graphql /usr/src/app/async-graphql
COPY --chown=async-graphql:async-graphql ../../examples/async-graphql/config/*.toml /usr/src/app/config/
COPY --chown=async-graphql:async-graphql ../../examples/async-graphql/config.*.toml /usr/src/app/config/

USER async-graphql
WORKDIR /usr/src/app
Expand Down
74 changes: 56 additions & 18 deletions nakago/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,14 @@ where
}

/// Trigger the given lifecycle event
pub async fn trigger(&mut self, event: &EventType) -> hooks::Result<()> {
pub async fn trigger(&self, event: &EventType) -> hooks::Result<()> {
self.events.trigger(event, self.i.clone()).await
}

/// Load the App's dependencies and configuration. Triggers the Load lifecycle event.
pub async fn load(&self, config_path: Option<PathBuf>) -> hooks::Result<()> {
// Trigger the Load lifecycle event
self.events
.trigger(&EventType::Load, self.i.clone())
self.trigger(&EventType::Load)
.await
.map_err(from_hook_error)?;

Expand All @@ -98,9 +97,7 @@ where
/// Initialize the App and provide the top-level Config. Triggers the Init lifecycle event.
pub async fn init(&self) -> hooks::Result<()> {
// Trigger the Init lifecycle event
self.events
.trigger(&EventType::Init, self.i.clone())
.await?;
self.trigger(&EventType::Init).await?;

tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
Expand All @@ -117,20 +114,12 @@ where

/// Trigger the Startup lifecycle event.
pub async fn start(&self) -> hooks::Result<()> {
self.events
.trigger(&EventType::Startup, self.i.clone())
.await?;

Ok(())
self.trigger(&EventType::Startup).await
}

/// Trigger the Shutdown lifecycle event.
pub async fn stop(&self) -> hooks::Result<()> {
self.events
.trigger(&EventType::Shutdown, self.i.clone())
.await?;

Ok(())
self.trigger(&EventType::Shutdown).await
}

/// Get the top-level Config by tag or type
Expand Down Expand Up @@ -174,10 +163,26 @@ fn handle_panic(info: &PanicInfo<'_>) {
pub mod test {
use anyhow::Result;

use crate::config::{hooks::test::TestLoader, loader::test::Config, AddLoaders, Loader};
use crate::config::{
hooks::test::TestLoader,
loader::test::{Config, CONFIG},
AddLoaders, Loader,
};

use super::*;

#[tokio::test]
async fn test_app_deref_success() -> Result<()> {
let mut app = Application::<Config>::default();

let keys = app.get_available_keys().await;
assert_eq!(keys.len(), 0);

let mut _m = &mut *app;

Ok(())
}

#[tokio::test]
async fn test_app_load_success() -> Result<()> {
let mut app = Application::<Config>::default();
Expand All @@ -203,7 +208,13 @@ pub mod test {

#[tokio::test]
async fn test_app_init_success() -> Result<()> {
let app = Application::<Config>::default();
let mut app = Application::<Config>::new(None);

assert_eq!(app.config_tag, None);

app = app.with_config_tag(&CONFIG);

assert_eq!(app.config_tag, Some(&CONFIG));

app.init().await?;

Expand All @@ -227,4 +238,31 @@ pub mod test {

Ok(())
}

#[tokio::test]
async fn test_app_getconfig_success() -> Result<()> {
let mut app = Application::<Config>::default();

let config = app.get_config().await;
assert!(config.is_err());

app.inject_type(Config::default()).await?;

let config = app.get_config().await;
assert!(config.is_ok());

app.remove_type::<Config>().await?;

app = app.with_config_tag(&CONFIG);

let config = app.get_config().await;
assert!(config.is_err());

app.inject(&CONFIG, Config::default()).await?;

let config = app.get_config().await;
assert!(config.is_ok());

Ok(())
}
}
21 changes: 16 additions & 5 deletions nakago/src/config/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ impl<C: Config> Hook for Init<C> {

let config = loader
.load(self.custom_path.clone())
.extract()
.map_err(|e| Error::Any(Arc::new(e.into())))?;

if let Some(tag) = self.tag {
Expand All @@ -108,18 +109,18 @@ impl<C: Config> Hook for Init<C> {

#[cfg(test)]
pub(crate) mod test {
use figment::providers::Env;
use figment::Figment;

use crate::config::loader::test::Config;
use crate::config::loader::test::{Config, CONFIG};

use super::*;

#[derive(Default, Debug, PartialEq, Eq)]
pub struct TestLoader {}

impl Loader for TestLoader {
fn load_env(&self, env: Env) -> Env {
env
fn load(&self, figment: Figment) -> Figment {
figment
}
}

Expand Down Expand Up @@ -163,7 +164,17 @@ pub(crate) mod test {
async fn test_init_success() -> Result<()> {
let i = Inject::default();

let hook = Init::<Config>::new(None, None);
let hook = Init::<Config>::default();
assert!(hook.custom_path.is_none());
assert!(hook.tag.is_none());

i.handle(hook).await?;

let hook = Init::<Config>::default().with_path("TEST_PATH".into());
assert!(hook.custom_path.is_some());

let hook = Init::<Config>::default().with_tag(&CONFIG);
assert!(hook.tag.is_some());

i.handle(hook).await?;

Expand Down
39 changes: 18 additions & 21 deletions nakago/src/config/loader.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
use std::{any::Any, fmt::Debug, marker::PhantomData, path::PathBuf, sync::Arc};

use figment::{
providers::{Env, Format, Json, Serialized, Toml, Yaml},
providers::{Format, Json, Serialized, Toml, Yaml},
Figment,
};
use serde::{Deserialize, Serialize};

/// A Loader uses hooks to augment the Config loaded for the application
///
/// TODO: Add more hooks! 🙂
pub trait Loader: Any + Send + Sync {
/// Apply transformations to the environment variables loaded by Figment
fn load_env(&self, env: Env) -> Env;
/// Apply transformations to the Figment provider
fn load(&self, figment: Figment) -> Figment;
}

/// Config is the final loaded result
Expand All @@ -36,7 +34,7 @@ impl<C: Config> LoadAll<C> {
}

/// Create a new Config by merging in various sources
pub fn load(&self, custom_path: Option<PathBuf>) -> figment::error::Result<C> {
pub fn load(&self, custom_path: Option<PathBuf>) -> Figment {
let mut config = Figment::new()
// Load defaults
.merge(Serialized::defaults(C::default()))
Expand All @@ -59,38 +57,36 @@ impl<C: Config> LoadAll<C> {
}
}

// Environment Variables
// ---------------------

let mut env = Env::raw();

// Apply individual loaders to transform the Figment provider
for loader in &self.loaders {
env = loader.load_env(env);
config = loader.load(config);
}

config = config.merge(env);

// Serialize and freeze
config.extract()
config
}
}

#[cfg(test)]
pub(crate) mod test {
use anyhow::Result;

use crate::Tag;

use super::*;

#[derive(Default, Debug, Serialize, Deserialize, Clone)]
#[derive(Default, Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
pub struct Config {}

impl crate::Config for Config {}

/// Tag(app::Config)
pub const CONFIG: Tag<Config> = Tag::new("app::Config");

#[tokio::test]
async fn test_load_all_success() -> Result<()> {
let loader = LoadAll::<Config>::new(vec![]);

loader.load(None)?;
let _config: Config = loader.load(None).extract()?;

Ok(())
}
Expand All @@ -99,9 +95,10 @@ pub(crate) mod test {
async fn test_load_all_custom_path() -> Result<()> {
let loader = LoadAll::<Config>::new(vec![]);

let custom_path = PathBuf::from("config.toml");

loader.load(Some(custom_path))?;
let _figment: Figment = loader.load(Some("config.toml".into()));
let _figment: Figment = loader.load(Some("config.yml".into()));
let _figment: Figment = loader.load(Some("config.yaml".into()));
let _figment: Figment = loader.load(Some("config.json".into()));

Ok(())
}
Expand Down
61 changes: 61 additions & 0 deletions nakago/src/inject/tag.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,24 @@ pub(crate) mod test {
Ok(())
}

#[tokio::test]
async fn test_consume_provider_in_use() -> Result<()> {
let i = Inject::default();

let expected: String = fake::uuid::UUIDv4.fake();

i.provide(&SERVICE_TAG, TestServiceProvider::new(expected.clone()))
.await?;

let _borrow = i.get(&SERVICE_TAG).await?;

let result = i.consume(&SERVICE_TAG).await;

assert!(result.is_err());

Ok(())
}

#[tokio::test]
async fn test_consume_provider_multiple() -> Result<()> {
let i = Inject::default();
Expand Down Expand Up @@ -699,6 +717,31 @@ pub(crate) mod test {
Ok(())
}

#[tokio::test]
async fn test_modify_in_use() -> Result<()> {
let i = Inject::default();

let initial: String = fake::uuid::UUIDv4.fake();
let expected: String = fake::uuid::UUIDv4.fake();

i.provide(&SERVICE_TAG, TestServiceProvider::new(initial.clone()))
.await?;

let _borrow = i.get(&SERVICE_TAG).await?;

let result = i
.modify(&SERVICE_TAG, |mut t| {
t.id = expected.clone();

Ok(t)
})
.await;

assert!(result.is_err());

Ok(())
}

#[tokio::test]
async fn test_remove_provider_success() -> Result<()> {
let i = Inject::default();
Expand Down Expand Up @@ -808,4 +851,22 @@ pub(crate) mod test {

Ok(())
}

#[tokio::test]
async fn test_eject_provider_in_use() -> Result<()> {
let i = Inject::default();

let expected: String = fake::uuid::UUIDv4.fake();

i.provide(&SERVICE_TAG, TestServiceProvider::new(expected.clone()))
.await?;

let _borrow = i.get(&SERVICE_TAG).await?;

let result = i.eject(&SERVICE_TAG).await;

assert!(result.is_err());

Ok(())
}
}

0 comments on commit bbf0792

Please sign in to comment.