diff --git a/.cargo-husky/hooks/pre-push b/.cargo-husky/hooks/pre-push index 89acb182..dece58ee 100755 --- a/.cargo-husky/hooks/pre-push +++ b/.cargo-husky/hooks/pre-push @@ -11,10 +11,10 @@ cargo fmt --all --check echo "### build --no-default-features ###" RUSTFLAGS="-D warnings" cargo build --no-default-features echo "### nextest run --no-default-features ###" -cargo nextest run --no-default-features +cargo nextest run --no-default-features --no-fail-fast # Nextest doesn't support doc tests, run those separately echo "### test --doc --no-default-features ###" -cargo test --doc --no-default-features +cargo test --doc --no-default-features --no-fail-fast echo "### check --no-default-features ###" cargo check --no-default-features echo "### clippy --all-targets --no-default-features -- -D warnings ###" @@ -24,10 +24,10 @@ cargo clippy --all-targets --no-default-features -- -D warnings echo "### build ###" cargo build echo "### nextest run ###" -cargo nextest run +cargo nextest run --no-fail-fast # Nextest doesn't support doc tests, run those separately echo "### test --doc ###" -cargo test --doc +cargo test --doc --no-fail-fast echo "### check ###" cargo check echo "### clippy --all-targets -- -D warnings ###" @@ -37,10 +37,10 @@ cargo clippy --all-targets -- -D warnings echo "### build --all-features ###" cargo build --all-features echo "### nextest run --all-features --workspace ###" -cargo nextest run --all-features --workspace +cargo nextest run --all-features --no-fail-fast --workspace # Nextest doesn't support doc tests, run those separately echo "### test --doc --all-features --workspace ###" -cargo test --doc --all-features --workspace +cargo test --doc --all-features --no-fail-fast --workspace echo "### check --all-features --workspace ###" cargo check --all-features --workspace echo "### clippy --workspace --all-targets --all-features -- -D warnings ###" diff --git a/.github/workflows/feature_powerset.yml b/.github/workflows/feature_powerset.yml index ee5bdf0e..886acc77 100644 --- a/.github/workflows/feature_powerset.yml +++ b/.github/workflows/feature_powerset.yml @@ -57,7 +57,7 @@ jobs: # protoc is needed to build examples that have grpc enabled - uses: taiki-e/install-action@protoc - name: Test - run: cargo hack nextest run --no-fail-fast --feature-powerset --depth 3 --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features + run: cargo hack nextest run --no-fail-fast --feature-powerset --depth 3 --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --group-features email-smtp,email --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features - name: Check disk usage run: df -h @@ -74,7 +74,7 @@ jobs: # protoc is needed to build examples that have grpc enabled - uses: taiki-e/install-action@protoc - name: Doc test - run: cargo hack test --doc --no-fail-fast --feature-powerset --depth 3 --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features + run: cargo hack test --doc --no-fail-fast --feature-powerset --depth 3 --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --group-features email-smtp,email --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features powerset_check: name: Powerset Check @@ -89,7 +89,7 @@ jobs: # protoc is needed to build examples that have grpc enabled - uses: taiki-e/install-action@protoc - name: Check - run: cargo hack check --feature-powerset --depth 3 --no-dev-deps --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features + run: cargo hack check --feature-powerset --depth 3 --no-dev-deps --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --group-features email-smtp,email --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features powerset_clippy: name: Powerset Clippy @@ -104,4 +104,4 @@ jobs: # protoc is needed to build examples that have grpc enabled - uses: taiki-e/install-action@protoc - name: Clippy - run: cargo hack clippy --all-targets --feature-powerset --depth 3 --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features -- -D warnings + run: cargo hack clippy --all-targets --feature-powerset --depth 3 --skip default --group-features jwt-ietf,jwt --group-features jwt-openid,jwt --group-features open-api,http --group-features email-smtp,email --clean-per-run --log-group github-actions --exclude-no-default-features --exclude-all-features -- -D warnings diff --git a/Cargo.toml b/Cargo.toml index 5962fca0..d6696822 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ http = ["dep:axum-extra", "dep:tower", "dep:tower-http"] open-api = ["http", "dep:aide", "dep:schemars"] sidekiq = ["dep:rusty-sidekiq", "dep:bb8", "dep:num_cpus"] db-sql = ["dep:sea-orm", "dep:sea-orm-migration"] +email = ["dep:lettre"] +email-smtp = ["email"] jwt = ["dep:jsonwebtoken"] jwt-ietf = ["jwt"] jwt-openid = ["jwt"] @@ -62,6 +64,9 @@ http-body-util = "0.1.0" sea-orm = { workspace = true, features = ["debug-print", "runtime-tokio-rustls", "sqlx-postgres", "macros"], optional = true } sea-orm-migration = { workspace = true, features = ["runtime-tokio-rustls", "sqlx-postgres"], optional = true } +# Email +lettre = { workspace = true, features = ["serde"], optional = true } + # Workers rusty-sidekiq = { workspace = true, optional = true } bb8 = { version = "0.8.0", optional = true } @@ -95,7 +100,7 @@ strum_macros = "0.26.0" itertools = "0.13.0" serde_json = "1.0.96" toml = "0.8.0" -url = { version = "2.2.2", features = ["serde"] } +url = { version = "2.4.0", features = ["serde"] } uuid = { version = "1.1.2", features = ["v4", "serde"] } futures = "0.3.21" futures-core = "0.3.31" @@ -136,6 +141,9 @@ schemars = "0.8.16" sea-orm = { version = "1.0.1" } sea-orm-migration = { version = "1.0.0" } +# Email +lettre = "0.11.0" + # CLI clap = { version = "4.3.0", features = ["derive"] } diff --git a/examples/full/build.rs b/examples/full/build.rs index 34c5e3a6..57d12541 100644 --- a/examples/full/build.rs +++ b/examples/full/build.rs @@ -14,7 +14,7 @@ fn main() -> Result<(), Box> { let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); tonic_build::configure() .file_descriptor_set_path(out_dir.join("helloworld_descriptor.bin")) - .compile(&["proto/helloworld.proto"], &["proto"]) + .compile_protos(&["proto/helloworld.proto"], &["proto"]) .unwrap(); } diff --git a/examples/full/src/app.rs b/examples/full/src/app.rs index 438165dd..d2a246cc 100644 --- a/examples/full/src/app.rs +++ b/examples/full/src/app.rs @@ -10,7 +10,7 @@ use migration::Migrator; use roadster::app::context::AppContext; use roadster::app::metadata::AppMetadata; use roadster::app::App as RoadsterApp; -use roadster::config::app_config::AppConfig; +use roadster::config::AppConfig; use roadster::error::RoadsterResult; use roadster::service::function::service::FunctionService; #[cfg(feature = "grpc")] diff --git a/examples/leptos-ssr/src/server/mod.rs b/examples/leptos-ssr/src/server/mod.rs index 389132fc..80667548 100644 --- a/examples/leptos-ssr/src/server/mod.rs +++ b/examples/leptos-ssr/src/server/mod.rs @@ -11,8 +11,8 @@ use migration::Migrator; use roadster::app::context::AppContext; use roadster::app::metadata::AppMetadata; use roadster::app::App as RoadsterApp; -use roadster::config::app_config::AppConfig; use roadster::config::environment::Environment; +use roadster::config::AppConfig; use roadster::error::RoadsterResult; use roadster::service::http::service::HttpService; use roadster::service::registry::ServiceRegistry; diff --git a/justfile b/justfile index af77d141..445b7150 100644 --- a/justfile +++ b/justfile @@ -4,8 +4,8 @@ help: # Run all of our unit tests. test: - cargo nextest run --all-features - cargo test --doc --all-features + cargo nextest run --all-features --no-fail-fast + cargo test --doc --all-features --no-fail-fast # Run all of our unit tests whenever files in the repo change. test-watch: @@ -50,4 +50,4 @@ validate-codecov-config: # Initialize a new installation of the repo (e.g., install deps) init: - cargo binstall cargo-nextest cargo-llvm-cov sea-orm-cli + cargo binstall cargo-nextest cargo-llvm-cov sea-orm-cli cargo-insta cargo-minimal-versions cargo-hack diff --git a/src/api/http/docs.rs b/src/api/http/docs.rs index b794ef68..91c65133 100644 --- a/src/api/http/docs.rs +++ b/src/api/http/docs.rs @@ -125,7 +125,7 @@ fn api_schema_route(context: &AppContext) -> &str { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; // Todo: Is there a better way to structure these tests (and the ones in `health` and `ping`) diff --git a/src/api/http/health.rs b/src/api/http/health.rs index 17488cc7..0c489919 100644 --- a/src/api/http/health.rs +++ b/src/api/http/health.rs @@ -149,7 +149,7 @@ fn health_get_docs(op: TransformOperation) -> TransformOperation { #[cfg(test)] mod tests { use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; // Todo: Is there a better way to structure this test (and the ones in `docs` and `ping`) diff --git a/src/api/http/ping.rs b/src/api/http/ping.rs index ada60e8c..97244cb0 100644 --- a/src/api/http/ping.rs +++ b/src/api/http/ping.rs @@ -91,7 +91,7 @@ fn ping_get_docs(op: TransformOperation) -> TransformOperation { #[cfg(test)] mod tests { use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; // Todo: Is there a better way to structure this test (and the ones in `health` and `ping`) diff --git a/src/app/context.rs b/src/app/context.rs index d540c83e..2c59d8a4 100644 --- a/src/app/context.rs +++ b/src/app/context.rs @@ -1,6 +1,6 @@ use crate::app::metadata::AppMetadata; use crate::app::App; -use crate::config::app_config::AppConfig; +use crate::config::AppConfig; use crate::error::RoadsterResult; use crate::health_check::registry::HealthCheckRegistry; use crate::health_check::HealthCheck; diff --git a/src/app/mod.rs b/src/app/mod.rs index be3a688b..8ffc31bd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -29,9 +29,9 @@ use crate::api::cli::MockTestCli; #[cfg(feature = "cli")] use crate::api::cli::RunCommand; use crate::app::metadata::AppMetadata; -use crate::config::app_config::AppConfig; #[cfg(not(feature = "cli"))] use crate::config::environment::Environment; +use crate::config::AppConfig; use crate::error::RoadsterResult; use crate::health_check::registry::HealthCheckRegistry; use crate::lifecycle::registry::LifecycleHandlerRegistry; diff --git a/src/app/roadster_app.rs b/src/app/roadster_app.rs index 89518cad..0ff12228 100644 --- a/src/app/roadster_app.rs +++ b/src/app/roadster_app.rs @@ -4,7 +4,7 @@ use crate::app; use crate::app::context::AppContext; use crate::app::metadata::AppMetadata; use crate::app::App; -use crate::config::app_config::AppConfig; +use crate::config::AppConfig; use crate::error::RoadsterResult; use crate::health_check::registry::HealthCheckRegistry; use crate::health_check::HealthCheck; diff --git a/src/config/app_config.rs b/src/config/app_config.rs index 7b4e3b99..63df5d62 100644 --- a/src/config/app_config.rs +++ b/src/config/app_config.rs @@ -1,295 +1,10 @@ -use crate::config::auth::Auth; -#[cfg(feature = "db-sql")] -use crate::config::database::Database; -use crate::config::environment::{Environment, ENVIRONMENT_ENV_VAR_NAME}; -use crate::config::health_check::HealthCheck; -use crate::config::lifecycle::LifecycleHandler; -use crate::config::service::Service; -use crate::config::tracing::Tracing; -use crate::error::RoadsterResult; -use crate::util::serde::default_true; -use config::builder::DefaultState; -use config::{Case, Config, ConfigBuilder, FileFormat}; -use dotenvy::dotenv; -use serde_derive::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::BTreeMap; -use std::fs; -use std::path::{Path, PathBuf}; -use tracing::warn; -use validator::Validate; - -pub type CustomConfig = BTreeMap; - -#[derive(Debug, Clone, Validate, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -#[non_exhaustive] -pub struct AppConfig { - pub environment: Environment, - #[validate(nested)] - pub app: App, - #[validate(nested)] - pub lifecycle_handler: LifecycleHandler, - #[validate(nested)] - pub health_check: HealthCheck, - #[validate(nested)] - pub service: Service, - #[validate(nested)] - pub auth: Auth, - #[validate(nested)] - pub tracing: Tracing, - #[cfg(feature = "db-sql")] - #[validate(nested)] - pub database: Database, - /// Allows providing custom config values. Any configs that aren't pre-defined above - /// will be collected here. - /// - /// # Examples - /// - /// ```toml - /// [foo] - /// x = "y" - /// ``` - /// - /// This will be parsed as: - /// ```raw - /// AppConfig#custom: { - /// "foo": { - /// "x": "y", - /// } - /// } - /// ``` - #[serde(flatten, default)] - pub custom: CustomConfig, -} - -pub const ENV_VAR_PREFIX: &str = "ROADSTER"; -pub const ENV_VAR_SEPARATOR: &str = "__"; - -impl AppConfig { - #[deprecated( - since = "0.6.2", - note = "This wasn't intended to be made public and may be removed in a future version." - )] - pub fn new(environment: Option) -> RoadsterResult { - Self::new_with_config_dir(environment, Some(PathBuf::from("config/"))) - } - - // This runs before tracing is initialized, so we need to use `println` in order to - // log from this method. - #[allow(clippy::disallowed_macros)] - pub(crate) fn new_with_config_dir( - environment: Option, - config_dir: Option, - ) -> RoadsterResult { - dotenv().ok(); - - let environment = if let Some(environment) = environment { - println!("Using environment from CLI args: {environment:?}"); - environment - } else { - Environment::new()? - }; - let environment_str: &str = environment.clone().into(); - - let config_root_dir = config_dir - .unwrap_or_else(|| PathBuf::from("config/")) - .canonicalize()?; - - println!("Loading configuration from directory {config_root_dir:?}"); - - let config = Self::default_config(environment); - let config = config_env_file("default", &config_root_dir, config); - let config = config_env_dir("default", &config_root_dir, config)?; - let config = config_env_file(environment_str, &config_root_dir, config); - let config = config_env_dir(environment_str, &config_root_dir, config)?; - let config = config - .add_source( - config::Environment::default() - .prefix(ENV_VAR_PREFIX) - .convert_case(Case::Kebab) - .separator(ENV_VAR_SEPARATOR), - ) - .set_override(ENVIRONMENT_ENV_VAR_NAME, environment_str)? - .build()?; - let config: AppConfig = config.try_deserialize()?; - - Ok(config) - } - - #[cfg(test)] - #[cfg_attr(coverage_nightly, coverage(off))] - pub(crate) fn test(config_str: Option<&str>) -> RoadsterResult { - let config = Self::default_config(Environment::Test) - .add_source(config::File::from_str( - config_str.unwrap_or( - r#" - environment = "test" - - [app] - name = "Test" - - [tracing] - level = "debug" - - [database] - uri = "postgres://example:example@invalid_host:5432/example_test" - auto-migrate = true - max-connections = 10 - - [auth.jwt] - secret = "secret-test" - - [service.http] - host = "127.0.0.1" - port = 3000 - - [service.grpc] - host = "127.0.0.1" - port = 3001 - - [service.sidekiq] - # This field normally is determined by the number of CPU cores if not provided. - # We provide it in the test config to avoid snapshot failures when running - # on varying hardware. - num-workers = 16 - - [service.sidekiq.redis] - uri = "redis://invalid_host:1234" - "#, - ), - FileFormat::Toml, - )) - .build()?; - - let config: AppConfig = config.try_deserialize()?; - Ok(config) - } - - #[allow(clippy::let_and_return)] - fn default_config( - #[allow(unused_variables)] environment: Environment, - ) -> ConfigBuilder { - let config = Config::builder() - .add_source(config::File::from_str( - include_str!("default.toml"), - FileFormat::Toml, - )) - .add_source(crate::config::tracing::default_config()); - - #[cfg(feature = "http")] - let config = { - let config = config.add_source(crate::config::service::http::default_config()); - let config = crate::config::service::http::default_config_per_env(environment) - .into_iter() - .fold(config, |config, source| config.add_source(source)); - config - }; - - #[cfg(feature = "grpc")] - let config = config.add_source(crate::config::service::grpc::default_config()); - - #[cfg(feature = "sidekiq")] - let config = config.add_source(crate::config::service::worker::sidekiq::default_config()); - - let config = config.add_source(crate::config::lifecycle::default_config()); - - let config = config.add_source(crate::config::health_check::default_config()); - - config - } - - pub(crate) fn validate(&self, exit_on_error: bool) -> RoadsterResult<()> { - let result = Validate::validate(self); - if exit_on_error { - result?; - } else if let Err(err) = result { - warn!("An error occurred when validating the app config: {}", err); - } - Ok(()) - } -} - -/// Adds a config file in the relative path `config/{environment}.toml` to the -/// [`ConfigBuilder`]. If no such file exists, does nothing. -fn config_env_file( - environment: &str, - config_dir: &Path, - config: ConfigBuilder, -) -> ConfigBuilder { - // Todo: allow other file formats? - let path = config_dir.join(format!("{environment}.toml")); - if !path.is_file() { - return config; - } - - config.add_source(config::File::from(path)) -} - -/// Recursively adds all the config files in the given relative path `config/{environment}/` to the -/// [`ConfigBuilder`]. If no such directory exists, does nothing. -fn config_env_dir( - environment: &str, - config_dir: &Path, - config: ConfigBuilder, -) -> RoadsterResult> { - let path = config_dir.join(environment); - if !path.is_dir() { - return Ok(config); - } - - config_env_dir_recursive(&path, config) -} - -/// Helper method for [`config_env_dir`] to recursively add config files in the given path -/// to the [`ConfigBuilder`]. -// Todo: allow other file formats? -fn config_env_dir_recursive( - path: &Path, - config: ConfigBuilder, -) -> RoadsterResult> { - fs::read_dir(path)?.try_fold(config, |config, dir_entry| { - let path = dir_entry?.path(); - if path.is_dir() { - config_env_dir_recursive(&path, config) - } else if path.is_file() && path.extension().unwrap_or_default() == "toml" { - Ok(config.add_source(config::File::from(path))) - } else { - Ok(config) - } - }) -} - -#[derive(Debug, Clone, Validate, Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -#[non_exhaustive] -pub struct App { - pub name: String, - /// Shutdown the whole app if an error occurs in one of the app's top-level tasks (API, workers, etc). - #[serde(default = "default_true")] - pub shutdown_on_error: bool, -} - -#[cfg(all( - test, - feature = "http", - feature = "grpc", - feature = "sidekiq", - feature = "db-sql", - feature = "open-api", - feature = "jwt", - feature = "jwt-ietf", - feature = "otel" -))] -mod tests { - use super::*; - use insta::assert_toml_snapshot; - - #[test] - #[cfg_attr(coverage_nightly, coverage(off))] - fn test() { - let config = AppConfig::test(None).unwrap(); - - assert_toml_snapshot!(config); - } -} +#[deprecated(since = "0.6.5", note = "Moved to `roadster::config` module")] +pub use super::App; +#[deprecated(since = "0.6.5", note = "Moved to `roadster::config` module")] +pub use super::AppConfig; +#[deprecated(since = "0.6.5", note = "Moved to `roadster::config` module")] +pub use super::CustomConfig; +#[deprecated(since = "0.6.5", note = "Moved to `roadster::config` module")] +pub use super::ENV_VAR_PREFIX; +#[deprecated(since = "0.6.5", note = "Moved to `roadster::config` module")] +pub use super::ENV_VAR_SEPARATOR; diff --git a/src/config/database/mod.rs b/src/config/database/mod.rs index f5f173d9..e823a3af 100644 --- a/src/config/database/mod.rs +++ b/src/config/database/mod.rs @@ -72,7 +72,7 @@ impl From<&Database> for ConnectOptions { } #[cfg(test)] -mod deserialize_tests { +mod tests { use super::*; use crate::testing::snapshot::TestCase; use insta::{assert_debug_snapshot, assert_toml_snapshot}; @@ -104,7 +104,7 @@ mod deserialize_tests { "# )] #[cfg_attr(coverage_nightly, coverage(off))] - fn sidekiq(_case: TestCase, #[case] config: &str) { + fn serialization(_case: TestCase, #[case] config: &str) { let database: Database = toml::from_str(config).unwrap(); assert_toml_snapshot!(database); diff --git a/src/config/database/snapshots/roadster__config__database__tests__db_config_to_connect_options.snap b/src/config/database/snapshots/roadster__config__database__tests__db_config_to_connect_options.snap new file mode 100644 index 00000000..55e30fa8 --- /dev/null +++ b/src/config/database/snapshots/roadster__config__database__tests__db_config_to_connect_options.snap @@ -0,0 +1,33 @@ +--- +source: src/config/database/mod.rs +expression: connect_options +--- +ConnectOptions { + url: "postgres://example:example@example:1234/example_app", + max_connections: Some( + 20, + ), + min_connections: Some( + 10, + ), + connect_timeout: Some( + 1s, + ), + idle_timeout: Some( + 3s, + ), + acquire_timeout: Some( + 2s, + ), + max_lifetime: Some( + 4s, + ), + sqlx_logging: false, + sqlx_logging_level: Info, + sqlx_slow_statements_logging_level: Off, + sqlx_slow_statements_logging_threshold: 1s, + sqlcipher_key: None, + schema_search_path: None, + test_before_acquire: true, + connect_lazy: true, +} diff --git a/src/config/database/snapshots/roadster__config__database__tests__serialization@case_1.snap b/src/config/database/snapshots/roadster__config__database__tests__serialization@case_1.snap new file mode 100644 index 00000000..37f03427 --- /dev/null +++ b/src/config/database/snapshots/roadster__config__database__tests__serialization@case_1.snap @@ -0,0 +1,11 @@ +--- +source: src/config/database/mod.rs +expression: database +--- +uri = 'https://example.com:1234/' +auto-migrate = true +connect-timeout = 1000 +connect-lazy = true +acquire-timeout = 1000 +min-connections = 0 +max-connections = 1 diff --git a/src/config/database/snapshots/roadster__config__database__tests__serialization@case_2.snap b/src/config/database/snapshots/roadster__config__database__tests__serialization@case_2.snap new file mode 100644 index 00000000..f7e27f7b --- /dev/null +++ b/src/config/database/snapshots/roadster__config__database__tests__serialization@case_2.snap @@ -0,0 +1,13 @@ +--- +source: src/config/database/mod.rs +expression: database +--- +uri = 'https://example.com:1234/' +auto-migrate = true +connect-timeout = 1000 +connect-lazy = true +acquire-timeout = 2000 +idle-timeout = 3000 +max-lifetime = 4000 +min-connections = 0 +max-connections = 1 diff --git a/src/config/email/mod.rs b/src/config/email/mod.rs new file mode 100644 index 00000000..100d92bc --- /dev/null +++ b/src/config/email/mod.rs @@ -0,0 +1,19 @@ +#[cfg(feature = "email-smtp")] +pub mod smtp; + +use lettre::message::Mailbox; +use serde_derive::{Deserialize, Serialize}; +#[cfg(feature = "email-smtp")] +use smtp::Smtp; +use validator::Validate; + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct Email { + pub from: Mailbox, + pub reply_to: Option, + #[cfg(feature = "email-smtp")] + #[validate(nested)] + pub smtp: Smtp, +} diff --git a/src/config/email/smtp.rs b/src/config/email/smtp.rs new file mode 100644 index 00000000..dea9a931 --- /dev/null +++ b/src/config/email/smtp.rs @@ -0,0 +1,127 @@ +use crate::config::email::Email; +use lettre::message::MessageBuilder; +use serde_derive::{Deserialize, Serialize}; +use url::Url; +use validator::{Validate, ValidationErrors}; + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct Smtp { + #[validate(nested)] + pub connection: SmtpConnection, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged, rename_all = "kebab-case")] +#[non_exhaustive] +pub enum SmtpConnection { + Fields(SmtpConnectionFields), + Uri(SmtpConnectionUri), +} + +impl Validate for SmtpConnection { + fn validate(&self) -> Result<(), ValidationErrors> { + match self { + SmtpConnection::Fields(fields) => fields.validate(), + SmtpConnection::Uri(uri) => uri.validate(), + } + } +} + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct SmtpConnectionFields { + pub host: String, + pub username: String, + pub password: String, +} + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct SmtpConnectionUri { + pub uri: Url, +} + +impl From<&Email> for MessageBuilder { + fn from(value: &Email) -> Self { + let builder = MessageBuilder::new().from(value.from.clone()); + let builder = if let Some(reply_to) = value.reply_to.as_ref() { + builder.reply_to(reply_to.clone()) + } else { + builder + }; + + builder + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testing::snapshot::TestCase; + use insta::assert_toml_snapshot; + use rstest::{fixture, rstest}; + + #[fixture] + #[cfg_attr(coverage_nightly, coverage(off))] + fn case() -> TestCase { + Default::default() + } + + #[rstest] + #[case( + r#" + [from] + email = "no-reply@example.com" + + [smtp.connection] + uri = "smtps://username:password@smtp.example.com:425" + "# + )] + #[case( + r#" + from = "No Reply " + + [reply-to] + email = "no-reply@example.com" + name = "No Reply" + + [smtp.connection] + host = "smtp.example.com" + username = "username" + password = "password" + "# + )] + #[case( + r#" + [from] + email = "no-reply@example.com" + + [smtp.connection] + host = "smtp.example.com" + username = "username" + password = "password" + "# + )] + #[case( + r#" + reply-to = "No Reply " + + [from] + email = "no-reply@example.com" + name = "No Reply" + + [smtp.connection] + uri = "smtps://username:password@smtp.example.com:425" + "# + )] + #[cfg_attr(coverage_nightly, coverage(off))] + fn serialization(_case: TestCase, #[case] config: &str) { + let email: Email = toml::from_str(config).unwrap(); + + assert_toml_snapshot!(email); + } +} diff --git a/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_1.snap b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_1.snap new file mode 100644 index 00000000..07136034 --- /dev/null +++ b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_1.snap @@ -0,0 +1,7 @@ +--- +source: src/config/email/smtp.rs +expression: email +--- +from = 'no-reply@example.com' +[smtp.connection] +uri = 'smtps://username:password@smtp.example.com:425' diff --git a/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_2.snap b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_2.snap new file mode 100644 index 00000000..3c662169 --- /dev/null +++ b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_2.snap @@ -0,0 +1,10 @@ +--- +source: src/config/email/smtp.rs +expression: email +--- +from = 'No Reply ' +reply-to = 'No Reply ' +[smtp.connection] +host = 'smtp.example.com' +username = 'username' +password = 'password' diff --git a/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_3.snap b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_3.snap new file mode 100644 index 00000000..ddff805f --- /dev/null +++ b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_3.snap @@ -0,0 +1,9 @@ +--- +source: src/config/email/smtp.rs +expression: email +--- +from = 'no-reply@example.com' +[smtp.connection] +host = 'smtp.example.com' +username = 'username' +password = 'password' diff --git a/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_4.snap b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_4.snap new file mode 100644 index 00000000..a4c17fd5 --- /dev/null +++ b/src/config/email/snapshots/roadster__config__email__smtp__tests__serialization@case_4.snap @@ -0,0 +1,8 @@ +--- +source: src/config/email/smtp.rs +expression: email +--- +from = 'No Reply ' +reply-to = 'No Reply ' +[smtp.connection] +uri = 'smtps://username:password@smtp.example.com:425' diff --git a/src/config/environment.rs b/src/config/environment.rs index 98975437..c779dba5 100644 --- a/src/config/environment.rs +++ b/src/config/environment.rs @@ -1,4 +1,4 @@ -use crate::config::app_config::{ENV_VAR_PREFIX, ENV_VAR_SEPARATOR}; +use crate::config::{ENV_VAR_PREFIX, ENV_VAR_SEPARATOR}; use crate::error::RoadsterResult; use anyhow::anyhow; #[cfg(feature = "cli")] diff --git a/src/config/health_check/mod.rs b/src/config/health_check/mod.rs index 1b1a924e..ecd29a1e 100644 --- a/src/config/health_check/mod.rs +++ b/src/config/health_check/mod.rs @@ -1,5 +1,5 @@ use crate::app::context::AppContext; -use crate::config::app_config::CustomConfig; +use crate::config::CustomConfig; use crate::util::serde::default_true; use axum::extract::FromRef; use config::{FileFormat, FileSourceString}; @@ -120,7 +120,7 @@ pub struct MaxDuration { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/config/lifecycle/mod.rs b/src/config/lifecycle/mod.rs index 6b2a0512..db3c733e 100644 --- a/src/config/lifecycle/mod.rs +++ b/src/config/lifecycle/mod.rs @@ -1,5 +1,5 @@ use crate::app::context::AppContext; -use crate::config::app_config::CustomConfig; +use crate::config::CustomConfig; use crate::util::serde::default_true; use config::{FileFormat, FileSourceString}; use serde_derive::{Deserialize, Serialize}; @@ -82,7 +82,7 @@ pub struct LifecycleHandlerConfig { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/config/mod.rs b/src/config/mod.rs index 51fe9c57..e7a7369f 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,9 +1,320 @@ +use crate::config::auth::Auth; +#[cfg(feature = "db-sql")] +use crate::config::database::Database; +#[cfg(feature = "email")] +use crate::config::email::Email; +use crate::config::environment::{Environment, ENVIRONMENT_ENV_VAR_NAME}; +use crate::config::health_check::HealthCheck; +use crate::config::lifecycle::LifecycleHandler; +use crate::config::service::Service; +use crate::config::tracing::Tracing; +use crate::error::RoadsterResult; +use crate::util::serde::default_true; +use ::tracing::warn; +use config::builder::DefaultState; +use config::{Config, ConfigBuilder, FileFormat}; +use convert_case::Case; +use dotenvy::dotenv; +use serde_derive::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; +use validator::Validate; + pub mod app_config; pub mod auth; #[cfg(feature = "db-sql")] pub mod database; +#[cfg(feature = "email")] +pub mod email; pub mod environment; pub mod health_check; -mod lifecycle; +pub mod lifecycle; pub mod service; pub mod tracing; + +pub type CustomConfig = BTreeMap; + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct AppConfig { + pub environment: Environment, + #[validate(nested)] + pub app: App, + #[validate(nested)] + pub lifecycle_handler: LifecycleHandler, + #[validate(nested)] + pub health_check: HealthCheck, + #[validate(nested)] + pub service: Service, + #[validate(nested)] + pub auth: Auth, + #[validate(nested)] + pub tracing: Tracing, + #[cfg(feature = "db-sql")] + #[validate(nested)] + pub database: Database, + #[cfg(feature = "email")] + #[validate(nested)] + pub email: Email, + /// Allows providing custom config values. Any configs that aren't pre-defined above + /// will be collected here. + /// + /// # Examples + /// + /// ```toml + /// [foo] + /// x = "y" + /// ``` + /// + /// This will be parsed as: + /// ```raw + /// AppConfig#custom: { + /// "foo": { + /// "x": "y", + /// } + /// } + /// ``` + #[serde(flatten, default)] + pub custom: CustomConfig, +} + +pub const ENV_VAR_PREFIX: &str = "ROADSTER"; +pub const ENV_VAR_SEPARATOR: &str = "__"; + +impl AppConfig { + #[deprecated( + since = "0.6.2", + note = "This wasn't intended to be made public and may be removed in a future version." + )] + pub fn new(environment: Option) -> RoadsterResult { + Self::new_with_config_dir(environment, Some(PathBuf::from("config/"))) + } + + // This runs before tracing is initialized, so we need to use `println` in order to + // log from this method. + #[allow(clippy::disallowed_macros)] + pub(crate) fn new_with_config_dir( + environment: Option, + config_dir: Option, + ) -> RoadsterResult { + dotenv().ok(); + + let environment = if let Some(environment) = environment { + println!("Using environment from CLI args: {environment:?}"); + environment + } else { + Environment::new()? + }; + let environment_str: &str = environment.clone().into(); + + let config_root_dir = config_dir + .unwrap_or_else(|| PathBuf::from("config/")) + .canonicalize()?; + + println!("Loading configuration from directory {config_root_dir:?}"); + + let config = Self::default_config(environment); + let config = config_env_file("default", &config_root_dir, config); + let config = config_env_dir("default", &config_root_dir, config)?; + let config = config_env_file(environment_str, &config_root_dir, config); + let config = config_env_dir(environment_str, &config_root_dir, config)?; + let config = config + .add_source( + config::Environment::default() + .prefix(ENV_VAR_PREFIX) + .convert_case(Case::Kebab) + .separator(ENV_VAR_SEPARATOR), + ) + .set_override(ENVIRONMENT_ENV_VAR_NAME, environment_str)? + .build()?; + let config: AppConfig = config.try_deserialize()?; + + Ok(config) + } + + #[cfg(test)] + #[cfg_attr(coverage_nightly, coverage(off))] + pub(crate) fn test(config_str: Option<&str>) -> RoadsterResult { + let config = Self::default_config(Environment::Test) + .add_source(config::File::from_str( + config_str.unwrap_or( + r#" + environment = "test" + + [app] + name = "Test" + + [tracing] + level = "debug" + + [database] + uri = "postgres://example:example@invalid_host:5432/example_test" + auto-migrate = true + max-connections = 10 + + [auth.jwt] + secret = "secret-test" + + [service.http] + host = "127.0.0.1" + port = 3000 + + [service.grpc] + host = "127.0.0.1" + port = 3001 + + [service.sidekiq] + # This field normally is determined by the number of CPU cores if not provided. + # We provide it in the test config to avoid snapshot failures when running + # on varying hardware. + num-workers = 16 + + [service.sidekiq.redis] + uri = "redis://invalid_host:1234" + + [email.from] + email = "no-reply@example.com" + + [email.smtp.connection] + uri = "smtps://username:password@smtp.example.com:425" + "#, + ), + FileFormat::Toml, + )) + .build()?; + + let config: AppConfig = config.try_deserialize()?; + Ok(config) + } + + #[allow(clippy::let_and_return)] + fn default_config( + #[allow(unused_variables)] environment: Environment, + ) -> ConfigBuilder { + let config = Config::builder() + .add_source(config::File::from_str( + include_str!("default.toml"), + FileFormat::Toml, + )) + .add_source(crate::config::tracing::default_config()); + + #[cfg(feature = "http")] + let config = { + let config = config.add_source(crate::config::service::http::default_config()); + let config = crate::config::service::http::default_config_per_env(environment) + .into_iter() + .fold(config, |config, source| config.add_source(source)); + config + }; + + #[cfg(feature = "grpc")] + let config = config.add_source(crate::config::service::grpc::default_config()); + + #[cfg(feature = "sidekiq")] + let config = config.add_source(crate::config::service::worker::sidekiq::default_config()); + + let config = config.add_source(crate::config::lifecycle::default_config()); + + let config = config.add_source(crate::config::health_check::default_config()); + + config + } + + pub(crate) fn validate(&self, exit_on_error: bool) -> RoadsterResult<()> { + let result = Validate::validate(self); + if exit_on_error { + result?; + } else if let Err(err) = result { + warn!("An error occurred when validating the app config: {}", err); + } + Ok(()) + } +} + +/// Adds a config file in the relative path `config/{environment}.toml` to the +/// [`ConfigBuilder`]. If no such file exists, does nothing. +fn config_env_file( + environment: &str, + config_dir: &Path, + config: ConfigBuilder, +) -> ConfigBuilder { + // Todo: allow other file formats? + let path = config_dir.join(format!("{environment}.toml")); + if !path.is_file() { + return config; + } + + config.add_source(config::File::from(path)) +} + +/// Recursively adds all the config files in the given relative path `config/{environment}/` to the +/// [`ConfigBuilder`]. If no such directory exists, does nothing. +fn config_env_dir( + environment: &str, + config_dir: &Path, + config: ConfigBuilder, +) -> RoadsterResult> { + let path = config_dir.join(environment); + if !path.is_dir() { + return Ok(config); + } + + config_env_dir_recursive(&path, config) +} + +/// Helper method for [`config_env_dir`] to recursively add config files in the given path +/// to the [`ConfigBuilder`]. +// Todo: allow other file formats? +fn config_env_dir_recursive( + path: &Path, + config: ConfigBuilder, +) -> RoadsterResult> { + fs::read_dir(path)?.try_fold(config, |config, dir_entry| { + let path = dir_entry?.path(); + if path.is_dir() { + config_env_dir_recursive(&path, config) + } else if path.is_file() && path.extension().unwrap_or_default() == "toml" { + Ok(config.add_source(config::File::from(path))) + } else { + Ok(config) + } + }) +} + +#[derive(Debug, Clone, Validate, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +#[non_exhaustive] +pub struct App { + pub name: String, + /// Shutdown the whole app if an error occurs in one of the app's top-level tasks (API, workers, etc). + #[serde(default = "default_true")] + pub shutdown_on_error: bool, +} + +#[cfg(all( + test, + feature = "http", + feature = "grpc", + feature = "sidekiq", + feature = "db-sql", + feature = "open-api", + feature = "jwt", + feature = "jwt-ietf", + feature = "otel", + feature = "email-smtp" +))] +mod tests { + use super::*; + use insta::assert_toml_snapshot; + + #[test] + #[cfg_attr(coverage_nightly, coverage(off))] + fn test() { + let config = AppConfig::test(None).unwrap(); + + assert_toml_snapshot!(config); + } +} diff --git a/src/config/service/http/default_routes.rs b/src/config/service/http/default_routes.rs index f32e699f..bbe13096 100644 --- a/src/config/service/http/default_routes.rs +++ b/src/config/service/http/default_routes.rs @@ -82,8 +82,8 @@ impl DefaultRouteConfig { #[cfg(test)] mod tests { - use crate::config::app_config::AppConfig; use crate::config::service::http::*; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/config/service/http/initializer.rs b/src/config/service/http/initializer.rs index af7e066e..57462230 100644 --- a/src/config/service/http/initializer.rs +++ b/src/config/service/http/initializer.rs @@ -1,5 +1,5 @@ use crate::app::context::AppContext; -use crate::config::app_config::CustomConfig; +use crate::config::CustomConfig; use crate::service::http::initializer::normalize_path::NormalizePathConfig; use crate::util::serde::default_true; use axum::extract::FromRef; diff --git a/src/config/service/http/middleware.rs b/src/config/service/http/middleware.rs index 2d82baee..3f9f79c1 100644 --- a/src/config/service/http/middleware.rs +++ b/src/config/service/http/middleware.rs @@ -1,5 +1,5 @@ use crate::app::context::AppContext; -use crate::config::app_config::CustomConfig; +use crate::config::CustomConfig; use crate::service::http::middleware::catch_panic::CatchPanicConfig; use crate::service::http::middleware::compression::{ RequestDecompressionConfig, ResponseCompressionConfig, @@ -127,7 +127,7 @@ pub struct MiddlewareConfig { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/config/snapshots/roadster__config__app_config__tests__test.snap b/src/config/snapshots/roadster__config__tests__test.snap similarity index 95% rename from src/config/snapshots/roadster__config__app_config__tests__test.snap rename to src/config/snapshots/roadster__config__tests__test.snap index d0f1c66c..3e73a48b 100644 --- a/src/config/snapshots/roadster__config__app_config__tests__test.snap +++ b/src/config/snapshots/roadster__config__tests__test.snap @@ -1,5 +1,5 @@ --- -source: src/config/app_config.rs +source: src/config/mod.rs expression: config --- environment = 'test' @@ -162,3 +162,8 @@ connect-lazy = true acquire-timeout = 1000 min-connections = 0 max-connections = 10 + +[email] +from = 'no-reply@example.com' +[email.smtp.connection] +uri = 'smtps://username:password@smtp.example.com:425' diff --git a/src/health_check/database.rs b/src/health_check/database.rs index 45938786..25a4a73e 100644 --- a/src/health_check/database.rs +++ b/src/health_check/database.rs @@ -37,7 +37,7 @@ fn enabled(context: &AppContext) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/health_check/default.rs b/src/health_check/default.rs index 636ef909..754d8e78 100644 --- a/src/health_check/default.rs +++ b/src/health_check/default.rs @@ -37,7 +37,7 @@ pub fn default_health_checks( #[cfg(all(test, feature = "sidekiq", feature = "db-sql",))] mod tests { use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use crate::testing::snapshot::TestCase; use bb8::Pool; use insta::assert_toml_snapshot; diff --git a/src/health_check/registry.rs b/src/health_check/registry.rs index 64815146..e5fad317 100644 --- a/src/health_check/registry.rs +++ b/src/health_check/registry.rs @@ -59,7 +59,7 @@ impl HealthCheckRegistry { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use crate::health_check::MockHealthCheck; use rstest::rstest; diff --git a/src/health_check/sidekiq_enqueue.rs b/src/health_check/sidekiq_enqueue.rs index f0883e3c..ba3e29fc 100644 --- a/src/health_check/sidekiq_enqueue.rs +++ b/src/health_check/sidekiq_enqueue.rs @@ -37,7 +37,7 @@ fn enabled(context: &AppContext) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/health_check/sidekiq_fetch.rs b/src/health_check/sidekiq_fetch.rs index 6984b9ee..6d451eca 100644 --- a/src/health_check/sidekiq_fetch.rs +++ b/src/health_check/sidekiq_fetch.rs @@ -46,7 +46,7 @@ fn enabled(context: &AppContext) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use bb8::Pool; use rstest::rstest; use sidekiq::RedisConnectionManager; diff --git a/src/lifecycle/db_migration.rs b/src/lifecycle/db_migration.rs index ebacf385..5417d89c 100644 --- a/src/lifecycle/db_migration.rs +++ b/src/lifecycle/db_migration.rs @@ -56,7 +56,7 @@ where mod tests { use super::*; use crate::app::MockApp; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/lifecycle/mod.rs b/src/lifecycle/mod.rs index 875a25ec..67080132 100644 --- a/src/lifecycle/mod.rs +++ b/src/lifecycle/mod.rs @@ -12,7 +12,7 @@ use axum::extract::FromRef; /// Trait used to hook into various stages of the app's lifecycle. /// /// The app's lifecycle generally looks something like this: -/// 1. Parse the [`crate::config::app_config::AppConfig`] +/// 1. Parse the [`crate::config::AppConfig`] /// 2. Initialize tracing to enable logs/traces /// 3. Build the [`crate::app::context::AppContext`] and the [`crate::app::App`]'s custom state /// 4. Run the roadster/app CLI command, if one was specified when the app was started diff --git a/src/service/http/initializer/default.rs b/src/service/http/initializer/default.rs index 0f17be0e..de0a3e80 100644 --- a/src/service/http/initializer/default.rs +++ b/src/service/http/initializer/default.rs @@ -21,7 +21,7 @@ where #[cfg(test)] mod tests { use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/catch_panic.rs b/src/service/http/middleware/catch_panic.rs index 4ce721cd..4b1e9c63 100644 --- a/src/service/http/middleware/catch_panic.rs +++ b/src/service/http/middleware/catch_panic.rs @@ -56,7 +56,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/compression.rs b/src/service/http/middleware/compression.rs index d39628e7..aec7a8ef 100644 --- a/src/service/http/middleware/compression.rs +++ b/src/service/http/middleware/compression.rs @@ -105,7 +105,7 @@ where mod tests { use super::*; use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/cors.rs b/src/service/http/middleware/cors.rs index 94c37aba..bacbb73b 100644 --- a/src/service/http/middleware/cors.rs +++ b/src/service/http/middleware/cors.rs @@ -285,7 +285,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use crate::testing::snapshot::TestCase; use crate::util::serde::Wrapper; use insta::assert_toml_snapshot; diff --git a/src/service/http/middleware/default.rs b/src/service/http/middleware/default.rs index 67542544..04773160 100644 --- a/src/service/http/middleware/default.rs +++ b/src/service/http/middleware/default.rs @@ -45,7 +45,7 @@ where #[cfg(test)] mod tests { use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use crate::testing::snapshot::TestCase; use insta::assert_toml_snapshot; use itertools::Itertools; diff --git a/src/service/http/middleware/request_id.rs b/src/service/http/middleware/request_id.rs index 54e63b67..a7523713 100644 --- a/src/service/http/middleware/request_id.rs +++ b/src/service/http/middleware/request_id.rs @@ -156,7 +156,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/sensitive_headers.rs b/src/service/http/middleware/sensitive_headers.rs index 950b0fce..fa752313 100644 --- a/src/service/http/middleware/sensitive_headers.rs +++ b/src/service/http/middleware/sensitive_headers.rs @@ -167,7 +167,7 @@ where mod tests { use super::*; use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/size_limit.rs b/src/service/http/middleware/size_limit.rs index 24295b2d..ebc15b6c 100644 --- a/src/service/http/middleware/size_limit.rs +++ b/src/service/http/middleware/size_limit.rs @@ -89,7 +89,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/timeout.rs b/src/service/http/middleware/timeout.rs index c9a883d1..94638ec3 100644 --- a/src/service/http/middleware/timeout.rs +++ b/src/service/http/middleware/timeout.rs @@ -81,7 +81,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/tracing/mod.rs b/src/service/http/middleware/tracing/mod.rs index 3eba6301..81a843a7 100644 --- a/src/service/http/middleware/tracing/mod.rs +++ b/src/service/http/middleware/tracing/mod.rs @@ -180,7 +180,7 @@ impl OnResponse for CustomOnResponse { #[cfg(test)] mod tests { use super::*; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use rstest::rstest; #[rstest] diff --git a/src/service/http/middleware/tracing/req_res_logging.rs b/src/service/http/middleware/tracing/req_res_logging.rs index 76e5d79c..24eb8348 100644 --- a/src/service/http/middleware/tracing/req_res_logging.rs +++ b/src/service/http/middleware/tracing/req_res_logging.rs @@ -134,7 +134,7 @@ async fn log_body(body: Body, max_len: i32, req: bool) -> Result RedisCommands for PooledConnection<'a, RedisConnectionManager> { mod tests { use super::*; use crate::app::context::AppContext; - use crate::config::app_config::AppConfig; + use crate::config::AppConfig; use bb8::Pool; use rstest::rstest; use sidekiq::RedisConnectionManager; diff --git a/src/tracing/mod.rs b/src/tracing/mod.rs index 3cd63366..9a2cbdb5 100644 --- a/src/tracing/mod.rs +++ b/src/tracing/mod.rs @@ -22,8 +22,8 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::EnvFilter; -use crate::config::app_config::AppConfig; use crate::config::tracing::Format; +use crate::config::AppConfig; use crate::error::RoadsterResult; pub fn init_tracing(