From 4d865beb34e54ef7ef169890c725f1cb3db8ed7f Mon Sep 17 00:00:00 2001 From: Mason Stallmo Date: Sat, 21 Sep 2024 00:32:01 -0700 Subject: [PATCH 1/4] Implement shared application state as a trait Transition from using the `AppContext` struct internally to using `AppContextTrait` to represent shared global state. This allows end users to implement and extend their own shared application state beyond what loco provides out of the box. The `AppContextTrait` is implemented for `AppContext` so `AppContext` can be used directly by users that don't have a need to extend what is already provided by the context. --- loco-extras/src/initializers/extra_db.rs | 6 +- loco-extras/src/initializers/mongodb/mod.rs | 6 +- loco-extras/src/initializers/multi_db.rs | 6 +- .../src/initializers/normalize_path.rs | 4 +- .../src/initializers/opentelemetry/mod.rs | 6 +- loco-extras/src/initializers/prometheus.rs | 4 +- src/app.rs | 158 ++++++++++++++++-- src/banner.rs | 36 ++-- src/boot.rs | 110 ++++++------ src/cli.rs | 58 +++---- src/controller/app_routes.rs | 101 +++++------ src/controller/describe.rs | 4 +- src/controller/health.rs | 12 +- src/controller/mod.rs | 12 +- src/controller/ping.rs | 4 +- src/controller/routes.rs | 24 +-- src/db.rs | 15 +- src/gen/controller.rs | 7 +- src/gen/mod.rs | 17 +- src/gen/model.rs | 8 +- src/gen/scaffold.rs | 9 +- src/logger.rs | 14 +- src/mailer/mod.rs | 14 +- src/scheduler.rs | 27 ++- src/task.rs | 15 +- src/testing.rs | 16 +- src/tests_cfg/db.rs | 8 +- src/tests_cfg/task.rs | 6 +- src/worker.rs | 13 +- 29 files changed, 447 insertions(+), 273 deletions(-) diff --git a/loco-extras/src/initializers/extra_db.rs b/loco-extras/src/initializers/extra_db.rs index 95c67cf7c..c982844b9 100644 --- a/loco-extras/src/initializers/extra_db.rs +++ b/loco-extras/src/initializers/extra_db.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use axum::{Extension, Router as AxumRouter}; -use loco_rs::{db, prelude::*}; +use loco_rs::{app::Context, db, prelude::*}; #[allow(clippy::module_name_repetitions)] pub struct ExtraDbInitializer; @@ -11,9 +11,9 @@ impl Initializer for ExtraDbInitializer { "extra-db".to_string() } - async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, ctx: &dyn Context) -> Result { let extra_db_config = ctx - .config + .config() .initializers .clone() .ok_or_else(|| Error::Message("initializers config not configured".to_string()))?; diff --git a/loco-extras/src/initializers/mongodb/mod.rs b/loco-extras/src/initializers/mongodb/mod.rs index 87564ae4e..f7cae508e 100644 --- a/loco-extras/src/initializers/mongodb/mod.rs +++ b/loco-extras/src/initializers/mongodb/mod.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use axum::{Extension, Router as AxumRouter}; -use loco_rs::prelude::*; +use loco_rs::{app::Context, prelude::*}; use mongodb::{bson::doc, options::ClientOptions, Client, Database}; #[allow(clippy::module_name_repetitions)] @@ -12,9 +12,9 @@ impl Initializer for MongoDbInitializer { "mongodb".to_string() } - async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, ctx: &dyn Context) -> Result { let mongo_db_config = ctx - .config + .config() .initializers .clone() .ok_or_else(|| Error::Message("initializers config not configured".to_string()))?; diff --git a/loco-extras/src/initializers/multi_db.rs b/loco-extras/src/initializers/multi_db.rs index 6c4cffbe3..b050544d4 100644 --- a/loco-extras/src/initializers/multi_db.rs +++ b/loco-extras/src/initializers/multi_db.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use axum::{Extension, Router as AxumRouter}; -use loco_rs::{db, errors::Error, prelude::*}; +use loco_rs::{app::Context, db, errors::Error, prelude::*}; #[allow(clippy::module_name_repetitions)] pub struct MultiDbInitializer; @@ -11,9 +11,9 @@ impl Initializer for MultiDbInitializer { "multi-db".to_string() } - async fn after_routes(&self, router: AxumRouter, ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, ctx: &dyn Context) -> Result { let settings = ctx - .config + .config() .initializers .clone() .ok_or_else(|| Error::Message("settings config not configured".to_string()))?; diff --git a/loco-extras/src/initializers/normalize_path.rs b/loco-extras/src/initializers/normalize_path.rs index 2c2ebd012..3c275c1c4 100644 --- a/loco-extras/src/initializers/normalize_path.rs +++ b/loco-extras/src/initializers/normalize_path.rs @@ -15,7 +15,7 @@ //! [axum-docs]: https://docs.rs/axum/latest/axum/middleware/index.html#rewriting-request-uri-in-middleware use async_trait::async_trait; use axum::Router; -use loco_rs::prelude::*; +use loco_rs::{app::Context, prelude::*}; use tower::Layer; use tower_http::normalize_path::NormalizePathLayer; @@ -28,7 +28,7 @@ impl Initializer for NormalizePathInitializer { "normalize-path".to_string() } - async fn after_routes(&self, router: Router, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: Router, _ctx: &dyn Context) -> Result { let router = NormalizePathLayer::trim_trailing_slash().layer(router); let router = Router::new().nest_service("", router); Ok(router) diff --git a/loco-extras/src/initializers/opentelemetry/mod.rs b/loco-extras/src/initializers/opentelemetry/mod.rs index dc7330fa7..7cd420403 100644 --- a/loco-extras/src/initializers/opentelemetry/mod.rs +++ b/loco-extras/src/initializers/opentelemetry/mod.rs @@ -1,7 +1,7 @@ use axum::{async_trait, Router as AxumRouter}; use axum_tracing_opentelemetry::middleware::{OtelAxumLayer, OtelInResponseLayer}; use loco_rs::{ - app::{AppContext, Initializer}, + app::{AppContext, Context, Initializer}, Error, Result, }; @@ -13,7 +13,7 @@ impl Initializer for OpenTelemetryInitializer { "opentelemetry".to_string() } - async fn before_run(&self, _app_context: &AppContext) -> Result<()> { + async fn before_run(&self, _app_context: &dyn Context) -> Result<()> { match init_tracing_opentelemetry::tracing_subscriber_ext::init_subscribers() { Ok(_) => Ok(()), Err(e) => { @@ -23,7 +23,7 @@ impl Initializer for OpenTelemetryInitializer { } } - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, _ctx: &dyn Context) -> Result { let router = router .layer(OtelInResponseLayer::default()) .layer(OtelAxumLayer::default()); diff --git a/loco-extras/src/initializers/prometheus.rs b/loco-extras/src/initializers/prometheus.rs index c071752a4..c1e664f11 100644 --- a/loco-extras/src/initializers/prometheus.rs +++ b/loco-extras/src/initializers/prometheus.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use axum::Router as AxumRouter; use axum_prometheus::PrometheusMetricLayer; -use loco_rs::prelude::*; +use loco_rs::{app::Context, prelude::*}; pub struct AxumPrometheusInitializer; @@ -11,7 +11,7 @@ impl Initializer for AxumPrometheusInitializer { "axum-prometheus".to_string() } - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, _ctx: &dyn Context) -> Result { let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair(); let router = router .route("/metrics", get(|| async move { metric_handle.render() })) diff --git a/src/app.rs b/src/app.rs index 5c0b9c9ba..6f9c28cf5 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,18 +15,45 @@ use axum::Router as AxumRouter; #[cfg(feature = "channels")] use crate::controller::channels::AppChannels; use crate::{ - boot::{BootResult, ServeParams, StartMode}, - cache::{self}, + boot::{create_mailer, BootResult, ServeParams, StartMode}, + cache::{self, Cache}, config::{self, Config}, controller::AppRoutes, environment::Environment, mailer::EmailSender, - storage::Storage, + storage::{self, Storage}, task::Tasks, worker::{Pool, Processor, RedisConnectionManager}, Result, }; +pub trait Context: Send + Sync + 'static { + fn environment(&self) -> &Environment; + #[cfg(feature = "with-db")] + fn db(&self) -> &DatabaseConnection; + fn queue(&self) -> &Option>; + fn config(&self) -> &Config; + fn mailer(&self) -> &Option; + fn storage(&self) -> Arc; + fn cache(&self) -> Arc; +} + +pub trait AppContextTrait: Clone + Default + Context { + #[cfg(feature = "with-db")] + fn create( + environment: Environment, + config: Config, + db: DatabaseConnection, + queue: Option>, + ) -> Result; + #[cfg(not(feature = "with-db"))] + fn create( + environment: Environment, + config: Config, + queue: Option>, + ) -> Result; +} + /// Represents the application context for a web server. /// /// This struct encapsulates various components and configurations required by @@ -53,6 +80,107 @@ pub struct AppContext { pub cache: Arc, } +impl Default for AppContext { + fn default() -> Self { + let environment = Environment::Test; + #[cfg(feature = "with-db")] + let db = DatabaseConnection::default(); + let config = environment + .load() + .expect("Failed to load config for test environment"); + + AppContext { + environment, + #[cfg(feature = "with-db")] + db, + queue: None, + storage: Storage::single(storage::drivers::null::new()).into(), + cache: Cache::new(cache::drivers::null::new()).into(), + config, + mailer: None, + } + } +} + +impl Context for AppContext { + fn environment(&self) -> &Environment { + &self.environment + } + + #[cfg(feature = "with-db")] + fn db(&self) -> &DatabaseConnection { + &self.db + } + + fn queue(&self) -> &Option> { + &self.queue + } + + fn config(&self) -> &Config { + &self.config + } + + fn mailer(&self) -> &Option { + &self.mailer + } + + fn storage(&self) -> Arc { + self.storage.clone() + } + + fn cache(&self) -> Arc { + self.cache.clone() + } +} + +impl AppContextTrait for AppContext { + #[cfg(feature = "with-db")] + fn create( + environment: Environment, + config: Config, + db: DatabaseConnection, + queue: Option>, + ) -> Result { + let mailer = if let Some(cfg) = config.mailer.as_ref() { + create_mailer(cfg)? + } else { + None + }; + + Ok(AppContext { + environment, + db, + queue, + storage: Storage::single(storage::drivers::null::new()).into(), + cache: Cache::new(cache::drivers::null::new()).into(), + config, + mailer, + }) + } + + #[cfg(not(feature = "with-db"))] + fn create( + environment: Environment, + config: Config, + queue: Option>, + ) -> Result { + let mailer = if let Some(cfg) = config.mailer.as_ref() { + create_mailer(cfg)? + } else { + None + }; + + Ok(AppContext { + environment, + queue, + storage: Storage::single(storage::drivers::null::new()).into(), + cache: Cache::new(cache::drivers::null::new()).into(), + config, + mailer, + }) + } +} + /// A trait that defines hooks for customizing and extending the behavior of a /// web server application. /// @@ -60,7 +188,7 @@ pub struct AppContext { /// the application's routing, worker connections, task registration, and /// database actions according to their specific requirements and use cases. #[async_trait] -pub trait Hooks { +pub trait Hooks { /// Defines the composite app version #[must_use] fn app_version() -> String { @@ -101,7 +229,7 @@ pub trait Hooks { /// /// # Errors /// Could not boot the application - async fn boot(mode: StartMode, environment: &Environment) -> Result; + async fn boot(mode: StartMode, environment: &Environment) -> Result>; /// Start serving the Axum web application on the specified address and /// port. @@ -141,7 +269,7 @@ pub trait Hooks { /// /// # Errors /// Return an [`Result`] when the router could not be created - async fn before_routes(_ctx: &AppContext) -> Result> { + async fn before_routes(_ctx: &AC) -> Result> { Ok(AxumRouter::new()) } @@ -151,39 +279,39 @@ pub trait Hooks { /// /// # Errors /// Axum router error - async fn after_routes(router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(router: AxumRouter, _ctx: &AC) -> Result { Ok(router) } /// Provide a list of initializers /// An initializer can be used to seamlessly add functionality to your app /// or to initialize some aspects of it. - async fn initializers(_ctx: &AppContext) -> Result>> { + async fn initializers(_ctx: &AC) -> Result>> { Ok(vec![]) } /// Calling the function before run the app /// You can now code some custom loading of resources or other things before /// the app runs - async fn before_run(_app_context: &AppContext) -> Result<()> { + async fn before_run(_app_context: &AC) -> Result<()> { Ok(()) } /// Defines the application's routing configuration. - fn routes(_ctx: &AppContext) -> AppRoutes; + fn routes(_ctx: &AC) -> AppRoutes; // Provides the options to change Loco [`AppContext`] after initialization. - async fn after_context(ctx: AppContext) -> Result { + async fn after_context(ctx: AC) -> Result { Ok(ctx) } #[cfg(feature = "channels")] /// Register channels endpoints to the application routers - fn register_channels(_ctx: &AppContext) -> AppChannels; + fn register_channels(_ctx: &AC) -> AppChannels; /// Connects custom workers to the application using the provided /// [`Processor`] and [`AppContext`]. - fn connect_workers<'a>(p: &'a mut Processor, ctx: &'a AppContext); + fn connect_workers<'a>(p: &'a mut Processor, ctx: &'a AC); /// Registers custom tasks with the provided [`Tasks`] object. fn register_tasks(tasks: &mut Tasks); @@ -212,14 +340,14 @@ pub trait Initializer: Sync + Send { /// Occurs after the app's `before_run`. /// Use this to for one-time initializations, load caches, perform web /// hooks, etc. - async fn before_run(&self, _app_context: &AppContext) -> Result<()> { + async fn before_run(&self, _app_context: &dyn Context) -> Result<()> { Ok(()) } /// Occurs after the app's `after_routes`. /// Use this to compose additional functionality and wire it into an Axum /// Router - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, _ctx: &dyn Context) -> Result { Ok(router) } } diff --git a/src/banner.rs b/src/banner.rs index a7af381cd..33042a006 100644 --- a/src/banner.rs +++ b/src/banner.rs @@ -1,31 +1,37 @@ use colored::Colorize; -use crate::boot::{BootResult, ServeParams}; +use crate::{ + app::AppContextTrait, + boot::{BootResult, ServeParams}, +}; pub const BANNER: &str = r" - ▄ ▀ - ▀ ▄ - ▄ ▀ ▄ ▄ ▄▀ - ▄ ▀▄▄ - ▄ ▀ ▀ ▀▄▀█▄ - ▀█▄ -▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ - ██████ █████ ███ █████ ███ █████ ███ ▀█ - ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ + ▄ ▀ + ▀ ▄ + ▄ ▀ ▄ ▄ ▄▀ + ▄ ▀▄▄ + ▄ ▀ ▀ ▀▄▀█▄ + ▀█▄ +▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█ + ██████ █████ ███ █████ ███ █████ ███ ▀█ + ██████ █████ ███ █████ ▀▀▀ █████ ███ ▄█▄ ██████ █████ ███ █████ █████ ███ ████▄ ██████ █████ ███ █████ ▄▄▄ █████ ███ █████ ██████ █████ ███ ████ ███ █████ ███ ████▀ - ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ - ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ + ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀ ██▀ + ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ https://loco.rs "; -pub fn print_banner(boot_result: &BootResult, server_config: &ServeParams) { +pub fn print_banner( + boot_result: &BootResult, + server_config: &ServeParams, +) { let ctx = &boot_result.app_context; println!("{BANNER}"); - let config = &ctx.config; + let config = ctx.config(); - println!("environment: {}", ctx.environment.to_string().green()); + println!("environment: {}", ctx.environment().to_string().green()); #[cfg(feature = "with-db")] { diff --git a/src/boot.rs b/src/boot.rs index fcf95da75..c20b34279 100644 --- a/src/boot.rs +++ b/src/boot.rs @@ -11,9 +11,8 @@ use tracing::{info, trace, warn}; #[cfg(feature = "with-db")] use crate::db; use crate::{ - app::{AppContext, Hooks}, + app::{AppContextTrait, Hooks}, banner::print_banner, - cache, config::{self, Config}, controller::ListRoutes, environment::Environment, @@ -21,7 +20,6 @@ use crate::{ mailer::{EmailSender, MailerWorker}, redis, scheduler::{self, Scheduler}, - storage::{self, Storage}, task::{self, Tasks}, worker::{self, AppWorker, Pool, Processor, RedisConnectionManager}, Result, @@ -37,9 +35,9 @@ pub enum StartMode { /// Pulling job worker and execute them WorkerOnly, } -pub struct BootResult { +pub struct BootResult { /// Application Context - pub app_context: AppContext, + pub app_context: AC, /// Web server routes pub router: Option, /// worker processor @@ -64,7 +62,10 @@ pub struct ServeParams { /// # Errors /// /// When could not initialize the application. -pub async fn start(boot: BootResult, server_config: ServeParams) -> Result<()> { +pub async fn start>( + boot: BootResult, + server_config: ServeParams, +) -> Result<()> { print_banner(&boot, &server_config); let BootResult { @@ -103,8 +104,8 @@ async fn process(processor: Processor) -> Result<()> { /// # Errors /// /// When running could not run the task. -pub async fn run_task( - app_context: &AppContext, +pub async fn run_task>( + app_context: &AC, task: Option<&String>, vars: &task::Vars, ) -> Result<()> { @@ -135,8 +136,8 @@ pub async fn run_task( /// # Errors /// /// When running could not run the scheduler. -pub async fn run_scheduler( - app_context: &AppContext, +pub async fn run_scheduler>( + app_context: &AC, config: Option<&PathBuf>, name: Option, tag: Option, @@ -149,10 +150,10 @@ pub async fn run_scheduler( let _guard = task_span.enter(); let scheduler = match config { - Some(path) => Scheduler::from_config::(path, &app_context.environment)?, + Some(path) => Scheduler::from_config::(path, app_context.environment())?, None => { - if let Some(config) = &app_context.config.scheduler { - Scheduler::new::(config, &app_context.environment)? + if let Some(config) = &app_context.config().scheduler { + Scheduler::new::(config, app_context.environment())? } else { return Err(Error::Scheduler(scheduler::Error::Empty)); } @@ -193,35 +194,35 @@ pub enum RunDbCommand { /// /// Return an error when the given command fails. mostly return /// [`sea_orm::DbErr`] -pub async fn run_db( - app_context: &AppContext, +pub async fn run_db, M: MigratorTrait>( + app_context: &AC, cmd: RunDbCommand, ) -> Result<()> { match cmd { RunDbCommand::Migrate => { tracing::warn!("migrate:"); - db::migrate::(&app_context.db).await?; + db::migrate::(app_context.db()).await?; } RunDbCommand::Down(steps) => { tracing::warn!("down:"); - db::down::(&app_context.db, steps).await?; + db::down::(app_context.db(), steps).await?; } RunDbCommand::Reset => { tracing::warn!("reset:"); - db::reset::(&app_context.db).await?; + db::reset::(app_context.db()).await?; } RunDbCommand::Status => { tracing::warn!("status:"); - db::status::(&app_context.db).await?; + db::status::(app_context.db()).await?; } RunDbCommand::Entities => { tracing::warn!("entities:"); - tracing::warn!("{}", db::entities::(app_context).await?); + tracing::warn!("{}", db::entities::(app_context).await?); } RunDbCommand::Truncate => { tracing::warn!("truncate:"); - H::truncate(&app_context.db).await?; + H::truncate(app_context.db()).await?; } } Ok(()) @@ -232,7 +233,9 @@ pub async fn run_db( /// /// # Errors /// When has an error to create DB connection. -pub async fn create_context(environment: &Environment) -> Result { +pub async fn create_context>( + environment: &Environment, +) -> Result { let config = environment.load()?; if config.logger.pretty_backtrace { @@ -242,26 +245,18 @@ pub async fn create_context(environment: &Environment) -> Result(environment: &Environment) -> Result( +pub async fn create_app, M: MigratorTrait>( mode: StartMode, environment: &Environment, -) -> Result { - let app_context = create_context::(environment).await?; - db::converge::(&app_context.db, &app_context.config.database).await?; +) -> Result> { + let app_context = create_context::(environment).await?; + db::converge::(app_context.db(), &app_context.config().database).await?; - if let Some(pool) = &app_context.queue { - redis::converge(pool, &app_context.config.queue).await?; + if let Some(pool) = app_context.queue() { + redis::converge(pool, &app_context.config().queue).await?; } - run_app::(&mode, app_context).await + run_app::(&mode, app_context).await } #[cfg(not(feature = "with-db"))] -pub async fn create_app( +pub async fn create_app>( mode: StartMode, environment: &Environment, ) -> Result { - let app_context = create_context::(environment).await?; + let app_context = create_context::(environment).await?; if let Some(pool) = &app_context.queue { redis::converge(pool, &app_context.config.queue).await?; } - run_app::(&mode, app_context).await + run_app::(&mode, app_context).await } /// Run the application with the given mode /// # Errors /// /// When could not create the application -pub async fn run_app(mode: &StartMode, app_context: AppContext) -> Result { +pub async fn run_app>( + mode: &StartMode, + app_context: AC, +) -> Result> { H::before_run(&app_context).await?; let initializers = H::initializers(&app_context).await?; info!(initializers = ?initializers.iter().map(|init| init.name()).collect::>().join(","), "initializers loaded"); @@ -326,7 +324,7 @@ pub async fn run_app(mode: &StartMode, app_context: AppContext) -> Res }) } StartMode::ServerAndWorker => { - let processor = create_processor::(&app_context)?; + let processor = create_processor::(&app_context)?; let app = H::before_routes(&app_context).await?; let app = H::routes(&app_context).to_router(app_context.clone(), app)?; let mut router = H::after_routes(app, &app_context).await?; @@ -340,7 +338,7 @@ pub async fn run_app(mode: &StartMode, app_context: AppContext) -> Res }) } StartMode::WorkerOnly => { - let processor = create_processor::(&app_context)?; + let processor = create_processor::(&app_context)?; Ok(BootResult { app_context, router: None, @@ -350,13 +348,13 @@ pub async fn run_app(mode: &StartMode, app_context: AppContext) -> Res } } /// Creates and configures a [`Processor`] for handling worker tasks. -fn create_processor(app_context: &AppContext) -> Result { - let queues = worker::get_queues(&app_context.config.workers.queues); +fn create_processor>(app_context: &AC) -> Result { + let queues = worker::get_queues(&app_context.config().workers.queues); trace!( queues = ?queues, "registering queues (merged config and default)" ); - let mut p = if let Some(queue) = &app_context.queue { + let mut p = if let Some(queue) = app_context.queue() { Processor::new(queue.clone(), queues) } else { return Err(Error::Message( @@ -372,13 +370,13 @@ fn create_processor(app_context: &AppContext) -> Result { } #[must_use] -pub fn list_endpoints(ctx: &AppContext) -> Vec { +pub fn list_endpoints>(ctx: &AC) -> Vec> { H::routes(ctx).collect() } /// Initializes an [`EmailSender`] based on the mailer configuration settings /// ([`config::Mailer`]). -fn create_mailer(config: &config::Mailer) -> Result> { +pub(crate) fn create_mailer(config: &config::Mailer) -> Result> { if config.stub { return Ok(Some(EmailSender::stub())); } diff --git a/src/cli.rs b/src/cli.rs index 7f20e737e..2732a836c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,7 +29,7 @@ use std::path::PathBuf; use clap::{Parser, Subcommand}; use crate::{ - app::{AppContext, Hooks}, + app::{AppContextTrait, Hooks}, boot::{ create_app, create_context, list_endpoints, run_scheduler, run_task, start, RunDbCommand, ServeParams, StartMode, @@ -366,11 +366,11 @@ where /// # Errors /// /// When could not create app context -pub async fn playground() -> crate::Result { +pub async fn playground>() -> crate::Result { let cli = Playground::parse(); let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into(); - let app_context = create_context::(&environment).await?; + let app_context = create_context::(&environment).await?; Ok(app_context) } @@ -399,14 +399,14 @@ pub async fn playground() -> crate::Result { /// } /// ``` #[cfg(feature = "with-db")] -pub async fn main() -> crate::Result<()> { +pub async fn main, M: MigratorTrait>() -> crate::Result<()> { let cli: Cli = Cli::parse(); let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into(); let config = environment.load()?; if !H::init_logger(&config, &environment)? { - logger::init::(&config.logger); + logger::init::(&config.logger); } let task_span = create_root_span(&environment); @@ -427,31 +427,31 @@ pub async fn main() -> crate::Result<()> { StartMode::ServerOnly }; - let boot_result = create_app::(start_mode, &environment).await?; + let boot_result = create_app::(start_mode, &environment).await?; let serve_params = ServeParams { - port: port.map_or(boot_result.app_context.config.server.port, |p| p), + port: port.map_or(boot_result.app_context.config().server.port, |p| p), binding: binding - .unwrap_or_else(|| boot_result.app_context.config.server.binding.to_string()), + .unwrap_or_else(|| boot_result.app_context.config().server.binding.to_string()), }; - start::(boot_result, serve_params).await?; + start::(boot_result, serve_params).await?; } #[cfg(feature = "with-db")] Commands::Db { command } => { if matches!(command, DbCommands::Create) { db::create(&environment.load()?.database.uri).await?; } else { - let app_context = create_context::(&environment).await?; - run_db::(&app_context, command.into()).await?; + let app_context = create_context::(&environment).await?; + run_db::(&app_context, command.into()).await?; } } Commands::Routes {} => { - let app_context = create_context::(&environment).await?; - show_list_endpoints::(&app_context); + let app_context = create_context::(&environment).await?; + show_list_endpoints::(&app_context); } Commands::Task { name, params } => { let vars = task::Vars::from_cli_args(params); - let app_context = create_context::(&environment).await?; - run_task::(&app_context, name.as_ref(), &vars).await?; + let app_context = create_context::(&environment).await?; + run_task::(&app_context, name.as_ref(), &vars).await?; } Commands::Scheduler { name, @@ -459,11 +459,11 @@ pub async fn main() -> crate::Result<()> { tag, list, } => { - let app_context = create_context::(&environment).await?; - run_scheduler::(&app_context, config.as_ref(), name, tag, list).await?; + let app_context = create_context::(&environment).await?; + run_scheduler::(&app_context, config.as_ref(), name, tag, list).await?; } Commands::Generate { component } => { - gen::generate::(component.try_into()?, &config)?; + gen::generate::(component.try_into()?, &config)?; } Commands::Doctor { config: config_arg } => { if config_arg { @@ -490,14 +490,14 @@ pub async fn main() -> crate::Result<()> { } #[cfg(not(feature = "with-db"))] -pub async fn main() -> crate::Result<()> { +pub async fn main>() -> crate::Result<()> { let cli = Cli::parse(); let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into(); let config = environment.load()?; if !H::init_logger(&config, &environment)? { - logger::init::(&config.logger); + logger::init::(&config.logger); } let task_span = create_root_span(&environment); @@ -518,7 +518,7 @@ pub async fn main() -> crate::Result<()> { StartMode::ServerOnly }; - let boot_result = create_app::(start_mode, &environment).await?; + let boot_result = create_app::(start_mode, &environment).await?; let serve_params = ServeParams { port: port.map_or(boot_result.app_context.config.server.port, |p| p), binding: binding.map_or( @@ -526,15 +526,15 @@ pub async fn main() -> crate::Result<()> { |b| b, ), }; - start::(boot_result, serve_params).await?; + start::(boot_result, serve_params).await?; } Commands::Routes {} => { - let app_context = create_context::(&environment).await?; + let app_context = create_context::(&environment).await?; show_list_endpoints::(&app_context) } Commands::Task { name, params } => { let vars = task::Vars::from_cli_args(params); - let app_context = create_context::(&environment).await?; + let app_context = create_context::(&environment).await?; run_task::(&app_context, name.as_ref(), &vars).await?; } Commands::Scheduler { @@ -543,11 +543,11 @@ pub async fn main() -> crate::Result<()> { tag, list, } => { - let app_context = create_context::(&environment).await?; - run_scheduler::(&app_context, config.as_ref(), name, tag, list).await?; + let app_context = create_context::(&environment).await?; + run_scheduler::(&app_context, config.as_ref(), name, tag, list).await?; } Commands::Generate { component } => { - gen::generate::(component.try_into()?, &config)?; + gen::generate::(component.try_into()?, &config)?; } Commands::Version {} => { println!("{}", H::app_version(),); @@ -556,8 +556,8 @@ pub async fn main() -> crate::Result<()> { Ok(()) } -fn show_list_endpoints(ctx: &AppContext) { - let mut routes = list_endpoints::(ctx); +fn show_list_endpoints>(ctx: &AC) { + let mut routes = list_endpoints::(ctx); routes.sort_by(|a, b| a.uri.cmp(&b.uri)); for router in routes { println!("{router}"); diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 6d75d64e0..063688e62 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -29,7 +29,7 @@ use super::{ routes::Routes, }; use crate::{ - app::AppContext, + app::AppContextTrait, config::{self, FallbackConfig}, controller::middleware::{ etag::EtagLayer, @@ -50,20 +50,20 @@ lazy_static! { /// Represents the routes of the application. #[derive(Clone)] -pub struct AppRoutes { +pub struct AppRoutes { prefix: Option, - routes: Vec, + routes: Vec>, #[cfg(feature = "channels")] channels: Option, } -pub struct ListRoutes { +pub struct ListRoutes { pub uri: String, pub actions: Vec, - pub method: axum::routing::MethodRouter, + pub method: axum::routing::MethodRouter, } -impl fmt::Display for ListRoutes { +impl fmt::Display for ListRoutes { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let actions_str = self .actions @@ -76,7 +76,7 @@ impl fmt::Display for ListRoutes { } } -impl AppRoutes { +impl AppRoutes { /// Create a new instance with the default routes. #[must_use] pub fn with_default_routes() -> Self { @@ -99,7 +99,7 @@ impl AppRoutes { } #[must_use] - pub fn collect(&self) -> Vec { + pub fn collect(&self) -> Vec> { let base_url_prefix = self .get_prefix() // add a leading slash forcefully. Axum routes must start with a leading slash. @@ -146,7 +146,7 @@ impl AppRoutes { /// Get the routes. #[must_use] - pub fn get_routes(&self) -> &[Routes] { + pub fn get_routes(&self) -> &[Routes] { self.routes.as_ref() } @@ -158,9 +158,9 @@ impl AppRoutes { /// In the following example you are adding api as a prefix for all routes /// /// ```rust - /// use loco_rs::controller::AppRoutes; + /// use loco_rs::{app::AppContext, controller::AppRoutes}; /// - /// AppRoutes::with_default_routes().prefix("api"); + /// AppRoutes::::with_default_routes().prefix("api"); /// ``` #[must_use] pub fn prefix(mut self, prefix: &str) -> Self { @@ -170,14 +170,14 @@ impl AppRoutes { /// Add a single route. #[must_use] - pub fn add_route(mut self, route: Routes) -> Self { + pub fn add_route(mut self, route: Routes) -> Self { self.routes.push(route); self } /// Add multiple routes. #[must_use] - pub fn add_routes(mut self, mounts: Vec) -> Self { + pub fn add_routes(mut self, mounts: Vec>) -> Self { for mount in mounts { self.routes.push(mount); } @@ -198,7 +198,7 @@ impl AppRoutes { /// Return an [`Result`] when could not convert the router setup to /// [`axum::Router`]. #[allow(clippy::cognitive_complexity)] - pub fn to_router(&self, ctx: AppContext, mut app: AXRouter) -> Result { + pub fn to_router(&self, ctx: AC, mut app: AXRouter) -> Result { // // IMPORTANT: middleware ordering in this function is opposite to what you // intuitively may think. when using `app.layer` to add individual middleware, @@ -249,72 +249,72 @@ impl AppRoutes { } } - if let Some(catch_panic) = &ctx.config.server.middlewares.catch_panic { + if let Some(catch_panic) = &ctx.config().server.middlewares.catch_panic { if catch_panic.enable { app = Self::add_catch_panic(app); } } - if let Some(etag) = &ctx.config.server.middlewares.etag { + if let Some(etag) = &ctx.config().server.middlewares.etag { if etag.enable { app = Self::add_etag_middleware(app); } } - if let Some(remote_ip) = &ctx.config.server.middlewares.remote_ip { + if let Some(remote_ip) = &ctx.config().server.middlewares.remote_ip { if remote_ip.enable { app = Self::add_remote_ip_middleware(app, remote_ip)?; } } - if let Some(compression) = &ctx.config.server.middlewares.compression { + if let Some(compression) = &ctx.config().server.middlewares.compression { if compression.enable { app = Self::add_compression_middleware(app); } } - if let Some(timeout_request) = &ctx.config.server.middlewares.timeout_request { + if let Some(timeout_request) = &ctx.config().server.middlewares.timeout_request { if timeout_request.enable { app = Self::add_timeout_middleware(app, timeout_request); } } - if let Some(cors) = &ctx.config.server.middlewares.cors { + if let Some(cors) = &ctx.config().server.middlewares.cors { if cors.enable { app = app.layer(cors_middleware(cors)?); } } - if let Some(limit) = &ctx.config.server.middlewares.limit_payload { + if let Some(limit) = &ctx.config().server.middlewares.limit_payload { if limit.enable { app = Self::add_limit_payload_middleware(app, limit)?; } } - if let Some(logger) = &ctx.config.server.middlewares.logger { + if let Some(logger) = &ctx.config().server.middlewares.logger { if logger.enable { - app = Self::add_logger_middleware(app, &ctx.environment); + app = Self::add_logger_middleware(app, ctx.environment()); } } - if let Some(static_assets) = &ctx.config.server.middlewares.static_assets { + if let Some(static_assets) = &ctx.config().server.middlewares.static_assets { if static_assets.enable { app = Self::add_static_asset_middleware(app, static_assets)?; } } - if let Some(secure_headers) = &ctx.config.server.middlewares.secure_headers { + if let Some(secure_headers) = &ctx.config().server.middlewares.secure_headers { app = app.layer(SecureHeaders::new(secure_headers)?); tracing::info!("[Middleware] +secure headers"); } - if let Some(fallback) = &ctx.config.server.middlewares.fallback { + if let Some(fallback) = &ctx.config().server.middlewares.fallback { if fallback.enable { app = Self::add_fallback(app, fallback)?; } } - app = Self::add_powered_by_header(app, &ctx.config.server); + app = Self::add_powered_by_header(app, &ctx.config().server); app = Self::add_request_id_middleware(app); @@ -322,10 +322,7 @@ impl AppRoutes { Ok(router) } - fn add_fallback( - app: AXRouter, - fallback: &FallbackConfig, - ) -> Result> { + fn add_fallback(app: AXRouter, fallback: &FallbackConfig) -> Result> { let app = if let Some(path) = &fallback.file { app.fallback_service(ServeFile::new(path)) } else if let Some(not_found) = &fallback.not_found { @@ -352,16 +349,16 @@ impl AppRoutes { Ok(app) } - fn add_request_id_middleware(app: AXRouter) -> AXRouter { + fn add_request_id_middleware(app: AXRouter) -> AXRouter { let app = app.layer(axum::middleware::from_fn(request_id_middleware)); tracing::info!("[Middleware] +request id"); app } fn add_static_asset_middleware( - app: AXRouter, + app: AXRouter, config: &config::StaticAssetsMiddleware, - ) -> Result> { + ) -> Result> { if config.must_exist && (!PathBuf::from(&config.folder.path).exists() || !PathBuf::from(&config.fallback).exists()) @@ -386,35 +383,35 @@ impl AppRoutes { )) } - fn add_compression_middleware(app: AXRouter) -> AXRouter { + fn add_compression_middleware(app: AXRouter) -> AXRouter { let app = app.layer(CompressionLayer::new()); tracing::info!("[Middleware] +compression"); app } - fn add_etag_middleware(app: AXRouter) -> AXRouter { + fn add_etag_middleware(app: AXRouter) -> AXRouter { let app = app.layer(EtagLayer::new()); tracing::info!("[Middleware] +etag"); app } fn add_remote_ip_middleware( - app: AXRouter, + app: AXRouter, config: &RemoteIPConfig, - ) -> Result> { + ) -> Result> { let app = app.layer(RemoteIPLayer::new(config)?); tracing::info!("[Middleware] +remote IP"); Ok(app) } - fn add_catch_panic(app: AXRouter) -> AXRouter { + fn add_catch_panic(app: AXRouter) -> AXRouter { app.layer(CatchPanicLayer::custom(handle_panic)) } fn add_limit_payload_middleware( - app: AXRouter, + app: AXRouter, limit: &config::LimitPayloadMiddleware, - ) -> Result> { + ) -> Result> { let app = app.layer(axum::extract::DefaultBodyLimit::max( byte_unit::Byte::from_str(&limit.body_limit) .map_err(Box::from)? @@ -424,10 +421,7 @@ impl AppRoutes { Ok(app) } - fn add_logger_middleware( - app: AXRouter, - environment: &Environment, - ) -> AXRouter { + fn add_logger_middleware(app: AXRouter, environment: &Environment) -> AXRouter { let app = app .layer( TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| { @@ -464,19 +458,16 @@ impl AppRoutes { } fn add_timeout_middleware( - app: AXRouter, + app: AXRouter, config: &config::TimeoutRequestMiddleware, - ) -> AXRouter { + ) -> AXRouter { let app = app.layer(TimeoutLayer::new(Duration::from_millis(config.timeout))); tracing::info!("[Middleware] +timeout"); app } - fn add_powered_by_header( - app: AXRouter, - config: &config::Server, - ) -> AXRouter { + fn add_powered_by_header(app: AXRouter, config: &config::Server) -> AXRouter { let ident_value = config.ident.as_ref().map_or_else( || Some(DEFAULT_IDENT_HEADER_VALUE.clone()), |ident| { @@ -538,7 +529,7 @@ mod tests { #[test] fn can_load_app_route_from_default() { - for route in AppRoutes::with_default_routes().collect() { + for route in AppRoutes::::with_default_routes().collect() { assert_debug_snapshot!( format!("[{}]", route.uri.replace('/', "[slash]")), format!("{:?} {}", route.actions, route.uri) @@ -548,12 +539,12 @@ mod tests { #[test] fn can_load_empty_app_routes() { - assert_eq!(AppRoutes::empty().collect().len(), 0); + assert_eq!(AppRoutes::::empty().collect().len(), 0); } #[test] fn can_load_routes() { - let router_without_prefix = Routes::new().add("/", get(action)); + let router_without_prefix = Routes::::new().add("/", get(action)); let normalizer = Routes::new() .prefix("/normalizer") .add("no-slash", get(action)) @@ -581,7 +572,7 @@ mod tests { #[test] fn can_load_routes_with_root_prefix() { - let router_without_prefix = Routes::new() + let router_without_prefix = Routes::::new() .add("/loco", get(action)) .add("loco-rs", get(action)); diff --git a/src/controller/describe.rs b/src/controller/describe.rs index 43aec5b29..25927ee4b 100644 --- a/src/controller/describe.rs +++ b/src/controller/describe.rs @@ -2,7 +2,7 @@ use axum::{http, routing::MethodRouter}; use lazy_static::lazy_static; use regex::Regex; -use crate::app::AppContext; +use crate::app::AppContextTrait; lazy_static! { static ref DESCRIBE_METHOD_ACTION: Regex = Regex::new(r"\b(\w+):\s*BoxedHandler\b").unwrap(); @@ -13,7 +13,7 @@ lazy_static! { /// Currently axum not exposed the action type of the router. for hold extra /// information about routers we need to convert the `method` to string and /// capture the details -pub fn method_action(method: &MethodRouter) -> Vec { +pub fn method_action(method: &MethodRouter) -> Vec { let method_str = format!("{method:?}"); DESCRIBE_METHOD_ACTION diff --git a/src/controller/health.rs b/src/controller/health.rs index 3829a3d4c..b044bf2e1 100644 --- a/src/controller/health.rs +++ b/src/controller/health.rs @@ -6,7 +6,7 @@ use axum::{extract::State, response::Response, routing::get}; use serde::Serialize; use super::{format, routes::Routes}; -use crate::{app::AppContext, redis, Result}; +use crate::{app::AppContextTrait, redis, Result}; /// Represents the health status of the application. #[derive(Serialize)] @@ -16,15 +16,15 @@ struct Health { /// Check the healthiness of the application bt ping to the redis and the DB to /// insure that connection -async fn health(State(ctx): State) -> Result { - let mut is_ok = match ctx.db.ping().await { +async fn health(State(ctx): State) -> Result { + let mut is_ok = match ctx.db().ping().await { Ok(()) => true, Err(error) => { tracing::error!(err.msg = %error, err.detail = ?error, "health_db_ping_error"); false } }; - if let Some(pool) = ctx.queue { + if let Some(pool) = ctx.queue() { if let Err(error) = redis::ping(&pool).await { tracing::error!(err.msg = %error, err.detail = ?error, "health_redis_ping_error"); is_ok = false; @@ -34,6 +34,6 @@ async fn health(State(ctx): State) -> Result { } /// Defines and returns the health-related routes. -pub fn routes() -> Routes { - Routes::new().add("/_health", get(health)) +pub fn routes() -> Routes { + Routes::new().add("/_health", get(health::)) } diff --git a/src/controller/mod.rs b/src/controller/mod.rs index be8e60cac..9e7713dee 100644 --- a/src/controller/mod.rs +++ b/src/controller/mod.rs @@ -34,21 +34,21 @@ //! } //! //! #[async_trait] -//! impl Hooks for App { +//! impl Hooks for App { //! //! fn app_name() -> &'static str { //! env!("CARGO_CRATE_NAME") //! } //! -//! fn routes(ctx: &AppContext) -> AppRoutes { +//! fn routes(ctx: &AppContext) -> AppRoutes { //! AppRoutes::with_default_routes() //! // .add_route(controllers::notes::routes()) //! } -//! -//! async fn boot(mode: StartMode, environment: &Environment) -> Result{ -//! create_app::(mode, environment).await +//! +//! async fn boot(mode: StartMode, environment: &Environment) -> Result>{ +//! create_app::(mode, environment).await //! } -//! +//! //! #[cfg(feature = "channels")] //! /// Only when `channels` feature is enabled //! fn register_channels(_ctx: &AppContext) -> AppChannels { diff --git a/src/controller/ping.rs b/src/controller/ping.rs index 078e32cb0..bff2889c5 100644 --- a/src/controller/ping.rs +++ b/src/controller/ping.rs @@ -6,7 +6,7 @@ use axum::{response::Response, routing::get}; use serde::Serialize; use super::{format, routes::Routes}; -use crate::Result; +use crate::{app::AppContextTrait, Result}; /// Represents the health status of the application. #[derive(Serialize)] @@ -20,6 +20,6 @@ async fn ping() -> Result { } /// Defines and returns the health-related routes. -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new().add("/_ping", get(ping)) } diff --git a/src/controller/routes.rs b/src/controller/routes.rs index 32da70207..d58e02144 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -4,22 +4,22 @@ use axum::{extract::Request, response::IntoResponse, routing::Route}; use tower::{Layer, Service}; use super::describe; -use crate::app::AppContext; +use crate::app::AppContextTrait; #[derive(Clone, Default)] -pub struct Routes { +pub struct Routes { pub prefix: Option, - pub handlers: Vec, + pub handlers: Vec>, // pub version: Option, } #[derive(Clone, Default)] -pub struct Handler { +pub struct Handler { pub uri: String, - pub method: axum::routing::MethodRouter, + pub method: axum::routing::MethodRouter, pub actions: Vec, } -impl Routes { +impl Routes { /// Creates a new [`Routes`] instance with default settings. #[must_use] pub fn new() -> Self { @@ -46,8 +46,8 @@ impl Routes { /// async fn ping() -> Result { /// format::json(Health { ok: true }) /// } - /// Routes::at("status").add("/_ping", get(ping)); - /// + /// Routes::::at("status").add("/_ping", get(ping)); + /// /// ```` #[must_use] pub fn at(prefix: &str) -> Self { @@ -75,10 +75,10 @@ impl Routes { /// async fn ping() -> Result { /// format::json(Health { ok: true }) /// } - /// Routes::new().add("/_ping", get(ping)); + /// Routes::::new().add("/_ping", get(ping)); /// ```` #[must_use] - pub fn add(mut self, uri: &str, method: axum::routing::MethodRouter) -> Self { + pub fn add(mut self, uri: &str, method: axum::routing::MethodRouter) -> Self { describe::method_action(&method); self.handlers.push(Handler { uri: uri.to_owned(), @@ -108,7 +108,7 @@ impl Routes { /// async fn ping() -> Result { /// format::json(Health { ok: true }) /// } - /// Routes::new().prefix("status").add("/_ping", get(ping)); + /// Routes::::new().prefix("status").add("/_ping", get(ping)); /// ```` #[must_use] pub fn prefix(mut self, uri: &str) -> Self { @@ -130,7 +130,7 @@ impl Routes { /// async fn ping() -> Result { /// format::json("Ok") /// } - /// Routes::new().prefix("status").add("/_ping", get(ping)).layer(TimeoutLayer::new(std::time::Duration::from_secs(5))); + /// Routes::::new().prefix("status").add("/_ping", get(ping)).layer(TimeoutLayer::new(std::time::Duration::from_secs(5))); /// ``` #[allow(clippy::needless_pass_by_value)] #[must_use] diff --git a/src/db.rs b/src/db.rs index c204b16fb..fde75c2dc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -18,7 +18,7 @@ use tracing::info; use super::Result as AppResult; use crate::{ - app::{AppContext, Hooks}, + app::{AppContextTrait, Hooks}, config, doctor, errors::Error, }; @@ -102,7 +102,7 @@ pub async fn verify_access(db: &DatabaseConnection) -> AppResult<()> { /// an `AppResult`, which is an alias for `Result<(), AppError>`. It may /// return an `AppError` variant representing different database operation /// failures. -pub async fn converge( +pub async fn converge, M: MigratorTrait>( db: &DatabaseConnection, config: &config::Database, ) -> AppResult<()> { @@ -251,9 +251,9 @@ where /// # Errors /// /// Returns a [`AppResult`] if an error occurs during generate model entity. -pub async fn entities(ctx: &AppContext) -> AppResult { +pub async fn entities(ctx: &AC) -> AppResult { doctor::check_seaorm_cli().to_result()?; - doctor::check_db(&ctx.config.database).await.to_result()?; + doctor::check_db(&ctx.config().database).await.to_result()?; let out = cmd!( "sea-orm-cli", @@ -264,7 +264,7 @@ pub async fn entities(ctx: &AppContext) -> AppResult { "--output-dir", "src/models/_entities", "--database-url", - &ctx.config.database.uri + &ctx.config().database.uri ) .stderr_to_stdout() .run() @@ -361,7 +361,10 @@ where /// # Errors /// /// when seed process is fails -pub async fn run_app_seed(db: &DatabaseConnection, path: &Path) -> AppResult<()> { +pub async fn run_app_seed>( + db: &DatabaseConnection, + path: &Path, +) -> AppResult<()> { H::seed(db, path).await } diff --git a/src/gen/controller.rs b/src/gen/controller.rs index b2947be4f..acd4e8ccb 100644 --- a/src/gen/controller.rs +++ b/src/gen/controller.rs @@ -1,7 +1,10 @@ use rrgen::RRgen; use serde_json::json; -use crate::{app::Hooks, gen}; +use crate::{ + app::{AppContextTrait, Hooks}, + gen, +}; const API_CONTROLLER_CONTROLLER_T: &str = include_str!("templates/controller/api/controller.t"); const API_CONTROLLER_TEST_T: &str = include_str!("templates/controller/api/test.t"); @@ -15,7 +18,7 @@ const HTML_VIEW_T: &str = include_str!("templates/controller/html/view.t"); use super::collect_messages; use crate::Result; -pub fn generate( +pub fn generate>( rrgen: &RRgen, name: &str, actions: &[String], diff --git a/src/gen/mod.rs b/src/gen/mod.rs index e5a68ab44..805b7a0fc 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -14,7 +14,11 @@ mod model; mod scaffold; use std::str::FromStr; -use crate::{app::Hooks, config::Config, errors, Result}; +use crate::{ + app::{AppContextTrait, Hooks}, + config::Config, + errors, Result, +}; const CONTROLLER_T: &str = include_str!("templates/controller.t"); const CONTROLLER_TEST_T: &str = include_str!("templates/request_test.t"); @@ -178,7 +182,10 @@ pub enum Component { Deployment {}, } #[allow(clippy::too_many_lines)] -pub fn generate(component: Component, config: &Config) -> Result<()> { +pub fn generate>( + component: Component, + config: &Config, +) -> Result<()> { let rrgen = RRgen::default(); match component { #[cfg(feature = "with-db")] @@ -190,14 +197,14 @@ pub fn generate(component: Component, config: &Config) -> Result<()> { } => { println!( "{}", - model::generate::(&rrgen, &name, link, migration_only, &fields)? + model::generate::(&rrgen, &name, link, migration_only, &fields)? ); } #[cfg(feature = "with-db")] Component::Scaffold { name, fields, kind } => { println!( "{}", - scaffold::generate::(&rrgen, &name, &fields, &kind)? + scaffold::generate::(&rrgen, &name, &fields, &kind)? ); } #[cfg(feature = "with-db")] @@ -212,7 +219,7 @@ pub fn generate(component: Component, config: &Config) -> Result<()> { } => { println!( "{}", - controller::generate::(&rrgen, &name, &actions, &kind)? + controller::generate::(&rrgen, &name, &actions, &kind)? ); } Component::Task { name } => { diff --git a/src/gen/model.rs b/src/gen/model.rs index e7d6fa08f..25c485444 100644 --- a/src/gen/model.rs +++ b/src/gen/model.rs @@ -5,7 +5,11 @@ use duct::cmd; use rrgen::RRgen; use serde_json::json; -use crate::{app::Hooks, errors::Error, Result}; +use crate::{ + app::{AppContextTrait, Hooks}, + errors::Error, + Result, +}; const MODEL_T: &str = include_str!("templates/model.t"); const MODEL_TEST_T: &str = include_str!("templates/model_test.t"); @@ -17,7 +21,7 @@ use super::{collect_messages, MAPPINGS}; /// generated by the Loco app and should be given pub const IGNORE_FIELDS: &[&str] = &["created_at", "updated_at", "create_at", "update_at"]; -pub fn generate( +pub fn generate>( rrgen: &RRgen, name: &str, is_link: bool, diff --git a/src/gen/scaffold.rs b/src/gen/scaffold.rs index e1e5b5093..a034d13d5 100644 --- a/src/gen/scaffold.rs +++ b/src/gen/scaffold.rs @@ -1,7 +1,10 @@ use rrgen::RRgen; use serde_json::json; -use crate::{app::Hooks, gen}; +use crate::{ + app::{AppContextTrait, Hooks}, + gen, +}; const API_CONTROLLER_SCAFFOLD_T: &str = include_str!("templates/scaffold/api/controller.t"); const API_CONTROLLER_TEST_T: &str = include_str!("templates/scaffold/api/test.t"); @@ -25,7 +28,7 @@ const HTML_VIEW_LIST_SCAFFOLD_T: &str = include_str!("templates/scaffold/html/vi use super::{collect_messages, model, MAPPINGS}; use crate::{errors::Error, Result}; -pub fn generate( +pub fn generate>( rrgen: &RRgen, name: &str, fields: &[(String, String)], @@ -34,7 +37,7 @@ pub fn generate( // - scaffold is never a link table // - never run with migration_only, because the controllers will refer to the // models. the models only arrive after migration and entities sync. - let model_messages = model::generate::(rrgen, name, false, false, fields)?; + let model_messages = model::generate::(rrgen, name, false, false, fields)?; let mut columns = Vec::new(); for (fname, ftype) in fields { diff --git a/src/logger.rs b/src/logger.rs index 44f2d05b5..ca5b74af6 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -10,7 +10,10 @@ use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; -use crate::{app::Hooks, config}; +use crate::{ + app::{AppContextTrait, Hooks}, + config, +}; // Define an enumeration for log levels #[derive(Debug, Default, Clone, Deserialize, Serialize)] @@ -91,7 +94,7 @@ static NONBLOCKING_WORK_GUARD_KEEP: OnceLock = OnceLock::new(); /// use via PR) /// 3. regardless of (1) and (2) operators in production, or elsewhere can /// always use `RUST_LOG` to quickly diagnose a service -pub fn init(config: &config::Logger) { +pub fn init>(config: &config::Logger) { let mut layers: Vec + Sync + Send>> = Vec::new(); if let Some(file_appender_config) = config.file_appender.as_ref() { @@ -153,7 +156,7 @@ pub fn init(config: &config::Logger) { } if !layers.is_empty() { - let env_filter = init_env_filter::(config.override_filter.as_ref(), &config.level); + let env_filter = init_env_filter::(config.override_filter.as_ref(), &config.level); tracing_subscriber::registry() .with(layers) .with(env_filter) @@ -161,7 +164,10 @@ pub fn init(config: &config::Logger) { } } -fn init_env_filter(override_filter: Option<&String>, level: &LogLevel) -> EnvFilter { +fn init_env_filter>( + override_filter: Option<&String>, + level: &LogLevel, +) -> EnvFilter { EnvFilter::try_from_default_env() .or_else(|_| { // user wanted a specific filter, don't care about our internal whitelist diff --git a/src/mailer/mod.rs b/src/mailer/mod.rs index ab2244652..83e5a8314 100644 --- a/src/mailer/mod.rs +++ b/src/mailer/mod.rs @@ -11,6 +11,8 @@ use include_dir::Dir; use serde::{Deserialize, Serialize}; use sidekiq::Worker; +use crate::app::AppContextTrait; + use self::template::Template; use super::{app::AppContext, worker::AppWorker, Result}; @@ -108,20 +110,20 @@ pub trait Mailer { /// The [`MailerWorker`] struct represents a worker responsible for asynchronous /// email processing. #[allow(clippy::module_name_repetitions)] -pub struct MailerWorker { - pub ctx: AppContext, +pub struct MailerWorker { + pub ctx: AC, } /// Implementation of the `AppWorker` trait for the [`MailerWorker`]. -impl AppWorker for MailerWorker { - fn build(ctx: &AppContext) -> Self { +impl AppWorker for MailerWorker { + fn build(ctx: &AC) -> Self { Self { ctx: ctx.clone() } } } /// Implementation of the [`Worker`] trait for the [`MailerWorker`]. #[async_trait] -impl Worker for MailerWorker { +impl Worker for MailerWorker { /// Returns options for the mailer worker, specifying the queue to process. fn opts() -> sidekiq::WorkerOpts { sidekiq::WorkerOpts::new().queue("mailer") @@ -130,7 +132,7 @@ impl Worker for MailerWorker { /// Performs the email sending operation using the provided [`AppContext`] /// and email details. async fn perform(&self, email: Email) -> sidekiq::Result<()> { - if let Some(mailer) = &self.ctx.mailer { + if let Some(mailer) = self.ctx.mailer() { Ok(mailer.mail(&email).await.map_err(Box::from)?) } else { Err(sidekiq::Error::Message( diff --git a/src/scheduler.rs b/src/scheduler.rs index f98909f3c..7db31a3e0 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -10,7 +10,11 @@ use std::{ time::Instant, }; -use crate::{app::Hooks, environment::Environment, task::Tasks}; +use crate::{ + app::{AppContextTrait, Hooks}, + environment::Environment, + task::Tasks, +}; use tokio_cron_scheduler::{JobScheduler, JobSchedulerError}; @@ -203,7 +207,10 @@ impl Scheduler { /// # Errors /// /// When could not parse the given file content into a [`Config`] struct. - pub fn from_config(config: &Path, environment: &Environment) -> Result { + pub fn from_config>( + config: &Path, + environment: &Environment, + ) -> Result { let config_str = std::fs::read_to_string(config).map_err(|error| Error::ConfigNotFound { path: config.to_path_buf(), @@ -213,7 +220,7 @@ impl Scheduler { let config: Config = serde_yaml::from_str(&config_str) .map_err(|error| Error::InvalidConfigSchema { error })?; - Self::new::(&config, environment) + Self::new::(&config, environment) } /// Creates a new scheduler instance from the provided configuration data. @@ -224,7 +231,10 @@ impl Scheduler { /// # Errors /// /// When there is not job in the given config - pub fn new(data: &Config, environment: &Environment) -> Result { + pub fn new>( + data: &Config, + environment: &Environment, + ) -> Result { let mut tasks = Tasks::default(); H::register_tasks(&mut tasks); @@ -352,7 +362,7 @@ impl Scheduler { mod tests { use super::*; - use crate::tests_cfg; + use crate::{app::AppContext, tests_cfg}; use insta::assert_debug_snapshot; use rstest::rstest; @@ -365,7 +375,10 @@ mod tests { .join("scheduler") .join("scheduler.yaml"); - Scheduler::from_config::(&scheduler_config_path, &Environment::Development) + Scheduler::from_config::( + &scheduler_config_path, + &Environment::Development, + ) } #[test] @@ -382,7 +395,7 @@ mod tests { #[tokio::test] pub async fn can_load_from_env_config() { let app_context = tests_cfg::app::get_app_context().await; - let scheduler = Scheduler::new::( + let scheduler = Scheduler::new::( &app_context.config.scheduler.unwrap(), &Environment::Development, ); diff --git a/src/task.rs b/src/task.rs index 3da449ee3..faecb736d 100644 --- a/src/task.rs +++ b/src/task.rs @@ -6,7 +6,11 @@ use std::collections::BTreeMap; use async_trait::async_trait; -use crate::{app::AppContext, errors::Error, Result}; +use crate::{ + app::{AppContextTrait, Context}, + errors::Error, + Result, +}; /// Struct representing a collection of task arguments. #[derive(Default, Debug)] @@ -75,7 +79,7 @@ pub trait Task: Send + Sync { /// Get information about the task. fn task(&self) -> TaskInfo; /// Execute the task with the provided application context and variables. - async fn run(&self, app_context: &AppContext, vars: &Vars) -> Result<()>; + async fn run(&self, app_context: &dyn Context, vars: &Vars) -> Result<()>; } /// Managing and running tasks. @@ -106,7 +110,12 @@ impl Tasks { /// /// Returns a [`Result`] if an task finished with error. mostly if the given /// task is not found or an error to run the task.s - pub async fn run(&self, app_context: &AppContext, task: &str, vars: &Vars) -> Result<()> { + pub async fn run( + &self, + app_context: &AC, + task: &str, + vars: &Vars, + ) -> Result<()> { let task = self .registry .get(task) diff --git a/src/testing.rs b/src/testing.rs index b2940012c..0a28aeb19 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -10,7 +10,7 @@ use lazy_static::lazy_static; use sea_orm::DatabaseConnection; use crate::{ - app::{AppContext, Hooks}, + app::{AppContext, AppContextTrait, Hooks}, boot::{self, BootResult}, environment::Environment, Result, @@ -108,19 +108,19 @@ pub fn cleanup_email() -> Vec<(&'static str, &'static str)> { /// application context. /// /// ```rust,ignore -/// use myapp::app::App; +/// use myapp::app::{App, AppContext}; /// use loco_rs::testing; /// use migration::Migrator; /// /// #[tokio::test] /// async fn test_create_user() { -/// let boot = testing::boot_test::().await; +/// let boot = testing::boot_test::().await; /// /// /// ..... /// assert!(false) /// } /// ``` -pub async fn boot_test() -> Result { +pub async fn boot_test>() -> Result> { H::boot(boot::StartMode::ServerOnly, &Environment::Test).await } @@ -150,7 +150,7 @@ pub async fn boot_test() -> Result { /// assert!(false) /// } /// ``` -pub async fn seed(db: &DatabaseConnection) -> Result<()> { +pub async fn seed>(db: &DatabaseConnection) -> Result<()> { let path = std::path::Path::new("src/fixtures"); H::seed(db, path).await } @@ -188,12 +188,12 @@ pub async fn seed(db: &DatabaseConnection) -> Result<()> { /// } /// ``` #[allow(clippy::future_not_send)] -pub async fn request(callback: F) +pub async fn request, F, Fut>(callback: F) where - F: FnOnce(TestServer, AppContext) -> Fut, + F: FnOnce(TestServer, AC) -> Fut, Fut: std::future::Future, { - let boot = boot_test::().await.unwrap(); + let boot = boot_test::().await.unwrap(); let config = TestServerConfig::builder() .default_content_type("application/json") diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index 8be43641a..77e66be42 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -81,7 +81,7 @@ impl MigratorTrait for Migrator { pub struct AppHook; #[async_trait] -impl Hooks for AppHook { +impl Hooks for AppHook { fn app_version() -> String { "test".to_string() } @@ -94,12 +94,12 @@ impl Hooks for AppHook { Ok(vec![]) } - fn routes(_ctx: &AppContext) -> AppRoutes { + fn routes(_ctx: &AppContext) -> AppRoutes { AppRoutes::with_default_routes() } - async fn boot(mode: StartMode, environment: &Environment) -> Result { - create_app::(mode, environment).await + async fn boot(mode: StartMode, environment: &Environment) -> Result> { + create_app::(mode, environment).await } fn connect_workers<'a>(_p: &'a mut Processor, _ctx: &'a AppContext) {} diff --git a/src/tests_cfg/task.rs b/src/tests_cfg/task.rs index 03d05aa61..667a46416 100644 --- a/src/tests_cfg/task.rs +++ b/src/tests_cfg/task.rs @@ -1,4 +1,4 @@ -use crate::prelude::*; +use crate::{app::Context, prelude::*}; pub struct Foo; #[async_trait] @@ -9,7 +9,7 @@ impl Task for Foo { detail: "run foo task".to_string(), } } - async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> { + async fn run(&self, _app_context: &dyn Context, _vars: &task::Vars) -> Result<()> { println!("Foo task executed!!!"); Ok(()) } @@ -24,7 +24,7 @@ impl Task for ParseArgs { detail: "Validate the paring args".to_string(), } } - async fn run(&self, _app_context: &AppContext, vars: &task::Vars) -> Result<()> { + async fn run(&self, _app_context: &dyn Context, vars: &task::Vars) -> Result<()> { let refresh = vars.cli_arg("test").is_ok_and(|test| test == "true"); let app = vars diff --git a/src/worker.rs b/src/worker.rs index 14629a963..1c191acdf 100644 --- a/src/worker.rs +++ b/src/worker.rs @@ -3,7 +3,7 @@ pub use bb8::Pool; pub use sidekiq::{Processor, RedisConnectionManager, Result, Worker}; use tracing::error; -use super::{app::AppContext, config::WorkerMode}; +use super::{app::AppContextTrait, config::WorkerMode}; pub const DEFAULT_QUEUES: &[&str] = &["default", "mailer"]; pub fn get_queues(config_queues: &Option>) -> Vec { @@ -26,16 +26,17 @@ pub fn get_queues(config_queues: &Option>) -> Vec { #[async_trait] #[allow(clippy::module_name_repetitions)] -pub trait AppWorker: Worker +pub trait AppWorker: Worker where Self: Sized, T: Send + Sync + serde::Serialize + 'static, + AC: AppContextTrait, { - fn build(ctx: &AppContext) -> Self; - async fn perform_later(ctx: &AppContext, args: T) -> Result<()> { - match &ctx.config.workers.mode { + fn build(ctx: &AC) -> Self; + async fn perform_later(ctx: &AC, args: T) -> Result<()> { + match &ctx.config().workers.mode { WorkerMode::BackgroundQueue => { - if let Some(queue) = &ctx.queue { + if let Some(queue) = ctx.queue() { Self::perform_async(queue, args).await.unwrap(); } else { error!( From 53711eee1fb540044da34478b6d391790be0803e Mon Sep 17 00:00:00 2001 From: Mason Stallmo Date: Sat, 21 Sep 2024 00:33:03 -0700 Subject: [PATCH 2/4] Update demo application to work with `AppContextTrait` --- examples/demo/Cargo.lock | 2 +- examples/demo/examples/playground.rs | 4 +- examples/demo/examples/start.rs | 6 ++- examples/demo/examples/task.rs | 5 +- examples/demo/examples/workers.rs | 6 ++- examples/demo/src/app.rs | 8 ++-- examples/demo/src/bin/main.rs | 4 +- examples/demo/src/bin/tool.rs | 4 +- examples/demo/src/controllers/auth.rs | 2 +- examples/demo/src/controllers/cache.rs | 2 +- examples/demo/src/controllers/mylayer.rs | 2 +- examples/demo/src/controllers/mysession.rs | 2 +- examples/demo/src/controllers/notes.rs | 2 +- examples/demo/src/controllers/responses.rs | 2 +- examples/demo/src/controllers/upload.rs | 2 +- examples/demo/src/controllers/user.rs | 2 +- examples/demo/src/controllers/view_engine.rs | 2 +- .../demo/src/initializers/axum_session.rs | 4 +- .../src/initializers/hello_view_engine.rs | 4 +- examples/demo/src/initializers/view_engine.rs | 4 +- examples/demo/src/tasks/foo.rs | 4 +- examples/demo/src/tasks/seed.rs | 8 ++-- examples/demo/src/tasks/user_report.rs | 6 +-- examples/demo/src/workers/downloader.rs | 2 +- examples/demo/tests/models/roles.rs | 12 ++--- examples/demo/tests/models/users.rs | 48 ++++++++++++------- examples/demo/tests/models/users_roles.rs | 6 +-- examples/demo/tests/requests/auth.rs | 10 ++-- examples/demo/tests/requests/cache.rs | 8 ++-- examples/demo/tests/requests/mylayer.rs | 18 +++---- examples/demo/tests/requests/notes.rs | 16 +++---- examples/demo/tests/requests/ping.rs | 4 +- examples/demo/tests/requests/responses.rs | 4 +- examples/demo/tests/requests/upload.rs | 4 +- examples/demo/tests/requests/user.rs | 10 ++-- examples/demo/tests/requests/view_engine.rs | 4 +- examples/demo/tests/tasks/foo.rs | 6 +-- examples/demo/tests/tasks/seed.rs | 6 +-- 38 files changed, 132 insertions(+), 113 deletions(-) diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 847488ded..83ececcad 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -2998,7 +2998,7 @@ dependencies = [ [[package]] name = "loco-rs" -version = "0.8.1" +version = "0.9.0" dependencies = [ "argon2", "async-trait", diff --git a/examples/demo/examples/playground.rs b/examples/demo/examples/playground.rs index c9501a325..7defb1f5e 100644 --- a/examples/demo/examples/playground.rs +++ b/examples/demo/examples/playground.rs @@ -1,10 +1,10 @@ use demo_app::app::App; #[allow(unused_imports)] -use loco_rs::{cli::playground, prelude::*}; +use loco_rs::{app::AppContext, cli::playground, prelude::*}; #[tokio::main] async fn main() -> loco_rs::Result<()> { - let _ctx = playground::().await?; + let _ctx = playground::().await?; // let active_model: articles::ActiveModel = ActiveModel { // title: Set(Some("how to build apps in 3 steps".to_string())), diff --git a/examples/demo/examples/start.rs b/examples/demo/examples/start.rs index fc5a0f250..b1dc0d0e2 100644 --- a/examples/demo/examples/start.rs +++ b/examples/demo/examples/start.rs @@ -1,5 +1,6 @@ use demo_app::app::App; use loco_rs::{ + app::AppContext, boot::{create_app, start, ServeParams, StartMode}, environment::{resolve_from_env, Environment}, }; @@ -9,11 +10,12 @@ use migration::Migrator; async fn main() -> loco_rs::Result<()> { let environment: Environment = resolve_from_env().into(); - let boot_result = create_app::(StartMode::ServerAndWorker, &environment).await?; + let boot_result = + create_app::(StartMode::ServerAndWorker, &environment).await?; let serve_params = ServeParams { port: boot_result.app_context.config.server.port, binding: boot_result.app_context.config.server.binding.to_string(), }; - start::(boot_result, serve_params).await?; + start::(boot_result, serve_params).await?; Ok(()) } diff --git a/examples/demo/examples/task.rs b/examples/demo/examples/task.rs index eb2f271f5..4704d5628 100644 --- a/examples/demo/examples/task.rs +++ b/examples/demo/examples/task.rs @@ -2,6 +2,7 @@ use std::env; use demo_app::app::App; use loco_rs::{ + app::AppContext, boot::{create_context, run_task}, environment::{resolve_from_env, Environment}, task, @@ -13,8 +14,8 @@ async fn main() -> loco_rs::Result<()> { let args = env::args().collect::>(); let cmd = args.get(1); - let app_context = create_context::(&environment).await?; - run_task::(&app_context, cmd, &task::Vars::default()).await?; + let app_context = create_context::(&environment).await?; + run_task::(&app_context, cmd, &task::Vars::default()).await?; Ok(()) } diff --git a/examples/demo/examples/workers.rs b/examples/demo/examples/workers.rs index 23ccdb78c..2aa2ebdc3 100644 --- a/examples/demo/examples/workers.rs +++ b/examples/demo/examples/workers.rs @@ -1,5 +1,6 @@ use demo_app::app::App; use loco_rs::{ + app::AppContext, boot::{create_app, start, ServeParams, StartMode}, environment::{resolve_from_env, Environment}, }; @@ -9,11 +10,12 @@ use migration::Migrator; async fn main() -> loco_rs::Result<()> { let environment: Environment = resolve_from_env().into(); - let boot_result = create_app::(StartMode::WorkerOnly, &environment).await?; + let boot_result = + create_app::(StartMode::WorkerOnly, &environment).await?; let serve_params = ServeParams { port: boot_result.app_context.config.server.port, binding: boot_result.app_context.config.server.binding.to_string(), }; - start::(boot_result, serve_params).await?; + start::(boot_result, serve_params).await?; Ok(()) } diff --git a/examples/demo/src/app.rs b/examples/demo/src/app.rs index 414e6310d..ca68bf769 100644 --- a/examples/demo/src/app.rs +++ b/examples/demo/src/app.rs @@ -27,7 +27,7 @@ use crate::{ pub struct App; #[async_trait] -impl Hooks for App { +impl Hooks for App { fn app_version() -> String { format!( "{} ({})", @@ -61,7 +61,7 @@ impl Hooks for App { } // - fn routes(ctx: &AppContext) -> AppRoutes { + fn routes(ctx: &AppContext) -> AppRoutes { AppRoutes::with_default_routes() .add_route( controllers::mylayer::routes(ctx.clone()) @@ -77,8 +77,8 @@ impl Hooks for App { .add_route(controllers::cache::routes()) } - async fn boot(mode: StartMode, environment: &Environment) -> Result { - create_app::(mode, environment).await + async fn boot(mode: StartMode, environment: &Environment) -> Result> { + create_app::(mode, environment).await } async fn after_context(ctx: AppContext) -> Result { diff --git a/examples/demo/src/bin/main.rs b/examples/demo/src/bin/main.rs index 8eb2a0306..41fb02ea2 100644 --- a/examples/demo/src/bin/main.rs +++ b/examples/demo/src/bin/main.rs @@ -1,8 +1,8 @@ use demo_app::app::App; -use loco_rs::cli; +use loco_rs::{app::AppContext, cli}; use migration::Migrator; #[tokio::main] async fn main() -> loco_rs::Result<()> { - cli::main::().await + cli::main::().await } diff --git a/examples/demo/src/bin/tool.rs b/examples/demo/src/bin/tool.rs index 8eb2a0306..41fb02ea2 100644 --- a/examples/demo/src/bin/tool.rs +++ b/examples/demo/src/bin/tool.rs @@ -1,8 +1,8 @@ use demo_app::app::App; -use loco_rs::cli; +use loco_rs::{app::AppContext, cli}; use migration::Migrator; #[tokio::main] async fn main() -> loco_rs::Result<()> { - cli::main::().await + cli::main::().await } diff --git a/examples/demo/src/controllers/auth.rs b/examples/demo/src/controllers/auth.rs index 3c2cbb0ec..6abdc0c13 100644 --- a/examples/demo/src/controllers/auth.rs +++ b/examples/demo/src/controllers/auth.rs @@ -136,7 +136,7 @@ async fn login(State(ctx): State, Json(params): Json) - format::json(UserSession::new(&user, &token)) } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("auth") .add("/register", post(register)) diff --git a/examples/demo/src/controllers/cache.rs b/examples/demo/src/controllers/cache.rs index 5edee1b8b..dd43ff339 100644 --- a/examples/demo/src/controllers/cache.rs +++ b/examples/demo/src/controllers/cache.rs @@ -32,7 +32,7 @@ async fn get_or_insert(State(ctx): State) -> Result { } } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("cache") .add("/", get(get_cache)) diff --git a/examples/demo/src/controllers/mylayer.rs b/examples/demo/src/controllers/mylayer.rs index b9428a4dc..c93a472a6 100644 --- a/examples/demo/src/controllers/mylayer.rs +++ b/examples/demo/src/controllers/mylayer.rs @@ -15,7 +15,7 @@ async fn echo() -> Result { format::json("Hello, World!") } -pub fn routes(ctx: AppContext) -> Routes { +pub fn routes(ctx: AppContext) -> Routes { Routes::new() .prefix("mylayer") // Only users with the RoleName::Admin can access this route diff --git a/examples/demo/src/controllers/mysession.rs b/examples/demo/src/controllers/mysession.rs index f8b948ebb..aa334ee86 100644 --- a/examples/demo/src/controllers/mysession.rs +++ b/examples/demo/src/controllers/mysession.rs @@ -11,6 +11,6 @@ pub async fn get_session(_session: Session) -> Result format::empty() } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new().prefix("mysession").add("/", get(get_session)) } diff --git a/examples/demo/src/controllers/notes.rs b/examples/demo/src/controllers/notes.rs index 6c5200bcc..91245353a 100644 --- a/examples/demo/src/controllers/notes.rs +++ b/examples/demo/src/controllers/notes.rs @@ -150,7 +150,7 @@ impl ListQueryParams { } } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("notes") .add("/", get(list)) diff --git a/examples/demo/src/controllers/responses.rs b/examples/demo/src/controllers/responses.rs index bb558309b..54d487ef1 100644 --- a/examples/demo/src/controllers/responses.rs +++ b/examples/demo/src/controllers/responses.rs @@ -97,7 +97,7 @@ pub async fn set_cookie() -> Result { format::render().cookies(&[cookie])?.json(()) } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("response") .add("/empty", get(empty)) diff --git a/examples/demo/src/controllers/upload.rs b/examples/demo/src/controllers/upload.rs index b04d0cd89..7fd2414bb 100644 --- a/examples/demo/src/controllers/upload.rs +++ b/examples/demo/src/controllers/upload.rs @@ -41,7 +41,7 @@ async fn upload_file(State(ctx): State, mut multipart: Multipart) -> }) } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("upload") .add("/file", post(upload_file)) diff --git a/examples/demo/src/controllers/user.rs b/examples/demo/src/controllers/user.rs index 48365d48c..afa1604fc 100644 --- a/examples/demo/src/controllers/user.rs +++ b/examples/demo/src/controllers/user.rs @@ -35,7 +35,7 @@ async fn convert_to_user( format::json(UserResponse::new(&auth.user, &roles)) } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("user") .add("/current", get(current)) diff --git a/examples/demo/src/controllers/view_engine.rs b/examples/demo/src/controllers/view_engine.rs index fc89d2b75..d73d70b2f 100644 --- a/examples/demo/src/controllers/view_engine.rs +++ b/examples/demo/src/controllers/view_engine.rs @@ -29,7 +29,7 @@ pub async fn render_simple() -> Result { format::render().template("{{name}} website", json!({"name": "Loco"})) } -pub fn routes() -> Routes { +pub fn routes() -> Routes { Routes::new() .prefix("view-engine") .add("/home", get(render_home)) diff --git a/examples/demo/src/initializers/axum_session.rs b/examples/demo/src/initializers/axum_session.rs index fb01bbee5..766b84972 100644 --- a/examples/demo/src/initializers/axum_session.rs +++ b/examples/demo/src/initializers/axum_session.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use axum::Router as AxumRouter; -use loco_rs::prelude::*; +use loco_rs::{app::Context, prelude::*}; pub struct AxumSessionInitializer; @@ -10,7 +10,7 @@ impl Initializer for AxumSessionInitializer { "axum-session".to_string() } - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, _ctx: &dyn Context) -> Result { let session_config = axum_session::SessionConfig::default().with_table_name("sessions_table"); diff --git a/examples/demo/src/initializers/hello_view_engine.rs b/examples/demo/src/initializers/hello_view_engine.rs index e160b7520..e9e01ad71 100644 --- a/examples/demo/src/initializers/hello_view_engine.rs +++ b/examples/demo/src/initializers/hello_view_engine.rs @@ -1,6 +1,6 @@ use axum::{async_trait, Extension, Router as AxumRouter}; use loco_rs::{ - app::{AppContext, Initializer}, + app::{AppContext, Context, Initializer}, controller::views::{ViewEngine, ViewRenderer}, Result, }; @@ -21,7 +21,7 @@ impl Initializer for HelloViewEngineInitializer { "custom-view-engine".to_string() } - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, _ctx: &dyn Context) -> Result { Ok(router.layer(Extension(ViewEngine::from(HelloView)))) } } diff --git a/examples/demo/src/initializers/view_engine.rs b/examples/demo/src/initializers/view_engine.rs index f0a5ffba9..2a324ca22 100644 --- a/examples/demo/src/initializers/view_engine.rs +++ b/examples/demo/src/initializers/view_engine.rs @@ -1,7 +1,7 @@ use axum::{async_trait, Extension, Router as AxumRouter}; use fluent_templates::{ArcLoader, FluentLoader}; use loco_rs::{ - app::{AppContext, Initializer}, + app::{AppContext, Context, Initializer}, controller::views::{engines, ViewEngine}, Error, Result, }; @@ -17,7 +17,7 @@ impl Initializer for ViewEngineInitializer { "view-engine".to_string() } - async fn after_routes(&self, router: AxumRouter, _ctx: &AppContext) -> Result { + async fn after_routes(&self, router: AxumRouter, _ctx: &dyn Context) -> Result { let mut tera_engine = engines::TeraView::build()?; if std::path::Path::new(I18N_DIR).exists() { let arc = ArcLoader::builder(&I18N_DIR, unic_langid::langid!("en-US")) diff --git a/examples/demo/src/tasks/foo.rs b/examples/demo/src/tasks/foo.rs index 43193b49d..3d885a6df 100644 --- a/examples/demo/src/tasks/foo.rs +++ b/examples/demo/src/tasks/foo.rs @@ -1,5 +1,5 @@ // -use loco_rs::prelude::*; +use loco_rs::{app::Context, prelude::*}; pub struct Foo; #[async_trait] @@ -10,7 +10,7 @@ impl Task for Foo { detail: "run foo task".to_string(), } } - async fn run(&self, _app_context: &AppContext, _vars: &task::Vars) -> Result<()> { + async fn run(&self, _app_context: &dyn Context, _vars: &task::Vars) -> Result<()> { Ok(()) } } diff --git a/examples/demo/src/tasks/seed.rs b/examples/demo/src/tasks/seed.rs index 34227a283..64824c877 100644 --- a/examples/demo/src/tasks/seed.rs +++ b/examples/demo/src/tasks/seed.rs @@ -12,7 +12,7 @@ //! command with the `refresh:true` argument: ```sh //! cargo run task seed_data refresh:true //! ``` -use loco_rs::{db, prelude::*}; +use loco_rs::{app::Context, db, prelude::*}; use migration::Migrator; use crate::app::App; @@ -27,16 +27,16 @@ impl Task for SeedData { detail: "Task for seeding data".to_string(), } } - async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { + async fn run(&self, app_context: &dyn Context, vars: &task::Vars) -> Result<()> { let refresh = vars .cli_arg("refresh") .is_ok_and(|refresh| refresh == "true"); if refresh { - db::reset::(&app_context.db).await?; + db::reset::(app_context.db()).await?; } let path = std::path::Path::new("src/fixtures"); - db::run_app_seed::(&app_context.db, path).await?; + db::run_app_seed::(app_context.db(), path).await?; Ok(()) } } diff --git a/examples/demo/src/tasks/user_report.rs b/examples/demo/src/tasks/user_report.rs index 4440fe404..7d1cf26d3 100644 --- a/examples/demo/src/tasks/user_report.rs +++ b/examples/demo/src/tasks/user_report.rs @@ -1,4 +1,4 @@ -use loco_rs::prelude::*; +use loco_rs::{app::Context, prelude::*}; use crate::models::_entities::users; @@ -11,8 +11,8 @@ impl Task for UserReport { detail: "output a user report".to_string(), } } - async fn run(&self, app_context: &AppContext, vars: &task::Vars) -> Result<()> { - let users = users::Entity::find().all(&app_context.db).await?; + async fn run(&self, app_context: &dyn Context, vars: &task::Vars) -> Result<()> { + let users = users::Entity::find().all(app_context.db()).await?; println!("args: {vars:?}"); println!("!!! user_report: listing users !!!"); println!("------------------------"); diff --git a/examples/demo/src/workers/downloader.rs b/examples/demo/src/workers/downloader.rs index 42c0bd7a4..2462859b6 100644 --- a/examples/demo/src/workers/downloader.rs +++ b/examples/demo/src/workers/downloader.rs @@ -15,7 +15,7 @@ pub struct DownloadWorkerArgs { pub user_guid: String, } -impl worker::AppWorker for DownloadWorker { +impl worker::AppWorker for DownloadWorker { fn build(ctx: &AppContext) -> Self { Self { ctx: ctx.clone() } } diff --git a/examples/demo/tests/models/roles.rs b/examples/demo/tests/models/roles.rs index 52f4d8af6..bc94b3a78 100644 --- a/examples/demo/tests/models/roles.rs +++ b/examples/demo/tests/models/roles.rs @@ -2,7 +2,7 @@ use demo_app::{ app::App, models::{roles, sea_orm_active_enums, users, users::RegisterParams, users_roles}, }; -use loco_rs::{prelude::*, testing}; +use loco_rs::{app::AppContext, prelude::*, testing}; use sea_orm::DatabaseConnection; use serial_test::serial; @@ -20,7 +20,7 @@ macro_rules! configure_insta { async fn can_add_user_to_admin() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { @@ -42,7 +42,7 @@ async fn can_add_user_to_admin() { async fn can_add_user_to_user() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { @@ -64,7 +64,7 @@ async fn can_add_user_to_user() { async fn can_convert_between_user_and_admin() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { @@ -94,7 +94,7 @@ async fn can_convert_between_user_and_admin() { async fn can_find_user_roles() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { @@ -131,7 +131,7 @@ async fn can_find_user_roles() { async fn cannot_find_user_before_conversation() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { diff --git a/examples/demo/tests/models/users.rs b/examples/demo/tests/models/users.rs index d4d935bd2..b90d9c615 100644 --- a/examples/demo/tests/models/users.rs +++ b/examples/demo/tests/models/users.rs @@ -3,7 +3,7 @@ use demo_app::{ models::users::{self, Model, RegisterParams}, }; use insta::assert_debug_snapshot; -use loco_rs::{model::ModelError, testing}; +use loco_rs::{app::AppContext, model::ModelError, testing}; use sea_orm::{ActiveModelTrait, ActiveValue, IntoActiveModel}; use serial_test::serial; @@ -21,7 +21,7 @@ macro_rules! configure_insta { async fn test_can_validate_model() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let res = users::ActiveModel { name: ActiveValue::set("1".to_string()), @@ -39,7 +39,7 @@ async fn test_can_validate_model() { async fn can_create_with_password() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let params = RegisterParams { email: "test@framework.com".to_string(), @@ -60,8 +60,10 @@ async fn can_create_with_password() { async fn handle_create_with_password_with_duplicate() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let new_user: Result = Model::create_with_password( &boot.app_context.db, @@ -80,8 +82,10 @@ async fn handle_create_with_password_with_duplicate() { async fn can_find_by_email() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let existing_user = Model::find_by_email(&boot.app_context.db, "user1@example.com").await; let non_existing_user_results = @@ -96,8 +100,10 @@ async fn can_find_by_email() { async fn can_find_by_pid() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let existing_user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111").await; @@ -113,8 +119,10 @@ async fn can_find_by_pid() { async fn can_verification_token() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -142,8 +150,10 @@ async fn can_verification_token() { async fn can_set_forgot_password_sent() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -171,8 +181,10 @@ async fn can_set_forgot_password_sent() { async fn can_verified() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await @@ -198,8 +210,10 @@ async fn can_verified() { async fn can_reset_password() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); - testing::seed::(&boot.app_context.db).await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); + testing::seed::(&boot.app_context.db) + .await + .unwrap(); let user = Model::find_by_pid(&boot.app_context.db, "11111111-1111-1111-1111-111111111111") .await diff --git a/examples/demo/tests/models/users_roles.rs b/examples/demo/tests/models/users_roles.rs index debacd2a8..90aff0c40 100644 --- a/examples/demo/tests/models/users_roles.rs +++ b/examples/demo/tests/models/users_roles.rs @@ -2,7 +2,7 @@ use demo_app::{ app::App, models::{roles, sea_orm_active_enums, users, users::RegisterParams, users_roles}, }; -use loco_rs::{prelude::*, testing}; +use loco_rs::{app::AppContext, prelude::*, testing}; use sea_orm::{ColumnTrait, DatabaseConnection}; use serial_test::serial; macro_rules! configure_insta { @@ -19,7 +19,7 @@ macro_rules! configure_insta { async fn can_connect_user_to_user_role() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { @@ -61,7 +61,7 @@ async fn can_connect_user_to_user_role() { async fn can_connect_user_to_admin_role() { configure_insta!(); - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); let new_user: Result = users::Model::create_with_password( &boot.app_context.db, &RegisterParams { diff --git a/examples/demo/tests/requests/auth.rs b/examples/demo/tests/requests/auth.rs index 957fb7632..b9b5f7b47 100644 --- a/examples/demo/tests/requests/auth.rs +++ b/examples/demo/tests/requests/auth.rs @@ -1,6 +1,6 @@ use demo_app::{app::App, models::users}; use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use rstest::rstest; use serial_test::serial; @@ -22,7 +22,7 @@ macro_rules! configure_insta { async fn can_register() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let email = "test@loco.com"; let payload = serde_json::json!({ "name": "loco", @@ -56,7 +56,7 @@ async fn can_register() { async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let email = "test@loco.com"; let register_payload = serde_json::json!({ "name": "loco", @@ -103,7 +103,7 @@ async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) async fn can_login_without_verify() { configure_insta!(); - testing::request::(|request, _ctx| async move { + testing::request::(|request, _ctx| async move { let email = "test@loco.com"; let password = "12341234"; let register_payload = serde_json::json!({ @@ -138,7 +138,7 @@ async fn can_login_without_verify() { async fn can_reset_password() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let login_data = prepare_data::init_user_login(&request, &ctx).await; let forgot_payload = serde_json::json!({ diff --git a/examples/demo/tests/requests/cache.rs b/examples/demo/tests/requests/cache.rs index 2321645a1..817e2fe1b 100644 --- a/examples/demo/tests/requests/cache.rs +++ b/examples/demo/tests/requests/cache.rs @@ -1,6 +1,6 @@ use demo_app::{app::App, models::users}; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use sea_orm::ModelTrait; use serial_test::serial; @@ -20,7 +20,7 @@ macro_rules! configure_insta { async fn ping() { configure_insta!(); - testing::request::(|request, _ctx| async move { + testing::request::(|request, _ctx| async move { let response = request.get("cache").await; assert_debug_snapshot!("key_not_exists", (response.text(), response.status_code())); let response = request.post("cache/insert").await; @@ -36,8 +36,8 @@ async fn ping() { async fn can_get_or_insert() { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); let response = request.get("/cache/get_or_insert").await; assert_eq!(response.text(), "user1"); diff --git a/examples/demo/tests/requests/mylayer.rs b/examples/demo/tests/requests/mylayer.rs index b78b45326..c67ca20bd 100644 --- a/examples/demo/tests/requests/mylayer.rs +++ b/examples/demo/tests/requests/mylayer.rs @@ -1,5 +1,5 @@ use demo_app::{app::App, views::user::UserResponse}; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use serial_test::serial; use crate::requests::prepare_data; @@ -15,7 +15,7 @@ macro_rules! configure_insta { #[serial] async fn cannot_get_echo_when_no_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -31,7 +31,7 @@ async fn cannot_get_echo_when_no_role_assigned() { #[serial] async fn can_get_echo_when_admin_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -54,7 +54,7 @@ async fn can_get_echo_when_admin_role_assigned() { #[serial] async fn can_get_echo_when_user_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -78,7 +78,7 @@ async fn can_get_echo_when_user_role_assigned() { #[serial] async fn cannot_get_admin_when_no_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -94,7 +94,7 @@ async fn cannot_get_admin_when_no_role() { #[serial] async fn cannot_get_admin_when_user_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -118,7 +118,7 @@ async fn cannot_get_admin_when_user_role_assigned() { #[serial] async fn can_get_admin_when_admin_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -142,7 +142,7 @@ async fn can_get_admin_when_admin_role_assigned() { #[serial] async fn cannot_get_user_when_no_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request @@ -158,7 +158,7 @@ async fn cannot_get_user_when_no_role() { #[serial] async fn can_get_user_when_user_role_assigned() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request diff --git a/examples/demo/tests/requests/notes.rs b/examples/demo/tests/requests/notes.rs index 7377771bc..8663e0cd7 100644 --- a/examples/demo/tests/requests/notes.rs +++ b/examples/demo/tests/requests/notes.rs @@ -1,6 +1,6 @@ use demo_app::{app::App, models::_entities::notes::Entity}; use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use rstest::rstest; use sea_orm::entity::prelude::*; use serde_json; @@ -27,8 +27,8 @@ macro_rules! configure_insta { async fn can_get_notes(#[case] test_name: &str, #[case] params: serde_json::Value) { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); let notes = request.get("notes").add_query_params(params).await; @@ -52,7 +52,7 @@ async fn can_get_notes(#[case] test_name: &str, #[case] params: serde_json::Valu async fn can_add_note() { configure_insta!(); - testing::request::(|request, _ctx| async move { + testing::request::(|request, _ctx| async move { let payload = serde_json::json!({ "title": "loco", "content": "loco note test", @@ -80,8 +80,8 @@ async fn can_add_note() { async fn can_get_note() { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); let add_note_request = request.get("notes/1").await; @@ -105,8 +105,8 @@ async fn can_get_note() { async fn can_delete_note() { configure_insta!(); - testing::request::(|request, ctx| async move { - testing::seed::(&ctx.db).await.unwrap(); + testing::request::(|request, ctx| async move { + testing::seed::(&ctx.db).await.unwrap(); let count_before_delete = Entity::find().all(&ctx.db).await.unwrap().len(); let delete_note_request = request.delete("notes/1").await; diff --git a/examples/demo/tests/requests/ping.rs b/examples/demo/tests/requests/ping.rs index 1be7243f0..562313f1c 100644 --- a/examples/demo/tests/requests/ping.rs +++ b/examples/demo/tests/requests/ping.rs @@ -1,6 +1,6 @@ use demo_app::app::App; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use rstest::rstest; // TODO: see how to dedup / extract this to app-local test utils @@ -24,7 +24,7 @@ macro_rules! configure_insta { async fn ping(#[case] test_name: &str, #[case] path: &str) { configure_insta!(); - testing::request::(|request, _ctx| async move { + testing::request::(|request, _ctx| async move { let response = request.get(path).await; assert_debug_snapshot!(test_name, (response.text(), response.status_code())); diff --git a/examples/demo/tests/requests/responses.rs b/examples/demo/tests/requests/responses.rs index e68b84c60..fb60982dd 100644 --- a/examples/demo/tests/requests/responses.rs +++ b/examples/demo/tests/requests/responses.rs @@ -1,7 +1,7 @@ use axum::http::HeaderMap; use demo_app::app::App; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use rstest::rstest; use serial_test::serial; // TODO: see how to dedup / extract this to app-local test utils @@ -29,7 +29,7 @@ macro_rules! configure_insta { #[serial] async fn can_return_different_responses(#[case] uri: &str) { configure_insta!(); - testing::request::(|request, _ctx| async move { + testing::request::(|request, _ctx| async move { let response = request.get(uri).await; let mut headers = HeaderMap::new(); diff --git a/examples/demo/tests/requests/upload.rs b/examples/demo/tests/requests/upload.rs index 0c2a46667..cb80e18cc 100644 --- a/examples/demo/tests/requests/upload.rs +++ b/examples/demo/tests/requests/upload.rs @@ -1,12 +1,12 @@ use axum_test::multipart::{MultipartForm, Part}; use demo_app::{app::App, views}; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use serial_test::serial; #[tokio::test] #[serial] async fn can_upload_file() { - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let file_content = "loco file upload"; let file_part = Part::bytes(file_content.as_bytes()).file_name("loco.txt"); diff --git a/examples/demo/tests/requests/user.rs b/examples/demo/tests/requests/user.rs index a6112a718..88eae3258 100644 --- a/examples/demo/tests/requests/user.rs +++ b/examples/demo/tests/requests/user.rs @@ -1,6 +1,6 @@ use demo_app::app::App; use insta::{assert_debug_snapshot, with_settings}; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use serial_test::serial; use super::prepare_data; @@ -21,7 +21,7 @@ macro_rules! configure_insta { async fn can_get_current_user() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); @@ -44,7 +44,7 @@ async fn can_get_current_user() { async fn can_get_current_user_with_api_key() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user_data = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user_data.user.api_key); @@ -67,7 +67,7 @@ async fn can_get_current_user_with_api_key() { async fn can_convert_user_to_user_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); @@ -90,7 +90,7 @@ async fn can_convert_user_to_user_role() { async fn can_convert_user_to_admin_role() { configure_insta!(); - testing::request::(|request, ctx| async move { + testing::request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); diff --git a/examples/demo/tests/requests/view_engine.rs b/examples/demo/tests/requests/view_engine.rs index 4f7422556..315169fa1 100644 --- a/examples/demo/tests/requests/view_engine.rs +++ b/examples/demo/tests/requests/view_engine.rs @@ -1,6 +1,6 @@ use demo_app::app::App; use insta::assert_debug_snapshot; -use loco_rs::testing; +use loco_rs::{app::AppContext, testing}; use rstest::rstest; use serial_test::serial; // TODO: see how to dedup / extract this to app-local test utils @@ -22,7 +22,7 @@ macro_rules! configure_insta { #[serial] async fn can_get_view_engine(#[case] uri: &str) { configure_insta!(); - testing::request::(|request, _ctx| async move { + testing::request::(|request, _ctx| async move { let response = request.get(&format!("/view-engine/{uri}")).await; assert_debug_snapshot!( diff --git a/examples/demo/tests/tasks/foo.rs b/examples/demo/tests/tasks/foo.rs index e45bbc085..c7b4c4e56 100644 --- a/examples/demo/tests/tasks/foo.rs +++ b/examples/demo/tests/tasks/foo.rs @@ -1,13 +1,13 @@ use demo_app::app::App; -use loco_rs::{boot::run_task, task, testing}; +use loco_rs::{app::AppContext, boot::run_task, task, testing}; use serial_test::serial; #[tokio::test] #[serial] async fn test_can_run_foo_task() { - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); - assert!(run_task::( + assert!(run_task::( &boot.app_context, Some(&"foo".to_string()), &task::Vars::default() diff --git a/examples/demo/tests/tasks/seed.rs b/examples/demo/tests/tasks/seed.rs index 2657bf2a4..6f854c3d1 100644 --- a/examples/demo/tests/tasks/seed.rs +++ b/examples/demo/tests/tasks/seed.rs @@ -1,13 +1,13 @@ use demo_app::app::App; -use loco_rs::{boot::run_task, task, testing}; +use loco_rs::{app::AppContext, boot::run_task, task, testing}; use serial_test::serial; #[tokio::test] #[serial] async fn test_can_seed_data() { - let boot = testing::boot_test::().await.unwrap(); + let boot = testing::boot_test::().await.unwrap(); - assert!(run_task::( + assert!(run_task::( &boot.app_context, Some(&"seed_data".to_string()), &task::Vars::default() From f35443aa4e60bbf236bf25d053edae406fc0005f Mon Sep 17 00:00:00 2001 From: Mason Stallmo Date: Sun, 22 Sep 2024 01:01:19 -0700 Subject: [PATCH 3/4] Add documentation for new traits --- src/app.rs | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 1 deletion(-) diff --git a/src/app.rs b/src/app.rs index 6f9c28cf5..3ec1363c4 100644 --- a/src/app.rs +++ b/src/app.rs @@ -27,6 +27,11 @@ use crate::{ Result, }; +/// Object-safe trait for representing application context needed +/// by the web server to operate. +/// +/// See [AppContextTrait] for more complete documentation on +/// application context. pub trait Context: Send + Sync + 'static { fn environment(&self) -> &Environment; #[cfg(feature = "with-db")] @@ -38,7 +43,114 @@ pub trait Context: Send + Sync + 'static { fn cache(&self) -> Arc; } -pub trait AppContextTrait: Clone + Default + Context { +/// This trait defines the configuration required by the +/// web server to operate. +/// +/// This trait along with [Context] should be implemented for any +/// struct used to represent the application context. A default implementation +/// is provided by the [AppContext] struct that can be used in your server. +/// +/// ```rust,ignore +/// use loco_rs::{app::{AppContext, AppContextTrait, Context}}; +/// +/// #[derive(Default)] +/// struct LocalContext { +/// app_context: AppContext, +/// } +/// +/// impl Context for LocalContext { +/// fn environment(&self) -> &Environment { +/// &self.app_context.environment +/// } +/// +/// #[cfg(feature = "with-db")] +/// fn db(&self) -> &DatabaseConnection { +/// &self.app_context.db +/// } +/// +/// fn queue(&self) -> &Option> { +/// &self.app_context.queue +/// } +/// +/// fn config(&self) -> &Config { +/// &self.app_context.config +/// } +/// +/// fn mailer(&self) -> &Option { +/// &self.app_context.mailer +/// } +/// +/// fn storage(&self) -> Arc { +/// self.app_context.storage.clone() +/// } +/// +/// fn cache(&self) -> Arc { +/// self.app_context.cache.clone() +/// } +/// } +/// +/// impl AppContextTrait for LocalContext { +/// +/// #[cfg(feature = "with-db")] +/// fn create( +/// environment: Environment, +/// config: Config, +/// db: DatabaseConnection, +/// queue: Option>, +/// ) -> Result { +/// let mailer = if let Some(cfg) = config.mailer.as_ref() { +/// create_mailer(cfg)? +/// } else { +/// None +/// }; +/// +/// Ok(LocalContext { +/// app_context: AppContext { +/// environment, +/// db, +/// queue, +/// storage: Storage::single(storage::drivers::null::new()).into(), +/// cache: Cache::new(cache::drivers::null::new()).into(), +/// config, +/// mailer, +/// } +/// }) +/// } +/// +/// +/// +/// #[cfg(not(feature = "with-db"))] +/// fn create( +/// environment: Environment, +/// config: Config, +/// queue: Option>, +/// ) -> Result { +/// let mailer = if let Some(cfg) = config.mailer.as_ref() { +/// create_mailer(cfg)? +/// } else { +/// None +/// }; +/// +/// Ok(LocalContext { +/// app_context: AppContext { +/// environment, +/// queue, +/// storage: Storage::single(storage::drivers::null::new()).into(), +/// cache: Cache::new(cache::drivers::null::new()).into(), +/// config, +/// mailer, +/// } +/// }) +/// } +/// } +/// +/// impl Hooks for App { +/// . +/// . +/// . +/// } +/// ``` +pub trait AppContextTrait: Context + Clone + Default { #[cfg(feature = "with-db")] fn create( environment: Environment, From a4e1b0bf16354704242c7c66d2d9c752d0c80f2e Mon Sep 17 00:00:00 2001 From: Mason Stallmo Date: Sun, 22 Sep 2024 01:05:36 -0700 Subject: [PATCH 4/4] Format with cargo +nightly fmt --- src/app.rs | 6 +++--- src/boot.rs | 14 ++++++++------ src/controller/app_routes.rs | 6 +++--- src/controller/format.rs | 5 +++-- src/controller/routes.rs | 1 - src/controller/views/mod.rs | 4 ++-- src/logger.rs | 7 +++---- src/mailer/mod.rs | 3 +-- src/scheduler.rs | 20 ++++++++++---------- src/tests_cfg/config.rs | 3 ++- src/tests_cfg/db.rs | 12 ++++++------ 11 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3ec1363c4..a0917468f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -30,7 +30,7 @@ use crate::{ /// Object-safe trait for representing application context needed /// by the web server to operate. /// -/// See [AppContextTrait] for more complete documentation on +/// See [`AppContextTrait`] for more complete documentation on /// application context. pub trait Context: Send + Sync + 'static { fn environment(&self) -> &Environment; @@ -46,9 +46,9 @@ pub trait Context: Send + Sync + 'static { /// This trait defines the configuration required by the /// web server to operate. /// -/// This trait along with [Context] should be implemented for any +/// This trait along with [`Context`] should be implemented for any /// struct used to represent the application context. A default implementation -/// is provided by the [AppContext] struct that can be used in your server. +/// is provided by the [`AppContext`] struct that can be used in your server. /// /// ```rust,ignore /// use loco_rs::{app::{AppContext, AppContextTrait, Context}}; diff --git a/src/boot.rs b/src/boot.rs index c20b34279..1ced9f73f 100644 --- a/src/boot.rs +++ b/src/boot.rs @@ -125,13 +125,15 @@ pub async fn run_task>( Ok(()) } -/// Runs the scheduler with the given configuration and context. in case if list args is true -/// prints scheduler job configuration +/// Runs the scheduler with the given configuration and context. in case if list +/// args is true prints scheduler job configuration /// -/// This function initializes the scheduler, registers tasks through the provided [`Hooks`], -/// and executes the scheduler based on the specified configuration or context. The scheduler -/// continuously runs, managing and executing scheduled tasks until a signal is received to shut down. -/// Upon receiving this signal, the function gracefully shuts down all running tasks and exits safely. +/// This function initializes the scheduler, registers tasks through the +/// provided [`Hooks`], and executes the scheduler based on the specified +/// configuration or context. The scheduler continuously runs, managing and +/// executing scheduled tasks until a signal is received to shut down. +/// Upon receiving this signal, the function gracefully shuts down all running +/// tasks and exits safely. /// /// # Errors /// diff --git a/src/controller/app_routes.rs b/src/controller/app_routes.rs index 063688e62..715aee7fb 100644 --- a/src/controller/app_routes.rs +++ b/src/controller/app_routes.rs @@ -516,13 +516,13 @@ fn handle_panic(err: Box) -> axum::response: #[cfg(test)] mod tests { - use super::*; - use crate::prelude::*; - use crate::tests_cfg; use insta::assert_debug_snapshot; use rstest::rstest; use tower::ServiceExt; + use super::*; + use crate::{prelude::*, tests_cfg}; + async fn action() -> Result { format::json("loco") } diff --git a/src/controller/format.rs b/src/controller/format.rs index 8acb70382..1f8aa7a4a 100644 --- a/src/controller/format.rs +++ b/src/controller/format.rs @@ -371,11 +371,12 @@ pub fn render() -> RenderBuilder { #[cfg(test)] mod tests { - use super::*; - use crate::{controller::views::engines::TeraView, prelude::*}; use insta::assert_debug_snapshot; use tree_fs; + use super::*; + use crate::{controller::views::engines::TeraView, prelude::*}; + async fn response_body_to_string(response: hyper::Response) -> String { let bytes = axum::body::to_bytes(response.into_body(), 200) .await diff --git a/src/controller/routes.rs b/src/controller/routes.rs index d58e02144..a49317ce5 100644 --- a/src/controller/routes.rs +++ b/src/controller/routes.rs @@ -47,7 +47,6 @@ impl Routes { /// format::json(Health { ok: true }) /// } /// Routes::::at("status").add("/_ping", get(ping)); - /// /// ```` #[must_use] pub fn at(prefix: &str) -> Self { diff --git a/src/controller/views/mod.rs b/src/controller/views/mod.rs index 2948cb61f..4eccc672e 100644 --- a/src/controller/views/mod.rs +++ b/src/controller/views/mod.rs @@ -28,8 +28,8 @@ impl ViewEngine { /// A struct representing an inline Tera view renderer. /// -/// This struct provides functionality to render templates using the Tera templating engine -/// directly from raw template strings. +/// This struct provides functionality to render templates using the Tera +/// templating engine directly from raw template strings. /// /// # Example /// ``` diff --git a/src/logger.rs b/src/logger.rs index ca5b74af6..f44aa29ba 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -5,10 +5,9 @@ use std::sync::OnceLock; use serde::{Deserialize, Serialize}; use serde_variant::to_variant_name; use tracing_appender::non_blocking::WorkerGuard; -use tracing_subscriber::fmt::MakeWriter; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; +use tracing_subscriber::{ + fmt, fmt::MakeWriter, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry, +}; use crate::{ app::{AppContextTrait, Hooks}, diff --git a/src/mailer/mod.rs b/src/mailer/mod.rs index 83e5a8314..fd6a3230c 100644 --- a/src/mailer/mod.rs +++ b/src/mailer/mod.rs @@ -11,10 +11,9 @@ use include_dir::Dir; use serde::{Deserialize, Serialize}; use sidekiq::Worker; -use crate::app::AppContextTrait; - use self::template::Template; use super::{app::AppContext, worker::AppWorker, Result}; +use crate::app::AppContextTrait; pub const DEFAULT_FROM_SENDER: &str = "System "; diff --git a/src/scheduler.rs b/src/scheduler.rs index 7db31a3e0..bac3f71bd 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -1,8 +1,6 @@ //! # Scheduler Module //! TBD -use regex::Regex; -use serde::{Deserialize, Serialize}; use std::{ collections::HashMap, fmt, io, @@ -10,14 +8,16 @@ use std::{ time::Instant, }; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use tokio_cron_scheduler::{JobScheduler, JobSchedulerError}; + use crate::{ app::{AppContextTrait, Hooks}, environment::Environment, task::Tasks, }; -use tokio_cron_scheduler::{JobScheduler, JobSchedulerError}; - lazy_static::lazy_static! { static ref RE_IS_CRON_SYNTAX: Regex = Regex::new(r"^[\*\d]").unwrap(); } @@ -75,7 +75,7 @@ pub struct Job { /// /// The format is as follows: /// sec min hour day of month month day of week year - /// * * * * * * * + /// * * * * * * * pub cron: String, /// Tags for tagging the job. pub tags: Option>, @@ -225,8 +225,8 @@ impl Scheduler { /// Creates a new scheduler instance from the provided configuration data. /// - /// When creating a new scheduler instance all register task should be loaded for validate the - /// given configuration. + /// When creating a new scheduler instance all register task should be + /// loaded for validate the given configuration. /// /// # Errors /// @@ -361,14 +361,14 @@ impl Scheduler { #[cfg(test)] mod tests { - use super::*; - use crate::{app::AppContext, tests_cfg}; use insta::assert_debug_snapshot; - use rstest::rstest; use tests_cfg::db::AppHook; use tokio::time::{self, Duration}; + use super::*; + use crate::{app::AppContext, tests_cfg}; + fn get_scheduler_from_config() -> Result { let scheduler_config_path = PathBuf::from("tests") .join("fixtures") diff --git a/src/tests_cfg/config.rs b/src/tests_cfg/config.rs index 7e8651c2e..bfb67e5fb 100644 --- a/src/tests_cfg/config.rs +++ b/src/tests_cfg/config.rs @@ -1,8 +1,9 @@ +use std::collections::HashMap; + use crate::{ config::{self, Config}, logger, scheduler, }; -use std::collections::HashMap; #[must_use] pub fn test_config() -> Config { diff --git a/src/tests_cfg/db.rs b/src/tests_cfg/db.rs index 77e66be42..8af150a43 100644 --- a/src/tests_cfg/db.rs +++ b/src/tests_cfg/db.rs @@ -1,3 +1,9 @@ +use std::path::Path; + +use async_trait::async_trait; +use sea_orm::DatabaseConnection; +pub use sea_orm_migration::prelude::*; + #[cfg(feature = "channels")] use crate::controller::channels::AppChannels; use crate::{ @@ -10,12 +16,6 @@ use crate::{ Result, }; -use std::path::Path; - -use async_trait::async_trait; -use sea_orm::DatabaseConnection; -pub use sea_orm_migration::prelude::*; - /// Creating a dummy db connection for docs /// /// # Panics