cot/project.rs
1//! This module contains the core types and traits for a Cot project.
2//!
3//! This module defines the [`Project`] and [`App`] traits, which are the main
4//! entry points for your application.
5//! # Examples
6//!
7//! ```no_run
8//! use cot::Project;
9//! use cot::cli::CliMetadata;
10//!
11//! struct MyProject;
12//! impl Project for MyProject {
13//! fn cli_metadata(&self) -> CliMetadata {
14//! cot::cli::metadata!()
15//! }
16//! }
17//!
18//! #[cot::main]
19//! fn main() -> impl Project {
20//! MyProject
21//! }
22//! ```
23use std::future::poll_fn;
24use std::panic::AssertUnwindSafe;
25use std::path::PathBuf;
26use std::sync::Arc;
27
28use async_trait::async_trait;
29use axum::handler::HandlerWithoutStateExt;
30use cot::Template;
31use cot_core::error::impl_into_cot_error;
32use cot_core::handler::BoxedHandler;
33use cot_core::request::AppName;
34use derive_more::with_trait::Debug;
35use futures_util::FutureExt;
36use thiserror::Error;
37use tower::util::BoxCloneSyncService;
38use tower::{Layer, Service};
39use tracing::{error, info, trace};
40
41use crate::admin::AdminModelManager;
42#[cfg(feature = "db")]
43use crate::auth::db::DatabaseUserBackend;
44use crate::auth::{AuthBackend, NoAuthBackend};
45#[cfg(feature = "cache")]
46use crate::cache::Cache;
47use crate::cli::Cli;
48#[cfg(feature = "cache")]
49use crate::config::CacheConfig;
50#[cfg(feature = "db")]
51use crate::config::DatabaseConfig;
52use crate::config::{AuthBackendConfig, ProjectConfig};
53#[cfg(feature = "db")]
54use crate::db::Database;
55#[cfg(feature = "db")]
56use crate::db::migrations::{MigrationEngine, SyncDynMigration};
57#[cfg(feature = "email")]
58use crate::email::Email;
59use crate::error::UncaughtPanic;
60use crate::error::handler::{DynErrorPageHandler, RequestOuterError};
61use crate::error_page::Diagnostics;
62use crate::html::Html;
63use crate::middleware::{IntoCotError, IntoCotErrorLayer, IntoCotResponse, IntoCotResponseLayer};
64use crate::request::{Request, RequestExt, RequestHead};
65use crate::response::{IntoResponse, Response};
66use crate::router::{Route, Router, RouterService};
67use crate::static_files::StaticFile;
68use crate::utils::accept_header_parser::AcceptHeaderParser;
69use crate::{Body, Error, cli, error_page};
70
71/// A building block for a Cot project.
72///
73/// A Cot app is a part (ideally, reusable) of a Cot project that is
74/// responsible for its own set of functionalities. Examples of apps could be:
75/// * admin panel
76/// * user authentication
77/// * blog
78/// * message board
79/// * session management
80/// * etc.
81///
82/// Each app can have its own set of URLs that can be mounted on the project's
83/// router, its own set of middlewares, database migrations (which can depend on
84/// other apps), etc.
85#[async_trait]
86pub trait App: Send + Sync {
87 /// The name of the app.
88 ///
89 /// This should usually be the name of the crate.
90 ///
91 /// # Examples
92 ///
93 /// ```
94 /// use cot::App;
95 ///
96 /// struct MyApp;
97 /// impl App for MyApp {
98 /// fn name(&self) -> &str {
99 /// env!("CARGO_PKG_NAME")
100 /// }
101 /// }
102 /// ```
103 fn name(&self) -> &str;
104
105 /// Initializes the app.
106 ///
107 /// This method is called when the app is initialized. It can be used to
108 /// initialize whatever is needed for the app to work, possibly depending on
109 /// other apps, or the project's configuration.
110 ///
111 /// # Errors
112 ///
113 /// This method returns an error if the app fails to initialize.
114 #[expect(unused_variables)]
115 async fn init(&self, context: &mut ProjectContext) -> crate::Result<()> {
116 Ok(())
117 }
118
119 /// Returns the router for the app. By default, it returns an empty router.
120 ///
121 /// # Examples
122 ///
123 /// ```
124 /// use cot::App;
125 /// use cot::html::Html;
126 /// use cot::router::{Route, Router};
127 ///
128 /// async fn index() -> Html {
129 /// Html::new("Hello world!")
130 /// }
131 ///
132 /// struct MyApp;
133 /// impl App for MyApp {
134 /// fn name(&self) -> &str {
135 /// "my_app"
136 /// }
137 ///
138 /// fn router(&self) -> Router {
139 /// Router::with_urls([Route::with_handler("/", index)])
140 /// }
141 /// }
142 /// ```
143 fn router(&self) -> Router {
144 Router::empty()
145 }
146
147 /// Returns the migrations for the app. By default, it returns an empty
148 /// list.
149 #[cfg(feature = "db")]
150 fn migrations(&self) -> Vec<Box<SyncDynMigration>> {
151 vec![]
152 }
153
154 /// Returns the admin model managers for the app. By default, it returns an
155 /// empty list.
156 fn admin_model_managers(&self) -> Vec<Box<dyn AdminModelManager>> {
157 vec![]
158 }
159
160 /// Returns a list of static files that the app serves. By default, it
161 /// returns an empty list.
162 fn static_files(&self) -> Vec<StaticFile> {
163 vec![]
164 }
165}
166
167/// The main trait for a Cot project.
168///
169/// This is the main entry point for your application. This trait defines
170/// the configuration, apps, and other project-wide resources.
171///
172/// It's mainly meant to be used with the [`cot::main`] attribute macro.
173///
174/// # Examples
175///
176/// ```no_run
177/// use cot::Project;
178/// use cot::cli::CliMetadata;
179///
180/// struct MyProject;
181/// impl Project for MyProject {
182/// fn cli_metadata(&self) -> CliMetadata {
183/// cot::cli::metadata!()
184/// }
185/// }
186///
187/// #[cot::main]
188/// fn main() -> impl Project {
189/// MyProject
190/// }
191/// ```
192pub trait Project {
193 /// Returns the metadata for the CLI.
194 ///
195 /// This method is used to set the name, version, authors, and description
196 /// of the CLI application. This is meant to be typically used with
197 /// [`cli::metadata!()`] which automatically retrieves this data from the
198 /// crate metadata.
199 ///
200 /// The default implementation sets the name, version, authors, and
201 /// description of the `cot` crate.
202 ///
203 /// # Examples
204 ///
205 /// ```
206 /// use cot::Project;
207 /// use cot::cli::CliMetadata;
208 ///
209 /// struct HelloProject;
210 /// impl Project for HelloProject {
211 /// fn cli_metadata(&self) -> CliMetadata {
212 /// cot::cli::metadata!()
213 /// }
214 /// }
215 /// ```
216 fn cli_metadata(&self) -> cli::CliMetadata {
217 cli::metadata!()
218 }
219
220 /// Returns the configuration for the project.
221 ///
222 /// The default implementation reads the configuration from the `config`
223 /// directory in the current working directory (for instance, if
224 /// `config_name` is `test`, then `config/test.toml` in the current working
225 /// directory is read). If the file does not exist, it tries to read the
226 /// file directly at `config_name` path.
227 ///
228 /// You might want to override this method if you want to read the
229 /// configuration from a different source, or if you want to hardcode
230 /// it in the binary.
231 ///
232 /// # Errors
233 ///
234 /// This method may return an error if it cannot read or parse the
235 /// configuration.
236 ///
237 /// # Examples
238 ///
239 /// ```
240 /// use cot::Project;
241 /// use cot::config::ProjectConfig;
242 ///
243 /// struct MyProject;
244 /// impl Project for MyProject {
245 /// fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
246 /// Ok(ProjectConfig::default())
247 /// }
248 /// }
249 /// ```
250 fn config(&self, config_name: &str) -> crate::Result<ProjectConfig> {
251 read_config(config_name)
252 }
253
254 /// Adds a task to the CLI.
255 ///
256 /// This method is used to add a task to the CLI. The task will be available
257 /// as a subcommand of the main CLI command.
258 ///
259 /// # Examples
260 ///
261 /// ```
262 /// use async_trait::async_trait;
263 /// use clap::{ArgMatches, Command};
264 /// use cot::cli::{Cli, CliTask};
265 /// use cot::project::WithConfig;
266 /// use cot::{Bootstrapper, Project};
267 ///
268 /// struct Frobnicate;
269 ///
270 /// #[async_trait(?Send)]
271 /// impl CliTask for Frobnicate {
272 /// fn subcommand(&self) -> Command {
273 /// Command::new("frobnicate")
274 /// }
275 ///
276 /// async fn execute(
277 /// &mut self,
278 /// _matches: &ArgMatches,
279 /// _bootstrapper: Bootstrapper<WithConfig>,
280 /// ) -> cot::Result<()> {
281 /// println!("Frobnicating...");
282 ///
283 /// Ok(())
284 /// }
285 /// }
286 ///
287 /// struct MyProject;
288 /// impl Project for MyProject {
289 /// fn register_tasks(&self, cli: &mut Cli) {
290 /// cli.add_task(Frobnicate)
291 /// }
292 /// }
293 /// ```
294 #[expect(unused_variables)]
295 fn register_tasks(&self, cli: &mut Cli) {}
296
297 /// Registers the apps for the project.
298 ///
299 /// # Examples
300 ///
301 /// ```
302 /// use cot::project::{AppBuilder, RegisterAppsContext};
303 /// use cot::{App, Project};
304 ///
305 /// struct MyApp;
306 /// impl App for MyApp {
307 /// fn name(&self) -> &str {
308 /// "my_app"
309 /// }
310 /// }
311 ///
312 /// struct MyProject;
313 /// impl Project for MyProject {
314 /// fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) {
315 /// apps.register(MyApp);
316 /// }
317 /// }
318 /// ```
319 #[expect(unused_variables)]
320 fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) {}
321
322 /// Sets the authentication backend to use.
323 ///
324 /// Note that it's typically not necessary to override this method, as it
325 /// already provides a default implementation that uses the auth backend
326 /// specified in the project's configuration.
327 ///
328 /// # Examples
329 ///
330 /// ```
331 /// use std::sync::Arc;
332 ///
333 /// use cot::auth::{AuthBackend, NoAuthBackend};
334 /// use cot::project::AuthBackendContext;
335 /// use cot::{App, Project};
336 ///
337 /// struct HelloProject;
338 /// impl Project for HelloProject {
339 /// fn auth_backend(&self, context: &AuthBackendContext) -> Arc<dyn AuthBackend> {
340 /// Arc::new(NoAuthBackend)
341 /// }
342 /// }
343 /// ```
344 fn auth_backend(&self, context: &AuthBackendContext) -> Arc<dyn AuthBackend> {
345 #[expect(trivial_casts)] // cast to Arc<dyn AuthBackend>
346 match &context.config().auth_backend {
347 AuthBackendConfig::None => Arc::new(NoAuthBackend) as Arc<dyn AuthBackend>,
348 #[cfg(feature = "db")]
349 AuthBackendConfig::Database => Arc::new(DatabaseUserBackend::new(
350 context
351 .try_database()
352 .expect(
353 "Database missing when constructing database auth backend. \
354 Make sure the database config is set up correctly or disable \
355 authentication in the config.",
356 )
357 .clone(),
358 )) as Arc<dyn AuthBackend>,
359 }
360 }
361
362 /// Returns the middlewares for the project.
363 ///
364 /// This method is used to return the middlewares for the project. The
365 /// middlewares will be applied to all routes in the project.
366 ///
367 /// # Examples
368 ///
369 /// ```
370 /// use cot::Project;
371 /// use cot::middleware::LiveReloadMiddleware;
372 /// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
373 ///
374 /// struct MyProject;
375 /// impl Project for MyProject {
376 /// fn middlewares(
377 /// &self,
378 /// handler: RootHandlerBuilder,
379 /// context: &MiddlewareContext,
380 /// ) -> RootHandler {
381 /// handler
382 /// .middleware(LiveReloadMiddleware::from_context(context))
383 /// .build()
384 /// }
385 /// }
386 /// ```
387 #[expect(unused_variables)]
388 fn middlewares(&self, handler: RootHandlerBuilder, context: &MiddlewareContext) -> RootHandler {
389 handler.build()
390 }
391
392 /// Returns the error handler for the project.
393 ///
394 /// The default handler returns a simple, minimalistic error page
395 /// that displays the status code canonical name and a
396 /// generic error message.
397 ///
398 /// # Errors
399 ///
400 /// This method may return an error if the handler fails to build a
401 /// response. In this case, the error will be logged and a generic
402 /// error page will be returned to the user. Because of that, you
403 /// should avoid returning an error from this handler and try to
404 /// handle the error gracefully instead.
405 ///
406 /// # Panics
407 ///
408 /// Note that this handler is exempt from the typical panic handling
409 /// machinery in Cot. This means that if this handler panics, no
410 /// response will be sent to a user. Because of that, you should
411 /// avoid panicking here and return [`Err`] instead.
412 ///
413 /// # Examples
414 ///
415 /// ```
416 /// use cot::Project;
417 /// use cot::error::handler::{DynErrorPageHandler, RequestError};
418 /// use cot::html::Html;
419 /// use cot::response::IntoResponse;
420 ///
421 /// struct MyProject;
422 /// impl Project for MyProject {
423 /// fn error_handler(&self) -> DynErrorPageHandler {
424 /// DynErrorPageHandler::new(error_handler)
425 /// }
426 /// }
427 ///
428 /// async fn error_handler(error: RequestError) -> impl IntoResponse {
429 /// Html::new(format!("An error occurred: {error}")).with_status(error.status_code())
430 /// }
431 /// ```
432 fn error_handler(&self) -> DynErrorPageHandler {
433 DynErrorPageHandler::new(default_error_handler)
434 }
435}
436
437/// An alias for `ProjectContext` in appropriate phase for use with the
438/// [`Project::register_apps`] method. This represents the `ProjectContext`
439/// during the [`WithConfig`] phase.
440pub type RegisterAppsContext = ProjectContext<WithConfig>;
441
442/// An alias for `ProjectContext` in appropriate phase for use with the
443/// [`Project::auth_backend`] method.
444pub type AuthBackendContext = ProjectContext<WithCache>;
445
446/// An alias for `ProjectContext` in appropriate phase for use with the
447/// [`Project::middlewares`] method.
448pub type MiddlewareContext = ProjectContext<WithCache>;
449
450/// A helper struct to build the root handler for the project.
451///
452/// This is mainly useful for attaching middlewares to the project.
453///
454/// # Examples
455///
456/// ```
457/// use cot::Project;
458/// use cot::middleware::LiveReloadMiddleware;
459/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
460///
461/// struct MyProject;
462/// impl Project for MyProject {
463/// fn middlewares(
464/// &self,
465/// handler: RootHandlerBuilder,
466/// context: &MiddlewareContext,
467/// ) -> RootHandler {
468/// handler
469/// .middleware(LiveReloadMiddleware::from_context(context))
470/// .build()
471/// }
472/// }
473/// ```
474#[derive(Debug, Clone, PartialEq, Eq, Hash)]
475pub struct RootHandlerBuilder<S = RouterService, SE = DynErrorPageHandler> {
476 handler: S,
477 error_handler: SE,
478}
479
480impl<RootService, ErrorHandlerService> RootHandlerBuilder<RootService, ErrorHandlerService>
481where
482 RootService:
483 Service<Request, Response = Response, Error = Error> + Send + Sync + Clone + 'static,
484 RootService::Future: Send,
485 ErrorHandlerService:
486 Service<Request, Response = Response, Error = Error> + Send + Sync + Clone + 'static,
487 ErrorHandlerService::Future: Send,
488{
489 /// Adds middleware to the project.
490 ///
491 /// This method is used to add middleware to the project. The middleware
492 /// will be applied to all routes in the project.
493 ///
494 /// # Examples
495 ///
496 /// ```
497 /// use cot::Project;
498 /// use cot::middleware::LiveReloadMiddleware;
499 /// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
500 ///
501 /// struct MyProject;
502 /// impl Project for MyProject {
503 /// fn middlewares(
504 /// &self,
505 /// handler: RootHandlerBuilder,
506 /// context: &MiddlewareContext,
507 /// ) -> RootHandler {
508 /// handler
509 /// .middleware(LiveReloadMiddleware::from_context(context))
510 /// .build()
511 /// }
512 /// }
513 /// ```
514 #[must_use]
515 pub fn middleware<M>(
516 self,
517 middleware: M,
518 ) -> RootHandlerBuilder<
519 WrappedMiddleware<M, RootService>,
520 WrappedMiddleware<M, ErrorHandlerService>,
521 >
522 where
523 M: Layer<RootService> + Layer<ErrorHandlerService>,
524 {
525 let layer = (
526 IntoCotErrorLayer::new(),
527 IntoCotResponseLayer::new(),
528 middleware,
529 );
530
531 RootHandlerBuilder {
532 handler: layer.layer(self.handler),
533 error_handler: layer.layer(self.error_handler),
534 }
535 }
536
537 /// Adds middleware only to the main request handler.
538 ///
539 /// This method is used to add middleware that should only be applied to
540 /// the main request handler, not to the error handler. This is useful when
541 /// you have middleware that should only run for successful requests, such
542 /// as logging middleware that should not interfere with error handling.
543 ///
544 /// # Examples
545 ///
546 /// ```
547 /// use cot::Project;
548 /// use cot::middleware::LiveReloadMiddleware;
549 /// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
550 ///
551 /// struct MyProject;
552 /// impl Project for MyProject {
553 /// fn middlewares(
554 /// &self,
555 /// handler: RootHandlerBuilder,
556 /// context: &MiddlewareContext,
557 /// ) -> RootHandler {
558 /// handler
559 /// .main_handler_middleware(LiveReloadMiddleware::from_context(context))
560 /// .build()
561 /// }
562 /// }
563 /// ```
564 #[must_use]
565 pub fn main_handler_middleware<M>(
566 self,
567 middleware: M,
568 ) -> RootHandlerBuilder<WrappedMiddleware<M, RootService>, ErrorHandlerService>
569 where
570 M: Layer<RootService>,
571 {
572 let layer = (
573 IntoCotErrorLayer::new(),
574 IntoCotResponseLayer::new(),
575 middleware,
576 );
577
578 RootHandlerBuilder {
579 handler: layer.layer(self.handler),
580 error_handler: self.error_handler,
581 }
582 }
583
584 /// Adds middleware only to the error handler.
585 ///
586 /// This method is used to add middleware that should only be applied to
587 /// the error handler, not to the main request handler. This is useful when
588 /// you have middleware that should only run when handling errors, such as
589 /// error reporting middleware or middleware that modifies error responses.
590 ///
591 /// # Examples
592 ///
593 /// ```
594 /// use cot::Project;
595 /// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
596 /// use cot::static_files::StaticFilesMiddleware;
597 ///
598 /// struct MyProject;
599 /// impl Project for MyProject {
600 /// fn middlewares(
601 /// &self,
602 /// handler: RootHandlerBuilder,
603 /// context: &MiddlewareContext,
604 /// ) -> RootHandler {
605 /// handler
606 /// .error_handler_middleware(StaticFilesMiddleware::from_context(context))
607 /// .build()
608 /// }
609 /// }
610 /// ```
611 #[must_use]
612 pub fn error_handler_middleware<M>(
613 self,
614 middleware: M,
615 ) -> RootHandlerBuilder<RootService, WrappedMiddleware<M, ErrorHandlerService>>
616 where
617 M: Layer<ErrorHandlerService>,
618 {
619 let layer = (
620 IntoCotErrorLayer::new(),
621 IntoCotResponseLayer::new(),
622 middleware,
623 );
624
625 RootHandlerBuilder {
626 handler: self.handler,
627 error_handler: layer.layer(self.error_handler),
628 }
629 }
630
631 /// Builds the root handler for the project.
632 ///
633 /// # Examples
634 ///
635 /// ```
636 /// use cot::Project;
637 /// use cot::middleware::LiveReloadMiddleware;
638 /// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
639 ///
640 /// struct MyProject;
641 /// impl Project for MyProject {
642 /// fn middlewares(
643 /// &self,
644 /// handler: RootHandlerBuilder,
645 /// context: &MiddlewareContext,
646 /// ) -> RootHandler {
647 /// handler
648 /// .middleware(LiveReloadMiddleware::from_context(context))
649 /// .build()
650 /// }
651 /// }
652 /// ```
653 pub fn build(self) -> RootHandler {
654 RootHandler {
655 handler: BoxedHandler::new(self.handler),
656 error_handler: BoxedHandler::new(self.error_handler),
657 }
658 }
659}
660
661/// A type alias for the service type returned by the
662/// [`RootHandlerBuilder::middleware`] and similar methods.
663pub type WrappedMiddleware<M, S> = IntoCotError<IntoCotResponse<<M as Layer<S>>::Service>>;
664
665/// A built root handler that contains both the main request handler and error
666/// handler.
667///
668/// This struct is the result of building a [`RootHandlerBuilder`] and contains
669/// the finalized handlers that are ready to be used for processing requests
670/// and handling errors.
671///
672/// # Examples
673///
674/// ```
675/// use cot::Project;
676/// use cot::middleware::LiveReloadMiddleware;
677/// use cot::project::{MiddlewareContext, RootHandler, RootHandlerBuilder};
678///
679/// struct MyProject;
680/// impl Project for MyProject {
681/// fn middlewares(
682/// &self,
683/// handler: RootHandlerBuilder,
684/// context: &MiddlewareContext,
685/// ) -> RootHandler {
686/// handler
687/// .middleware(LiveReloadMiddleware::from_context(context))
688/// .build()
689/// }
690/// }
691/// ```
692#[derive(Debug)]
693pub struct RootHandler {
694 /// The main request handler that processes incoming requests.
695 pub(crate) handler: BoxedHandler,
696 /// The error handler that processes errors that occur during request
697 /// handling.
698 pub(crate) error_handler: BoxedHandler,
699}
700
701/// A helper struct to build the apps for the project.
702///
703/// # Examples
704///
705/// ```
706/// use cot::project::{AppBuilder, RegisterAppsContext};
707/// use cot::{App, Project};
708///
709/// struct MyApp;
710/// impl App for MyApp {
711/// fn name(&self) -> &str {
712/// "my_app"
713/// }
714/// }
715///
716/// struct MyProject;
717/// impl Project for MyProject {
718/// fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) {
719/// apps.register(MyApp);
720/// }
721/// }
722/// ```
723#[derive(Debug)]
724pub struct AppBuilder {
725 #[debug("..")]
726 apps: Vec<Box<dyn App>>,
727 urls: Vec<Route>,
728}
729
730impl AppBuilder {
731 fn new() -> Self {
732 Self {
733 apps: Vec::new(),
734 urls: Vec::new(),
735 }
736 }
737
738 /// Registers an app.
739 ///
740 /// This method is used to register an app. The app's views, if any, will
741 /// not be available.
742 ///
743 /// # Examples
744 ///
745 /// ```
746 /// use cot::project::RegisterAppsContext;
747 /// use cot::{App, Project};
748 ///
749 /// struct HelloApp;
750 ///
751 /// impl App for HelloApp {
752 /// fn name(&self) -> &'static str {
753 /// env!("CARGO_PKG_NAME")
754 /// }
755 /// }
756 ///
757 /// struct HelloProject;
758 /// impl Project for HelloProject {
759 /// fn register_apps(&self, apps: &mut cot::AppBuilder, _context: &RegisterAppsContext) {
760 /// apps.register(HelloApp);
761 /// }
762 /// }
763 /// ```
764 pub fn register<T: App + 'static>(&mut self, module: T) {
765 self.apps.push(Box::new(module));
766 }
767
768 /// Registers an app with views.
769 ///
770 /// This method is used to register an app with views. The app's views will
771 /// be available at the given URL prefix.
772 ///
773 /// # Examples
774 ///
775 /// ```
776 /// use cot::project::RegisterAppsContext;
777 /// use cot::{App, Project};
778 ///
779 /// struct HelloApp;
780 ///
781 /// impl App for HelloApp {
782 /// fn name(&self) -> &'static str {
783 /// env!("CARGO_PKG_NAME")
784 /// }
785 /// }
786 ///
787 /// struct HelloProject;
788 /// impl Project for HelloProject {
789 /// fn register_apps(&self, apps: &mut cot::AppBuilder, _context: &RegisterAppsContext) {
790 /// apps.register_with_views(HelloApp, "/hello");
791 /// }
792 /// }
793 /// ```
794 pub fn register_with_views<T: App + 'static>(&mut self, app: T, url_prefix: &str) {
795 let mut router = app.router();
796 router.set_app_name(AppName(app.name().to_owned()));
797
798 self.urls.push(Route::with_router(url_prefix, router));
799 self.register(app);
800 }
801}
802
803async fn default_error_handler(error: RequestOuterError) -> crate::Result<impl IntoResponse> {
804 #[derive(Debug, Template)]
805 #[template(path = "default_error.html")]
806 struct ErrorTemplate {
807 error: RequestOuterError,
808 }
809
810 let status_code = error.status_code();
811 let error_template = ErrorTemplate { error };
812 let rendered = error_template.render()?;
813
814 Ok(Html::new(rendered).with_status(status_code))
815}
816
817/// The main struct for bootstrapping the project.
818///
819/// This is the core struct for bootstrapping the project. It goes over the
820/// different phases of bootstrapping the project which are defined in the
821/// [`BootstrapPhase`] trait. Each phase has its own subset of the project's
822/// context that is available, and you have access to specific parts of the
823/// project's context depending where you are in the bootstrapping process.
824///
825/// Note that you shouldn't have to use this struct directly most of the time.
826/// It's mainly used internally by the `cot` crate to bootstrap the project.
827/// It can be useful if you want to control the bootstrapping process in
828/// custom [`CliTask`](cli::CliTask)s.
829///
830/// # Examples
831///
832/// ```
833/// use cot::project::{Bootstrapper, WithConfig};
834/// use cot::{App, Project};
835///
836/// struct MyProject;
837/// impl Project for MyProject {}
838///
839/// # #[tokio::main]
840/// # async fn main() -> cot::Result<()> {
841/// let bootstrapper = Bootstrapper::new(MyProject)
842/// .with_config(cot::config::ProjectConfig::default())
843/// .boot()
844/// .await?;
845/// let bootstrapped_project = bootstrapper.finish();
846/// # Ok(())
847/// # }
848/// ```
849#[derive(Debug)]
850pub struct Bootstrapper<S: BootstrapPhase = Initialized> {
851 #[debug("..")]
852 project: Box<dyn Project + Send>,
853 context: ProjectContext<S>,
854 handler: S::RequestHandler,
855 error_handler: S::ErrorHandler,
856}
857
858impl Bootstrapper<Uninitialized> {
859 /// Creates a new bootstrapper.
860 ///
861 /// # Examples
862 ///
863 /// ```
864 /// use cot::project::{Bootstrapper, WithConfig};
865 /// use cot::{App, Project};
866 ///
867 /// struct MyProject;
868 /// impl Project for MyProject {}
869 ///
870 /// # #[tokio::main]
871 /// # async fn main() -> cot::Result<()> {
872 /// let bootstrapper = Bootstrapper::new(MyProject);
873 /// # Ok(())
874 /// # }
875 /// ```
876 #[must_use]
877 pub fn new<P: Project + Send + 'static>(project: P) -> Self {
878 Self {
879 project: Box::new(project),
880 context: ProjectContext::new(),
881 handler: (),
882 error_handler: (),
883 }
884 }
885}
886
887impl<S: BootstrapPhase> Bootstrapper<S> {
888 /// Returns the project for the bootstrapper.
889 ///
890 /// # Examples
891 ///
892 /// ```
893 /// use cot::project::{Bootstrapper, WithConfig};
894 /// use cot::{App, Project};
895 ///
896 /// struct MyProject;
897 /// impl Project for MyProject {}
898 ///
899 /// # #[tokio::main]
900 /// # async fn main() -> cot::Result<()> {
901 /// let bootstrapper = Bootstrapper::new(MyProject);
902 /// let project = bootstrapper.project();
903 /// # Ok(())
904 /// # }
905 /// ```
906 pub fn project(&self) -> &dyn Project {
907 self.project.as_ref()
908 }
909
910 /// Returns the context for the bootstrapper.
911 ///
912 /// # Examples
913 ///
914 /// ```
915 /// use cot::project::{Bootstrapper, WithConfig};
916 /// use cot::{App, Project};
917 ///
918 /// struct MyProject;
919 /// impl Project for MyProject {}
920 ///
921 /// # #[tokio::main]
922 /// # async fn main() -> cot::Result<()> {
923 /// let bootstrapper = Bootstrapper::new(MyProject);
924 /// let context = bootstrapper.context();
925 /// # Ok(())
926 /// # }
927 /// ```
928 #[must_use]
929 pub fn context(&self) -> &ProjectContext<S> {
930 &self.context
931 }
932}
933
934impl Bootstrapper<Uninitialized> {
935 #[expect(clippy::future_not_send)] // Send not needed; CLI is run async in a single thread
936 async fn run_cli(self) -> cot::Result<()> {
937 let mut cli = Cli::new();
938
939 cli.set_metadata(self.project.cli_metadata());
940 self.project.register_tasks(&mut cli);
941
942 let common_options = cli.common_options();
943 let self_with_context = self.with_config_name(common_options.config())?;
944
945 cli.execute(self_with_context).await
946 }
947
948 /// Reads the configuration of the project and moves to the next
949 /// bootstrapping phase.
950 ///
951 /// # Errors
952 ///
953 /// This method may return an error if it cannot read the configuration of
954 /// the project.
955 ///
956 /// # Examples
957 ///
958 /// ```
959 /// use cot::config::ProjectConfig;
960 /// use cot::{Bootstrapper, Project};
961 ///
962 /// struct MyProject;
963 /// impl Project for MyProject {
964 /// fn config(&self, config_name: &str) -> cot::Result<ProjectConfig> {
965 /// Ok(ProjectConfig::default())
966 /// }
967 /// }
968 ///
969 /// # #[tokio::main]
970 /// # async fn main() -> cot::Result<()> {
971 /// let bootstrapper = Bootstrapper::new(MyProject)
972 /// .with_config_name("test")?
973 /// .boot()
974 /// .await?;
975 /// let bootstrapped_project = bootstrapper.finish();
976 /// # Ok(())
977 /// # }
978 /// ```
979 pub fn with_config_name(self, config_name: &str) -> cot::Result<Bootstrapper<WithConfig>> {
980 let config = self.project.config(config_name)?;
981
982 Ok(self.with_config(config))
983 }
984
985 /// Sets the configuration for the project.
986 ///
987 /// This is mainly useful in tests, where you want to override the default
988 /// behavior of reading the configuration from a file.
989 ///
990 /// # Examples
991 ///
992 /// ```
993 /// use cot::config::ProjectConfig;
994 /// use cot::{Bootstrapper, Project};
995 ///
996 /// struct MyProject;
997 /// impl Project for MyProject {}
998 ///
999 /// # #[tokio::main]
1000 /// # async fn main() -> cot::Result<()> {
1001 /// let bootstrapper = Bootstrapper::new(MyProject)
1002 /// .with_config(ProjectConfig::default())
1003 /// .boot()
1004 /// .await?;
1005 /// let bootstrapped_project = bootstrapper.finish();
1006 /// # Ok(())
1007 /// # }
1008 /// ```
1009 #[must_use]
1010 pub fn with_config(self, config: ProjectConfig) -> Bootstrapper<WithConfig> {
1011 Bootstrapper {
1012 project: self.project,
1013 context: self.context.with_config(config),
1014 handler: self.handler,
1015 error_handler: self.error_handler,
1016 }
1017 }
1018}
1019
1020fn read_config(config: &str) -> cot::Result<ProjectConfig> {
1021 trace!(config, "Reading project configuration");
1022 let result = match std::fs::read_to_string(config) {
1023 Ok(config_content) => Ok(config_content),
1024 Err(_err) => {
1025 // try to read the config from the `config` directory if it's not a file
1026 let path = PathBuf::from("config").join(config).with_extension("toml");
1027 trace!(
1028 config,
1029 path = %path.display(),
1030 "Failed to read config as a file; trying to read from the `config` directory"
1031 );
1032
1033 std::fs::read_to_string(&path)
1034 }
1035 };
1036
1037 let config_content = result.map_err(|err| LoadConfig {
1038 config: config.to_owned(),
1039 source: err,
1040 })?;
1041
1042 ProjectConfig::from_toml(&config_content)
1043}
1044
1045#[derive(Debug, Error)]
1046#[error("could not read the config file at `{config}` or `config/{config}.toml`")]
1047struct LoadConfig {
1048 config: String,
1049 source: std::io::Error,
1050}
1051impl_into_cot_error!(LoadConfig);
1052
1053impl Bootstrapper<WithConfig> {
1054 /// Builds the initialized Cot project instance.
1055 ///
1056 /// This is the final step in the bootstrapping process. It initializes the
1057 /// project with the given configuration and returns a [`Bootstrapper`]
1058 /// instance that contains the project's context and handler.
1059 ///
1060 /// You shouldn't have to use this method directly most of the time. It's
1061 /// mainly useful for controlling the bootstrapping process in custom
1062 /// [`CliTask`](cli::CliTask)s.
1063 ///
1064 /// # Errors
1065 ///
1066 /// This method may return an error if it cannot initialize any of the
1067 /// project's components, such as the database.
1068 ///
1069 /// # Examples
1070 ///
1071 /// ```
1072 /// use cot::config::ProjectConfig;
1073 /// use cot::{Bootstrapper, Project};
1074 ///
1075 /// struct MyProject;
1076 /// impl Project for MyProject {}
1077 ///
1078 /// # #[tokio::main]
1079 /// # async fn main() -> cot::Result<()> {
1080 /// let bootstrapper = Bootstrapper::new(MyProject)
1081 /// .with_config(ProjectConfig::default())
1082 /// .boot()
1083 /// .await?;
1084 /// let bootstrapped_project = bootstrapper.finish();
1085 /// # Ok(())
1086 /// # }
1087 /// ```
1088 pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
1089 self.with_apps().boot().await
1090 }
1091
1092 /// Moves forward to the next phase of bootstrapping, the with-apps phase.
1093 ///
1094 /// See the [`BootstrapPhase`] and [`WithApps`] documentation for more
1095 /// details.
1096 ///
1097 /// # Examples
1098 ///
1099 /// ```
1100 /// use cot::config::ProjectConfig;
1101 /// use cot::project::{Bootstrapper, WithApps};
1102 /// use cot::{AppBuilder, Project};
1103 ///
1104 /// struct MyProject;
1105 /// impl Project for MyProject {}
1106 ///
1107 /// # #[tokio::main]
1108 /// # async fn main() -> cot::Result<()> {
1109 /// let bootstrapper = Bootstrapper::new(MyProject)
1110 /// .with_config(ProjectConfig::default())
1111 /// .with_apps()
1112 /// .boot()
1113 /// .await?;
1114 /// # Ok(())
1115 /// # }
1116 /// ```
1117 #[must_use]
1118 pub fn with_apps(self) -> Bootstrapper<WithApps> {
1119 let mut module_builder = AppBuilder::new();
1120 self.project
1121 .register_apps(&mut module_builder, &self.context);
1122
1123 let router = Arc::new(Router::with_urls(module_builder.urls));
1124
1125 let context = self.context.with_apps(module_builder.apps, router);
1126
1127 Bootstrapper {
1128 project: self.project,
1129 context,
1130 handler: self.handler,
1131 error_handler: self.error_handler,
1132 }
1133 }
1134}
1135
1136impl Bootstrapper<WithApps> {
1137 /// Builds the initialized Cot project instance.
1138 ///
1139 /// This is the final step in the bootstrapping process. It initializes the
1140 /// project with the given configuration and returns a [`Bootstrapper`]
1141 /// instance that contains the project's context and handler.
1142 ///
1143 /// You shouldn't have to use this method directly most of the time. It's
1144 /// mainly useful for controlling the bootstrapping process in custom
1145 /// [`CliTask`](cli::CliTask)s.
1146 ///
1147 /// # Errors
1148 ///
1149 /// This method may return an error if it cannot initialize any of the
1150 /// project's components, such as the database.
1151 ///
1152 /// # Examples
1153 ///
1154 /// ```
1155 /// use cot::config::ProjectConfig;
1156 /// use cot::{Bootstrapper, Project};
1157 ///
1158 /// struct MyProject;
1159 /// impl Project for MyProject {}
1160 ///
1161 /// # #[tokio::main]
1162 /// # async fn main() -> cot::Result<()> {
1163 /// let bootstrapper = Bootstrapper::new(MyProject)
1164 /// .with_config(ProjectConfig::default())
1165 /// .with_apps()
1166 /// .boot()
1167 /// .await?;
1168 /// let bootstrapped_project = bootstrapper.finish();
1169 /// # Ok(())
1170 /// # }
1171 /// ```
1172 pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
1173 self.with_database().await?.boot().await
1174 }
1175
1176 /// Moves forward to the next phase of bootstrapping, the with-database
1177 /// phase.
1178 ///
1179 /// See the [`BootstrapPhase`] and [`WithDatabase`] documentation for more
1180 /// details.
1181 ///
1182 /// # Errors
1183 ///
1184 /// This method may return an error if it cannot initialize the database.
1185 ///
1186 /// # Examples
1187 ///
1188 /// ```
1189 /// use cot::config::ProjectConfig;
1190 /// use cot::project::{Bootstrapper, WithApps};
1191 /// use cot::{AppBuilder, Project};
1192 ///
1193 /// struct MyProject;
1194 /// impl Project for MyProject {}
1195 ///
1196 /// # #[tokio::main]
1197 /// # async fn main() -> cot::Result<()> {
1198 /// let bootstrapper = Bootstrapper::new(MyProject)
1199 /// .with_config(ProjectConfig::default())
1200 /// .with_apps()
1201 /// .with_database()
1202 /// .await?
1203 /// .boot()
1204 /// .await?;
1205 /// # Ok(())
1206 /// # }
1207 /// ```
1208 pub async fn with_database(self) -> cot::Result<Bootstrapper<WithDatabase>> {
1209 #[cfg(feature = "db")]
1210 let database = Self::init_database(&self.context.config.database).await?;
1211 let context = self.context.with_database(
1212 #[cfg(feature = "db")]
1213 database,
1214 );
1215
1216 Ok(Bootstrapper {
1217 project: self.project,
1218 context,
1219 handler: self.handler,
1220 error_handler: self.error_handler,
1221 })
1222 }
1223
1224 #[cfg(feature = "db")]
1225 async fn init_database(config: &DatabaseConfig) -> cot::Result<Option<Database>> {
1226 match &config.url {
1227 Some(url) => {
1228 let database = Database::new(url.as_str()).await?;
1229 Ok(Some(database))
1230 }
1231 None => Ok(None),
1232 }
1233 }
1234}
1235
1236impl Bootstrapper<WithDatabase> {
1237 /// Builds the Cot project instance.
1238 ///
1239 /// This is the final step in the bootstrapping process. It initializes the
1240 /// project with the given configuration and returns a [`Bootstrapper`]
1241 /// instance that contains the project's context and handler.
1242 ///
1243 /// You shouldn't have to use this method directly most of the time. It's
1244 /// mainly useful for controlling the bootstrapping process in custom
1245 /// [`CliTask`](cli::CliTask)s.
1246 ///
1247 /// # Errors
1248 ///
1249 /// This method may return an error if it cannot initialize any of the
1250 /// project's components, such as the database.
1251 ///
1252 /// # Examples
1253 ///
1254 /// ```
1255 /// use cot::config::ProjectConfig;
1256 /// use cot::{Bootstrapper, Project};
1257 ///
1258 /// struct MyProject;
1259 /// impl Project for MyProject {}
1260 ///
1261 /// # #[tokio::main]
1262 /// # async fn main() -> cot::Result<()> {
1263 /// let bootstrapper = Bootstrapper::new(MyProject)
1264 /// .with_config(ProjectConfig::default())
1265 /// .boot()
1266 /// .await?;
1267 /// let bootstrapped_project = bootstrapper.finish();
1268 /// # Ok(())
1269 /// # }
1270 /// ```
1271 // Function marked `async` to be consistent with the other `boot` methods
1272 pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
1273 self.with_cache().await?.boot().await
1274 }
1275
1276 /// Moves forward to the next phase of bootstrapping, the with-cache phase.
1277 ///
1278 /// See the [`BootstrapPhase`] and [`WithCache`] documentation for more
1279 /// details.
1280 ///
1281 /// # Errors
1282 ///
1283 /// This method may return an error if it cannot initialize the cache.
1284 ///
1285 /// # Examples
1286 ///
1287 /// ```
1288 /// use cot::config::ProjectConfig;
1289 /// use cot::project::{Bootstrapper, WithApps};
1290 /// use cot::{AppBuilder, Project};
1291 ///
1292 /// struct MyProject;
1293 /// impl Project for MyProject {}
1294 ///
1295 /// # #[tokio::main]
1296 /// # async fn main() -> cot::Result<()> {
1297 /// let bootstrapper = Bootstrapper::new(MyProject)
1298 /// .with_config(ProjectConfig::default())
1299 /// .with_apps()
1300 /// .with_database()
1301 /// .await?
1302 /// .with_cache()
1303 /// .await?
1304 /// .boot()
1305 /// .await?;
1306 /// # Ok(())
1307 /// # }
1308 /// ```
1309 #[allow(
1310 clippy::unused_async,
1311 clippy::allow_attributes,
1312 reason = "see https://github.com/cot-rs/cot/pull/399#discussion_r2430379966"
1313 )]
1314 pub async fn with_cache(self) -> cot::Result<Bootstrapper<WithCache>> {
1315 #[cfg(feature = "cache")]
1316 let cache = Self::init_cache(&self.context.config.cache).await?;
1317
1318 let context = self.context.with_cache(
1319 #[cfg(feature = "cache")]
1320 cache,
1321 );
1322
1323 Ok(Bootstrapper {
1324 project: self.project,
1325 context,
1326 handler: self.handler,
1327 error_handler: self.error_handler,
1328 })
1329 }
1330
1331 #[cfg(feature = "cache")]
1332 async fn init_cache(config: &CacheConfig) -> cot::Result<Cache> {
1333 let cache = Cache::from_config(config).await?;
1334 Ok(cache)
1335 }
1336}
1337
1338impl Bootstrapper<WithCache> {
1339 /// Builds the initialized Cot project instance.
1340 ///
1341 /// This is the final step in the bootstrapping process. It initializes the
1342 /// project with the given configuration and returns a [`Bootstrapper`]
1343 /// instance that contains the project's context and handler.
1344 ///
1345 /// You shouldn't have to use this method directly most of the time.
1346 /// It's mainly useful for controlling the bootstrapping process in
1347 /// custom [`CliTask`](cli::CliTask)s.
1348 ///
1349 /// # Errors
1350 ///
1351 /// This method may return an error if it cannot initialize any of the
1352 /// project's components, such as the database.
1353 ///
1354 /// # Examples
1355 ///
1356 /// ```
1357 /// use cot::config::ProjectConfig;
1358 /// use cot::{Bootstrapper, Project};
1359 ///
1360 /// struct MyProject;
1361 /// impl Project for MyProject {}
1362 ///
1363 /// # #[tokio::main]
1364 /// # async fn main() -> cot::Result<()> {
1365 /// let bootstrapper = Bootstrapper::new(MyProject)
1366 /// .with_config(ProjectConfig::default())
1367 /// .boot()
1368 /// .await?;
1369 /// let bootstrapped_project = bootstrapper.finish();
1370 /// # Ok(())
1371 /// # }
1372 /// ```
1373 #[expect(
1374 clippy::unused_async,
1375 reason = "for consistency with other Bootstrapper::boot methods"
1376 )]
1377 pub async fn boot(self) -> cot::Result<Bootstrapper<Initialized>> {
1378 let router_service = RouterService::new(Arc::clone(&self.context.router));
1379 let handler_builder = RootHandlerBuilder {
1380 handler: router_service,
1381 error_handler: self.project.error_handler(),
1382 };
1383 let handler = self.project.middlewares(handler_builder, &self.context);
1384
1385 let auth_backend = self.project.auth_backend(&self.context);
1386 let context = self.context.with_auth(auth_backend);
1387
1388 Ok(Bootstrapper {
1389 project: self.project,
1390 context,
1391 handler: handler.handler,
1392 error_handler: handler.error_handler,
1393 })
1394 }
1395}
1396impl Bootstrapper<Initialized> {
1397 /// Returns the context and handlers of the bootstrapper.
1398 ///
1399 /// # Examples
1400 ///
1401 /// ```
1402 /// use cot::config::ProjectConfig;
1403 /// use cot::project::Bootstrapper;
1404 /// use cot::{Project, ProjectContext};
1405 ///
1406 /// struct MyProject;
1407 /// impl Project for MyProject {}
1408 ///
1409 /// # #[tokio::main]
1410 /// # async fn main() -> cot::Result<()> {
1411 /// let bootstrapper = Bootstrapper::new(MyProject)
1412 /// .with_config(ProjectConfig::default())
1413 /// .boot()
1414 /// .await?;
1415 /// let bootstrapped_project = bootstrapper.finish();
1416 /// # Ok(())
1417 /// # }
1418 /// ```
1419 #[must_use]
1420 pub fn finish(self) -> BootstrappedProject {
1421 BootstrappedProject {
1422 context: self.context,
1423 handler: self.handler,
1424 error_handler: self.error_handler,
1425 }
1426 }
1427}
1428
1429/// A fully bootstrapped project with all components initialized and ready to
1430/// run.
1431///
1432/// This struct contains the final state of a project after the bootstrapping
1433/// process is complete. It includes the project context with all dependencies
1434/// initialized, as well as the request and error handlers that are ready to
1435/// process incoming requests.
1436///
1437/// # Examples
1438///
1439/// ```
1440/// use cot::config::ProjectConfig;
1441/// use cot::project::Bootstrapper;
1442/// use cot::{Project, ProjectContext};
1443///
1444/// struct MyProject;
1445/// impl Project for MyProject {}
1446///
1447/// # #[tokio::main]
1448/// # async fn main() -> cot::Result<()> {
1449/// let bootstrapper = Bootstrapper::new(MyProject)
1450/// .with_config(ProjectConfig::default())
1451/// .boot()
1452/// .await?;
1453/// let bootstrapped_project = bootstrapper.finish();
1454/// # Ok(())
1455/// # }
1456/// ```
1457#[derive(Debug)]
1458#[non_exhaustive]
1459pub struct BootstrappedProject {
1460 /// The fully initialized project context containing all dependencies and
1461 /// configuration.
1462 pub context: ProjectContext<Initialized>,
1463 /// The main request handler that processes incoming HTTP requests.
1464 pub handler: BoxedHandler,
1465 /// The error handler that processes errors that occur during request
1466 /// handling.
1467 pub error_handler: BoxedHandler,
1468}
1469
1470mod sealed {
1471 pub trait Sealed {}
1472}
1473
1474/// A trait that represents the different phases of the bootstrapper.
1475///
1476/// This trait is used to define the types for the different phases of the
1477/// bootstrapper. It's used to ensure that you can't access nonexistent
1478/// data until the bootstrapper has reached the corresponding phase.
1479///
1480/// # Order of phases
1481///
1482/// 1. [`Uninitialized`]
1483/// 2. [`WithConfig`]
1484/// 3. [`WithApps`]
1485/// 4. [`WithDatabase`]
1486/// 5. [`WithCache`]
1487/// 6. [`Initialized`]
1488///
1489/// # Sealed
1490///
1491/// This trait is sealed and can't be implemented outside the `cot`
1492/// crate.
1493///
1494/// # Examples
1495///
1496/// ```
1497/// use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder};
1498/// use cot::{AppBuilder, Project};
1499///
1500/// struct MyProject;
1501/// impl Project for MyProject {
1502/// // `WithConfig` phase here
1503/// fn register_apps(&self, apps: &mut AppBuilder, context: &RegisterAppsContext) {
1504/// unimplemented!();
1505/// }
1506///
1507/// // `WithDatabase` phase here (which comes after `WithConfig`)
1508/// fn middlewares(
1509/// &self,
1510/// handler: RootHandlerBuilder,
1511/// context: &MiddlewareContext,
1512/// ) -> RootHandler {
1513/// unimplemented!()
1514/// }
1515/// }
1516/// ```
1517pub trait BootstrapPhase: sealed::Sealed {
1518 // Bootstrapper types
1519 /// The type of the request handler.
1520 type RequestHandler: Debug;
1521 /// The type of the error handler.
1522 type ErrorHandler: Debug;
1523
1524 // App context types
1525 /// The type of the configuration.
1526 type Config: Debug;
1527 /// The type of the email service.
1528 #[cfg(feature = "email")]
1529 type Email: Debug;
1530
1531 /// The type of the apps.
1532 type Apps;
1533 /// The type of the router.
1534 type Router: Debug;
1535 /// The type of the database.
1536 #[cfg(feature = "db")]
1537 type Database: Debug;
1538 /// The type of the auth backend.
1539 type AuthBackend;
1540 /// The type of the cache.
1541 #[cfg(feature = "cache")]
1542 type Cache: Debug;
1543}
1544
1545/// First phase of bootstrapping a Cot project, the uninitialized phase.
1546///
1547/// # See also
1548///
1549/// See the details about the different bootstrap phases in the
1550/// [`BootstrapPhase`] trait documentation.
1551#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1552pub enum Uninitialized {}
1553
1554impl sealed::Sealed for Uninitialized {}
1555impl BootstrapPhase for Uninitialized {
1556 type RequestHandler = ();
1557 type ErrorHandler = ();
1558 type Config = ();
1559 #[cfg(feature = "email")]
1560 type Email = ();
1561 type Apps = ();
1562 type Router = ();
1563 #[cfg(feature = "db")]
1564 type Database = ();
1565 type AuthBackend = ();
1566 #[cfg(feature = "cache")]
1567 type Cache = ();
1568}
1569
1570/// Second phase of bootstrapping a Cot project, the with-config phase.
1571///
1572/// # See also
1573///
1574/// See the details about the different bootstrap phases in the
1575/// [`BootstrapPhase`] trait documentation.
1576#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1577pub enum WithConfig {}
1578
1579impl sealed::Sealed for WithConfig {}
1580impl BootstrapPhase for WithConfig {
1581 type RequestHandler = ();
1582 type ErrorHandler = ();
1583 type Config = Arc<ProjectConfig>;
1584 #[cfg(feature = "email")]
1585 type Email = Email;
1586 type Apps = ();
1587 type Router = ();
1588 #[cfg(feature = "db")]
1589 type Database = ();
1590 type AuthBackend = ();
1591 #[cfg(feature = "cache")]
1592 type Cache = ();
1593}
1594
1595/// Third phase of bootstrapping a Cot project, the with-apps phase.
1596///
1597/// # See also
1598///
1599/// See the details about the different bootstrap phases in the
1600/// [`BootstrapPhase`] trait documentation.
1601#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1602pub enum WithApps {}
1603
1604impl sealed::Sealed for WithApps {}
1605impl BootstrapPhase for WithApps {
1606 type RequestHandler = ();
1607 type ErrorHandler = ();
1608 type Config = <WithConfig as BootstrapPhase>::Config;
1609 #[cfg(feature = "email")]
1610 type Email = <WithConfig as BootstrapPhase>::Email;
1611 type Apps = Vec<Box<dyn App>>;
1612 type Router = Arc<Router>;
1613 #[cfg(feature = "db")]
1614 type Database = ();
1615 type AuthBackend = ();
1616 #[cfg(feature = "cache")]
1617 type Cache = ();
1618}
1619
1620/// Fourth phase of bootstrapping a Cot project, the with-database phase.
1621///
1622/// # See also
1623///
1624/// See the details about the different bootstrap phases in the
1625/// [`BootstrapPhase`] trait documentation.
1626#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1627pub enum WithDatabase {}
1628
1629impl sealed::Sealed for WithDatabase {}
1630impl BootstrapPhase for WithDatabase {
1631 type RequestHandler = ();
1632 type ErrorHandler = ();
1633 type Config = <WithApps as BootstrapPhase>::Config;
1634 #[cfg(feature = "email")]
1635 type Email = <WithApps as BootstrapPhase>::Email;
1636 type Apps = <WithApps as BootstrapPhase>::Apps;
1637 type Router = <WithApps as BootstrapPhase>::Router;
1638 #[cfg(feature = "db")]
1639 type Database = Option<Database>;
1640 type AuthBackend = <WithApps as BootstrapPhase>::AuthBackend;
1641 #[cfg(feature = "cache")]
1642 type Cache = ();
1643}
1644
1645/// Fifth phase of bootstrapping a Cot project, the with-cache phase.
1646///
1647/// # See also
1648///
1649/// See the details about the different bootstrap phases in the
1650/// [`BootstrapPhase`] trait documentation.
1651#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1652pub enum WithCache {}
1653
1654impl sealed::Sealed for WithCache {}
1655impl BootstrapPhase for WithCache {
1656 type RequestHandler = ();
1657 type ErrorHandler = ();
1658 type Config = <WithApps as BootstrapPhase>::Config;
1659 #[cfg(feature = "email")]
1660 type Email = <WithApps as BootstrapPhase>::Email;
1661 type Apps = <WithApps as BootstrapPhase>::Apps;
1662 type Router = <WithApps as BootstrapPhase>::Router;
1663 #[cfg(feature = "db")]
1664 type Database = <WithDatabase as BootstrapPhase>::Database;
1665 type AuthBackend = <WithApps as BootstrapPhase>::AuthBackend;
1666 #[cfg(feature = "cache")]
1667 type Cache = Cache;
1668}
1669
1670/// The final phase of bootstrapping a Cot project, the initialized phase.
1671///
1672/// # See also
1673///
1674/// See the details about the different bootstrap phases in the
1675/// [`BootstrapPhase`] trait documentation.
1676#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1677pub enum Initialized {}
1678
1679impl sealed::Sealed for Initialized {}
1680impl BootstrapPhase for Initialized {
1681 type RequestHandler = BoxedHandler;
1682 type ErrorHandler = BoxedHandler;
1683 type Config = <WithDatabase as BootstrapPhase>::Config;
1684 #[cfg(feature = "email")]
1685 type Email = <WithDatabase as BootstrapPhase>::Email;
1686 type Apps = <WithDatabase as BootstrapPhase>::Apps;
1687 type Router = <WithDatabase as BootstrapPhase>::Router;
1688 #[cfg(feature = "db")]
1689 type Database = <WithDatabase as BootstrapPhase>::Database;
1690 type AuthBackend = Arc<dyn AuthBackend>;
1691 #[cfg(feature = "cache")]
1692 type Cache = <WithCache as BootstrapPhase>::Cache;
1693}
1694
1695/// Shared context and configs for all apps. Used in conjunction with the
1696/// [`Project`] trait.
1697#[derive(Debug)]
1698pub struct ProjectContext<S: BootstrapPhase = Initialized> {
1699 config: S::Config,
1700 #[debug("..")]
1701 apps: S::Apps,
1702 router: S::Router,
1703 #[cfg(feature = "db")]
1704 database: S::Database,
1705 #[debug("..")]
1706 auth_backend: S::AuthBackend,
1707 #[cfg(feature = "cache")]
1708 cache: S::Cache,
1709 #[cfg(feature = "email")]
1710 email: S::Email,
1711}
1712
1713impl ProjectContext<Uninitialized> {
1714 #[must_use]
1715 pub(crate) const fn new() -> Self {
1716 Self {
1717 config: (),
1718 apps: (),
1719 router: (),
1720 #[cfg(feature = "db")]
1721 database: (),
1722 auth_backend: (),
1723 #[cfg(feature = "cache")]
1724 cache: (),
1725 #[cfg(feature = "email")]
1726 email: (),
1727 }
1728 }
1729
1730 fn with_config(self, config: ProjectConfig) -> ProjectContext<WithConfig> {
1731 #[cfg(feature = "email")]
1732 let email = {
1733 Email::from_config(&config.email).unwrap_or_else(|err| {
1734 panic!("failed to initialize email service: {err:?}");
1735 })
1736 };
1737
1738 ProjectContext {
1739 config: Arc::new(config),
1740 apps: self.apps,
1741 router: self.router,
1742 #[cfg(feature = "db")]
1743 database: self.database,
1744 auth_backend: self.auth_backend,
1745 #[cfg(feature = "cache")]
1746 cache: self.cache,
1747 #[cfg(feature = "email")]
1748 email,
1749 }
1750 }
1751}
1752
1753impl<S: BootstrapPhase<Config = Arc<ProjectConfig>>> ProjectContext<S> {
1754 /// Returns the configuration for the project.
1755 ///
1756 /// # Examples
1757 ///
1758 /// ```
1759 /// use cot::request::{Request, RequestExt};
1760 /// use cot::response::Response;
1761 ///
1762 /// async fn index(request: Request) -> cot::Result<Response> {
1763 /// let config = request.context().config();
1764 /// // can also be accessed via:
1765 /// let config = request.project_config();
1766 ///
1767 /// let db_url = &config.database.url;
1768 ///
1769 /// // ...
1770 /// # unimplemented!()
1771 /// }
1772 /// ```
1773 #[must_use]
1774 pub fn config(&self) -> &ProjectConfig {
1775 &self.config
1776 }
1777}
1778
1779impl ProjectContext<WithConfig> {
1780 #[must_use]
1781 fn with_apps(self, apps: Vec<Box<dyn App>>, router: Arc<Router>) -> ProjectContext<WithApps> {
1782 ProjectContext {
1783 config: self.config,
1784 apps,
1785 router,
1786 #[cfg(feature = "db")]
1787 database: self.database,
1788 auth_backend: self.auth_backend,
1789 #[cfg(feature = "cache")]
1790 cache: self.cache,
1791 #[cfg(feature = "email")]
1792 email: self.email,
1793 }
1794 }
1795}
1796
1797impl<S: BootstrapPhase<Apps = Vec<Box<dyn App>>>> ProjectContext<S> {
1798 /// Returns the apps for the project.
1799 ///
1800 /// # Examples
1801 ///
1802 /// ```
1803 /// use cot::request::{Request, RequestExt};
1804 /// use cot::response::Response;
1805 ///
1806 /// async fn index(request: Request) -> cot::Result<Response> {
1807 /// let apps = request.context().apps();
1808 ///
1809 /// // ...
1810 /// # unimplemented!()
1811 /// }
1812 /// ```
1813 #[must_use]
1814 pub fn apps(&self) -> &[Box<dyn App>] {
1815 &self.apps
1816 }
1817}
1818
1819impl ProjectContext<WithApps> {
1820 #[must_use]
1821 fn with_database(
1822 self,
1823 #[cfg(feature = "db")] database: Option<Database>,
1824 ) -> ProjectContext<WithDatabase> {
1825 ProjectContext {
1826 config: self.config,
1827 apps: self.apps,
1828 router: self.router,
1829 #[cfg(feature = "db")]
1830 database,
1831 auth_backend: self.auth_backend,
1832 #[cfg(feature = "cache")]
1833 cache: self.cache,
1834 #[cfg(feature = "email")]
1835 email: self.email,
1836 }
1837 }
1838}
1839
1840impl ProjectContext<WithDatabase> {
1841 #[must_use]
1842 fn with_cache(self, #[cfg(feature = "cache")] cache: Cache) -> ProjectContext<WithCache> {
1843 ProjectContext {
1844 config: self.config,
1845 apps: self.apps,
1846 router: self.router,
1847 auth_backend: self.auth_backend,
1848 #[cfg(feature = "db")]
1849 database: self.database,
1850 #[cfg(feature = "cache")]
1851 cache,
1852 #[cfg(feature = "email")]
1853 email: self.email,
1854 }
1855 }
1856}
1857
1858impl ProjectContext<WithCache> {
1859 #[must_use]
1860 fn with_auth(self, auth_backend: Arc<dyn AuthBackend>) -> ProjectContext<Initialized> {
1861 ProjectContext {
1862 config: self.config,
1863 apps: self.apps,
1864 router: self.router,
1865 auth_backend,
1866 #[cfg(feature = "db")]
1867 database: self.database,
1868 #[cfg(feature = "cache")]
1869 cache: self.cache,
1870 #[cfg(feature = "email")]
1871 email: self.email,
1872 }
1873 }
1874}
1875impl ProjectContext<Initialized> {
1876 #[cfg(feature = "test")]
1877 pub(crate) fn initialized(
1878 config: <Initialized as BootstrapPhase>::Config,
1879 apps: <Initialized as BootstrapPhase>::Apps,
1880 router: <Initialized as BootstrapPhase>::Router,
1881 auth_backend: <Initialized as BootstrapPhase>::AuthBackend,
1882 #[cfg(feature = "db")] database: <Initialized as BootstrapPhase>::Database,
1883 #[cfg(feature = "cache")] cache: <Initialized as BootstrapPhase>::Cache,
1884 #[cfg(feature = "email")] email: <Initialized as BootstrapPhase>::Email,
1885 ) -> Self {
1886 Self {
1887 config,
1888 apps,
1889 router,
1890 #[cfg(feature = "db")]
1891 database,
1892 auth_backend,
1893 #[cfg(feature = "cache")]
1894 cache,
1895 #[cfg(feature = "email")]
1896 email,
1897 }
1898 }
1899}
1900
1901impl<S: BootstrapPhase<Router = Arc<Router>>> ProjectContext<S> {
1902 /// Returns the router for the project.
1903 ///
1904 /// # Examples
1905 ///
1906 /// ```
1907 /// use cot::request::{Request, RequestExt};
1908 /// use cot::response::Response;
1909 ///
1910 /// async fn index(request: Request) -> cot::Result<Response> {
1911 /// let router = request.context().config();
1912 /// // can also be accessed via:
1913 /// let router = request.router();
1914 ///
1915 /// let num_routes = router.routes().len();
1916 ///
1917 /// // ...
1918 /// # unimplemented!()
1919 /// }
1920 /// ```
1921 #[must_use]
1922 pub fn router(&self) -> &Arc<Router> {
1923 &self.router
1924 }
1925}
1926impl<S: BootstrapPhase<AuthBackend = Arc<dyn AuthBackend>>> ProjectContext<S> {
1927 /// Returns the authentication backend for the project.
1928 ///
1929 /// # Examples
1930 ///
1931 /// ```
1932 /// use cot::request::{Request, RequestExt};
1933 /// use cot::response::Response;
1934 ///
1935 /// async fn index(request: Request) -> cot::Result<Response> {
1936 /// let auth_backend = request.context().auth_backend();
1937 /// // ...
1938 /// # unimplemented!()
1939 /// }
1940 /// ```
1941 #[must_use]
1942 pub fn auth_backend(&self) -> &Arc<dyn AuthBackend> {
1943 &self.auth_backend
1944 }
1945}
1946
1947#[cfg(feature = "email")]
1948impl<S: BootstrapPhase<Email = Email>> ProjectContext<S> {
1949 #[must_use]
1950 /// Returns the email service for the project.
1951 ///
1952 /// # Examples
1953 ///
1954 /// ```
1955 /// use cot::request::{Request, RequestExt};
1956 /// use cot::response::Response;
1957 ///
1958 /// async fn index(request: Request) -> cot::Result<Response> {
1959 /// let email = request.context().email();
1960 /// // ...
1961 /// # unimplemented!()
1962 /// }
1963 /// ```
1964 pub fn email(&self) -> &Email {
1965 &self.email
1966 }
1967}
1968
1969#[cfg(feature = "cache")]
1970impl<S: BootstrapPhase<Cache = Cache>> ProjectContext<S> {
1971 /// Returns the cache for the project.
1972 ///
1973 /// # Examples
1974 ///
1975 /// ```
1976 /// use cot::request::{Request, RequestExt};
1977 /// use cot::response::Response;
1978 ///
1979 /// async fn index(request: Request) -> cot::Result<Response> {
1980 /// let cache = request.context().cache();
1981 /// // ...
1982 /// # unimplemented!()
1983 /// }
1984 /// ```
1985 #[must_use]
1986 #[cfg(feature = "cache")]
1987 pub fn cache(&self) -> &Cache {
1988 &self.cache
1989 }
1990}
1991
1992#[cfg(feature = "db")]
1993impl<S: BootstrapPhase<Database = Option<Database>>> ProjectContext<S> {
1994 /// Returns the database for the project, if it is enabled.
1995 ///
1996 /// # Examples
1997 ///
1998 /// ```
1999 /// use cot::request::{Request, RequestExt};
2000 /// use cot::response::Response;
2001 ///
2002 /// async fn index(request: Request) -> cot::Result<Response> {
2003 /// let database = request.context().try_database();
2004 /// if let Some(database) = database {
2005 /// // do something with the database
2006 /// } else {
2007 /// // database is not enabled
2008 /// }
2009 /// # unimplemented!()
2010 /// }
2011 /// ```
2012 #[must_use]
2013 #[cfg(feature = "db")]
2014 pub fn try_database(&self) -> Option<&Database> {
2015 self.database.as_ref()
2016 }
2017
2018 /// Returns the database for the project, if it is enabled.
2019 ///
2020 /// # Panics
2021 ///
2022 /// This method panics if the database is not enabled.
2023 ///
2024 /// # Examples
2025 ///
2026 /// ```
2027 /// use cot::request::{Request, RequestExt};
2028 /// use cot::response::Response;
2029 ///
2030 /// async fn index(request: Request) -> cot::Result<Response> {
2031 /// let database = request.context().database();
2032 /// // use the database
2033 /// # unimplemented!()
2034 /// }
2035 /// ```
2036 #[cfg(feature = "db")]
2037 #[must_use]
2038 #[track_caller]
2039 pub fn database(&self) -> &Database {
2040 self.try_database().expect(
2041 "Database missing. Did you forget to add the database when configuring cot::Project?",
2042 )
2043 }
2044}
2045
2046/// Runs the Cot project on the given address.
2047///
2048/// This function takes a Cot project and an address string and runs the
2049/// project on the given address.
2050///
2051/// # Errors
2052///
2053/// This function returns an error if the server fails to start.
2054pub async fn run(bootstrapper: Bootstrapper<Initialized>, address_str: &str) -> cot::Result<()> {
2055 let listener = tokio::net::TcpListener::bind(address_str)
2056 .await
2057 .map_err(StartServerError)?;
2058
2059 run_at(bootstrapper, listener).await
2060}
2061
2062/// Runs the Cot project on the given listener.
2063///
2064/// This function takes a Cot project and a [`tokio::net::TcpListener`] and
2065/// runs the project on the given listener.
2066///
2067/// If you need more control over the server listening socket, such as modifying
2068/// the underlying buffer sizes, you can create a [`tokio::net::TcpListener`]
2069/// and pass it to this function. Otherwise, the [`run`] function will be more
2070/// convenient.
2071///
2072/// # Errors
2073///
2074/// This function returns an error if the server fails to start.
2075pub async fn run_at(
2076 bootstrapper: Bootstrapper<Initialized>,
2077 listener: tokio::net::TcpListener,
2078) -> cot::Result<()> {
2079 run_at_with_shutdown(bootstrapper, listener, shutdown_signal()).await
2080}
2081
2082/// Runs the Cot project on the given listener.
2083///
2084/// This function takes a Cot project and a [`tokio::net::TcpListener`] and
2085/// runs the project on the given listener, similarly to the [`run_at`]
2086/// function. In addition to that, it takes a shutdown signal that can be used
2087/// to gracefully shut down the server in a response to a signal or other event.
2088///
2089/// If you don't need to customize shutdown signal handling, you should instead
2090/// use the [`run`] or [`run_at`] functions, as they are more convenient.
2091///
2092/// # Errors
2093///
2094/// This function returns an error if the server fails to start.
2095pub async fn run_at_with_shutdown(
2096 bootstrapper: Bootstrapper<Initialized>,
2097 listener: tokio::net::TcpListener,
2098 shutdown_signal: impl Future<Output = ()> + Send + 'static,
2099) -> cot::Result<()> {
2100 let BootstrappedProject {
2101 mut context,
2102 mut handler,
2103 mut error_handler,
2104 } = bootstrapper.finish();
2105
2106 #[cfg(feature = "db")]
2107 if let Some(database) = &context.database {
2108 let mut migrations: Vec<Box<SyncDynMigration>> = Vec::new();
2109 for app in &context.apps {
2110 migrations.extend(app.migrations());
2111 }
2112 let migration_engine = MigrationEngine::new(migrations)?;
2113 migration_engine.run(database).await?;
2114 }
2115
2116 let mut apps = std::mem::take(&mut context.apps);
2117 for app in &mut apps {
2118 info!("Initializing app: {}", app.name());
2119
2120 app.init(&mut context).await?;
2121 }
2122 context.apps = apps;
2123
2124 let context = Arc::new(context);
2125 let is_debug = context.config().debug;
2126 let register_panic_hook = context.config().register_panic_hook;
2127 #[cfg(feature = "db")]
2128 let context_cleanup = context.clone();
2129
2130 let handler = move |axum_request: axum::extract::Request| async move {
2131 // todo root tracing span
2132 // todo per-router error handlers
2133 let request = request_axum_to_cot(axum_request, Arc::clone(&context));
2134 let (head, request) = request.into_parts();
2135 let head_for_error_handler = head.clone();
2136 let request = Request::from_parts(head, request);
2137
2138 let (request_head, request) = request_parts_for_diagnostics(request);
2139
2140 let catch_unwind_response = AssertUnwindSafe(pass_to_axum(request, &mut handler))
2141 .catch_unwind()
2142 .await;
2143
2144 let response: Result<axum::response::Response, ErrorResponse> = match catch_unwind_response
2145 {
2146 Ok(Ok(response)) => Ok(response),
2147 Ok(Err(error)) => Err(ErrorResponse::ErrorReturned(error)),
2148 Err(error) => Err(ErrorResponse::Panic(error)),
2149 };
2150
2151 match response {
2152 Ok(response) => response,
2153 Err(error_response) => {
2154 if is_debug && accepts_html(request_head.as_ref()) {
2155 let diagnostics = Diagnostics::new(
2156 context.config().clone(),
2157 Arc::clone(&context.router),
2158 request_head,
2159 );
2160
2161 build_cot_error_page(error_response, &diagnostics)
2162 } else {
2163 build_custom_error_page(
2164 &mut error_handler,
2165 error_response,
2166 head_for_error_handler,
2167 )
2168 .await
2169 }
2170 }
2171 }
2172 };
2173
2174 eprintln!(
2175 "Starting the server at http://{}",
2176 listener.local_addr().map_err(StartServerError)?
2177 );
2178
2179 if register_panic_hook {
2180 let current_hook = std::panic::take_hook();
2181 let new_hook = move |hook_info: &std::panic::PanicHookInfo<'_>| {
2182 current_hook(hook_info);
2183 error_page::error_page_panic_hook(hook_info);
2184 };
2185 std::panic::set_hook(Box::new(new_hook));
2186 }
2187 axum::serve(listener, handler.into_make_service())
2188 .with_graceful_shutdown(shutdown_signal)
2189 .await
2190 .map_err(StartServerError)?;
2191 if register_panic_hook {
2192 let _ = std::panic::take_hook();
2193 }
2194 #[cfg(feature = "db")]
2195 if let Some(database) = &context_cleanup.database {
2196 database.close().await?;
2197 }
2198
2199 Ok(())
2200}
2201
2202#[derive(Debug, Error)]
2203#[error("failed to start the server: {0}")]
2204pub(crate) struct StartServerError(#[from] pub(crate) std::io::Error);
2205
2206impl From<StartServerError> for Error {
2207 fn from(error: StartServerError) -> Self {
2208 Error::wrap(error)
2209 }
2210}
2211
2212fn accepts_html(head: Option<&RequestHead>) -> bool {
2213 head.and_then(|p| p.headers.get(http::header::ACCEPT))
2214 .is_some_and(|accept| {
2215 let value = accept.to_str().unwrap_or_default();
2216 let accept = AcceptHeaderParser::parse(value);
2217 // we check if the "Accept" header contains "text/html" explicitly
2218 // we ignore wildcards, such as "*/*", because they are
2219 // sent by tools like curl as well
2220 accept.contains_explicit(&mime::TEXT_HTML)
2221 })
2222}
2223
2224enum ErrorResponse {
2225 ErrorReturned(Error),
2226 Panic(Box<dyn std::any::Any + Send>),
2227}
2228
2229fn build_cot_error_page(
2230 error_response: ErrorResponse,
2231 diagnostics: &Diagnostics,
2232) -> axum::response::Response {
2233 match error_response {
2234 ErrorResponse::ErrorReturned(error) => {
2235 error_page::handle_response_error(&error, diagnostics)
2236 }
2237 ErrorResponse::Panic(error) => error_page::handle_response_panic(&error, diagnostics),
2238 }
2239}
2240
2241async fn build_custom_error_page(
2242 error_handler: &mut BoxCloneSyncService<Request, Response, Error>,
2243 error_response: ErrorResponse,
2244 mut request_head: RequestHead,
2245) -> axum::response::Response {
2246 let error = match error_response {
2247 ErrorResponse::ErrorReturned(error) => error,
2248 ErrorResponse::Panic(payload) => Error::from(UncaughtPanic::new(payload)),
2249 };
2250
2251 prepare_request_for_error_handler(&mut request_head, error);
2252
2253 let poll_status = poll_fn(|cx| error_handler.poll_ready(cx)).await;
2254 if let Err(error) = poll_status {
2255 error!(
2256 ?error,
2257 "Error occurred when polling the server error handler for readiness; \
2258 returning default error page"
2259 );
2260
2261 return error_page::build_cot_server_error_page();
2262 }
2263
2264 let request = Request::from_parts(request_head, Body::empty());
2265 let response = error_handler.call(request).await;
2266 response.map_or_else(
2267 |error| {
2268 error!(
2269 ?error,
2270 "Error occurred while running custom server error handler; \
2271 returning default error page"
2272 );
2273
2274 error_page::build_cot_server_error_page()
2275 },
2276 response_cot_to_axum,
2277 )
2278}
2279
2280pub(crate) fn prepare_request_for_error_handler(request_head: &mut RequestHead, error: Error) {
2281 request_head
2282 .extensions
2283 .insert(RequestOuterError::new(error));
2284}
2285
2286/// Runs the CLI for the given project.
2287///
2288/// This function takes a [`Project`] and runs the CLI for the project. You
2289/// typically don't need to call this function directly. Instead, you can use
2290/// [`cot::main`] which is a more ergonomic way to run the CLI.
2291///
2292/// # Errors
2293///
2294/// This function returns an error if the CLI command fails to execute.
2295///
2296/// # Examples
2297///
2298/// ```no_run
2299/// use cot::{App, Project, run_cli};
2300///
2301/// struct MyProject;
2302/// impl Project for MyProject {}
2303///
2304/// # #[tokio::main]
2305/// # async fn main() -> cot::Result<()> {
2306/// run_cli(MyProject).await?;
2307/// # Ok(())
2308/// # }
2309/// ```
2310#[expect(clippy::future_not_send)] // Send not needed; CLI is run async in a single thread
2311pub async fn run_cli(project: impl Project + Send + 'static) -> cot::Result<()> {
2312 Bootstrapper::new(project).run_cli().await
2313}
2314
2315fn request_parts_for_diagnostics(request: Request) -> (Option<RequestHead>, Request) {
2316 if request.project_config().debug {
2317 let (head, body) = request.into_parts();
2318 let parts_clone = head.clone();
2319 let request = Request::from_parts(head, body);
2320 (Some(parts_clone), request)
2321 } else {
2322 (None, request)
2323 }
2324}
2325
2326fn request_axum_to_cot(
2327 axum_request: axum::extract::Request,
2328 context: Arc<ProjectContext>,
2329) -> Request {
2330 let mut request = axum_request.map(Body::axum);
2331 prepare_request(&mut request, context);
2332 request
2333}
2334
2335pub(crate) fn prepare_request(request: &mut Request, context: Arc<ProjectContext>) {
2336 request.extensions_mut().insert(context);
2337}
2338
2339async fn pass_to_axum(
2340 request: Request,
2341 handler: &mut BoxedHandler,
2342) -> cot::Result<axum::response::Response> {
2343 poll_fn(|cx| handler.poll_ready(cx)).await?;
2344 let response = handler.call(request).await?;
2345
2346 Ok(response_cot_to_axum(response))
2347}
2348
2349fn response_cot_to_axum(response: Response) -> axum::response::Response {
2350 response.map(axum::body::Body::new)
2351}
2352
2353async fn shutdown_signal() {
2354 let ctrl_c = async {
2355 tokio::signal::ctrl_c()
2356 .await
2357 .expect("failed to install Ctrl+C handler");
2358 };
2359
2360 #[cfg(unix)]
2361 let terminate = async {
2362 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
2363 .expect("failed to install signal handler")
2364 .recv()
2365 .await;
2366 };
2367
2368 #[cfg(not(unix))]
2369 let terminate = std::future::pending::<()>();
2370
2371 tokio::select! {
2372 () = ctrl_c => {},
2373 () = terminate => {},
2374 }
2375}
2376
2377#[cfg(test)]
2378mod tests {
2379 use std::task::{Context, Poll};
2380
2381 use cot::cache;
2382 use tower::util::MapResultLayer;
2383 use tower::{ServiceExt, service_fn};
2384
2385 use super::*;
2386 use crate::StatusCode;
2387 use crate::auth::UserId;
2388 use crate::config::{SecretKey, Timeout};
2389 use crate::error::handler::{RequestError, RequestOuterError};
2390 use crate::html::Html;
2391 use crate::request::extractors::FromRequestHead;
2392 use crate::test::serial_guard;
2393
2394 struct TestApp;
2395
2396 impl App for TestApp {
2397 fn name(&self) -> &'static str {
2398 "mock"
2399 }
2400 }
2401
2402 #[cot::test]
2403 async fn app_default_impl() {
2404 let app = TestApp {};
2405 assert_eq!(app.name(), "mock");
2406 assert_eq!(app.router().routes().len(), 0);
2407 assert_eq!(app.migrations().len(), 0);
2408 }
2409
2410 struct TestProject;
2411 impl Project for TestProject {}
2412
2413 #[test]
2414 fn project_default_cli_metadata() {
2415 let metadata = TestProject.cli_metadata();
2416
2417 assert_eq!(metadata.name, "cot");
2418 assert_eq!(metadata.version, env!("CARGO_PKG_VERSION"));
2419 assert_eq!(metadata.authors, env!("CARGO_PKG_AUTHORS"));
2420 assert_eq!(metadata.description, env!("CARGO_PKG_DESCRIPTION"));
2421 }
2422
2423 #[cot::test]
2424 async fn root_handler_builder_middleware() {
2425 // we create a root handler that returns an error for all requests
2426 // then we apply a middleware that returns a fixed "ok" response
2427 let root_handler_builder = RootHandlerBuilder {
2428 handler: service_fn(|_: Request| async move {
2429 Err::<Response, _>(Error::internal("error"))
2430 }),
2431 error_handler: service_fn(|_: Request| async move {
2432 Err::<Response, _>(Error::internal("error"))
2433 }),
2434 };
2435
2436 let root_handler = root_handler_builder
2437 .middleware(make_ok_middleware())
2438 .build();
2439
2440 expect_handler_ok(root_handler.handler).await;
2441 expect_handler_ok(root_handler.error_handler).await;
2442 }
2443
2444 #[cot::test]
2445 async fn root_handler_builder_main_handler_middleware() {
2446 let root_handler_builder = RootHandlerBuilder {
2447 handler: service_fn(|_: Request| async move {
2448 Err::<Response, _>(Error::internal("error"))
2449 }),
2450 error_handler: service_fn(|_: Request| async move {
2451 Err::<Response, _>(Error::internal("error"))
2452 }),
2453 };
2454
2455 let root_handler = root_handler_builder
2456 .main_handler_middleware(make_ok_middleware())
2457 .build();
2458
2459 expect_handler_ok(root_handler.handler).await;
2460 expect_handler_error(root_handler.error_handler).await;
2461 }
2462
2463 #[cot::test]
2464 async fn root_handler_builder_error_handler_middleware() {
2465 let root_handler_builder = RootHandlerBuilder {
2466 handler: service_fn(|_: Request| async move {
2467 Err::<Response, _>(Error::internal("error"))
2468 }),
2469 error_handler: service_fn(|_: Request| async move {
2470 Err::<Response, _>(Error::internal("error"))
2471 }),
2472 };
2473
2474 let root_handler = root_handler_builder
2475 .error_handler_middleware(make_ok_middleware())
2476 .build();
2477
2478 expect_handler_error(root_handler.handler).await;
2479 expect_handler_ok(root_handler.error_handler).await;
2480 }
2481
2482 #[expect(clippy::type_complexity, reason = "test function")]
2483 fn make_ok_middleware() -> MapResultLayer<fn(Result<Response, Error>) -> Result<Response, Error>>
2484 {
2485 MapResultLayer::new(|_| Ok(Response::new(Body::fixed("Hello, world!"))))
2486 }
2487
2488 async fn expect_handler_ok(handler: BoxedHandler) {
2489 let response = handler.oneshot(Request::default()).await;
2490 assert!(response.is_ok());
2491 assert_eq!(response.unwrap().status(), StatusCode::OK);
2492 }
2493
2494 async fn expect_handler_error(handler: BoxedHandler) {
2495 let response = handler.oneshot(Request::default()).await;
2496 assert!(response.is_err());
2497 }
2498
2499 #[cfg(feature = "live-reload")]
2500 #[cot::test]
2501 async fn project_middlewares() {
2502 struct TestProject;
2503 impl Project for TestProject {
2504 fn config(&self, _config_name: &str) -> cot::Result<ProjectConfig> {
2505 Ok(ProjectConfig::default())
2506 }
2507
2508 fn middlewares(
2509 &self,
2510 handler: RootHandlerBuilder,
2511 context: &MiddlewareContext,
2512 ) -> RootHandler {
2513 handler
2514 .middleware(crate::static_files::StaticFilesMiddleware::from_context(
2515 context,
2516 ))
2517 .middleware(crate::middleware::LiveReloadMiddleware::from_context(
2518 context,
2519 ))
2520 .build()
2521 }
2522 }
2523
2524 let response = crate::test::Client::new(TestProject)
2525 .await
2526 .get("/")
2527 .await
2528 .unwrap();
2529 assert_eq!(response.status(), StatusCode::NOT_FOUND);
2530 }
2531
2532 #[test]
2533 fn project_default_config() {
2534 let temp_dir = tempfile::tempdir().unwrap();
2535
2536 let config_dir = temp_dir.path().join("config");
2537 std::fs::create_dir(&config_dir).unwrap();
2538 let config = r#"
2539 debug = false
2540 secret_key = "123abc"
2541 "#;
2542
2543 let config_file_path = config_dir.as_path().join("dev.toml");
2544 std::fs::write(config_file_path, config).unwrap();
2545
2546 // ensure the tests run sequentially when setting the current directory
2547 let _guard = serial_guard();
2548
2549 std::env::set_current_dir(&temp_dir).unwrap();
2550 let config = TestProject.config("dev").unwrap();
2551
2552 assert!(!config.debug);
2553 assert_eq!(config.secret_key, SecretKey::from("123abc".to_string()));
2554 }
2555
2556 #[test]
2557 fn project_default_register_apps() {
2558 let mut apps = AppBuilder::new();
2559 let context = ProjectContext::new().with_config(ProjectConfig::default());
2560
2561 TestProject.register_apps(&mut apps, &context);
2562
2563 assert!(apps.apps.is_empty());
2564 }
2565
2566 #[cot::test]
2567 async fn default_auth_backend() {
2568 let cache_memory = Cache::new(
2569 cache::store::memory::Memory::new(),
2570 None,
2571 Timeout::default(),
2572 );
2573
2574 let context = ProjectContext::new()
2575 .with_config(
2576 ProjectConfig::builder()
2577 .auth_backend(AuthBackendConfig::None)
2578 .build(),
2579 )
2580 .with_apps(vec![], Arc::new(Router::empty()))
2581 .with_database(None)
2582 .with_cache(cache_memory);
2583
2584 let auth_backend = TestProject.auth_backend(&context);
2585 assert!(
2586 auth_backend
2587 .get_by_id(UserId::Int(0))
2588 .await
2589 .unwrap()
2590 .is_none()
2591 );
2592 }
2593
2594 #[cot::test]
2595 #[cfg_attr(
2596 miri,
2597 ignore = "unsupported operation: can't call foreign function `sqlite3_open_v2`"
2598 )]
2599 async fn bootstrapper() {
2600 struct TestProject;
2601 impl Project for TestProject {
2602 fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) {
2603 apps.register_with_views(TestApp {}, "/app");
2604 }
2605 }
2606
2607 let bootstrapper = Bootstrapper::new(TestProject)
2608 .with_config(ProjectConfig::default())
2609 .boot()
2610 .await
2611 .unwrap();
2612
2613 assert_eq!(bootstrapper.context().apps.len(), 1);
2614 assert_eq!(bootstrapper.context().router.routes().len(), 1);
2615 }
2616
2617 #[cot::test]
2618 async fn build_custom_error_page_poll_ready_failure() {
2619 #[derive(Clone)]
2620 struct TestService;
2621 impl Service<Request> for TestService {
2622 type Response = Response;
2623 type Error = Error;
2624 type Future = std::future::Ready<crate::Result<Response>>;
2625
2626 fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
2627 Poll::Ready(Err(Error::internal("Poll ready failed")))
2628 }
2629
2630 fn call(&mut self, _req: Request) -> Self::Future {
2631 std::future::ready(Ok(Response::default()))
2632 }
2633 }
2634
2635 let mut error_handler = BoxCloneSyncService::new(TestService);
2636
2637 let panic_payload = Box::new("Test panic message".to_string());
2638 let error_response = ErrorResponse::Panic(panic_payload);
2639
2640 let (request_head, _) = Request::new(Body::empty()).into_parts();
2641 let response =
2642 build_custom_error_page(&mut error_handler, error_response, request_head).await;
2643
2644 test_last_resort_error(response).await;
2645 }
2646
2647 #[cot::test]
2648 async fn build_custom_error_page_call_failure() {
2649 let mock_handler = service_fn(|_request: Request| async {
2650 Err::<Response, Error>(Error::internal("handler call failed"))
2651 });
2652
2653 let mut error_handler = BoxCloneSyncService::new(mock_handler);
2654
2655 let error = Error::internal("Test error");
2656 let error_response = ErrorResponse::ErrorReturned(error);
2657
2658 let (request_head, _) = Request::new(Body::empty()).into_parts();
2659 let response =
2660 build_custom_error_page(&mut error_handler, error_response, request_head).await;
2661
2662 test_last_resort_error(response).await;
2663 }
2664 #[cot::test]
2665 async fn build_custom_error_page_success() {
2666 // Create a mock error handler that succeeds
2667 let mock_handler = service_fn(|_request: Request| async {
2668 let html = Html::new("Custom error page content")
2669 .with_status(StatusCode::INTERNAL_SERVER_ERROR);
2670 Ok::<Response, Error>(html.into_response().unwrap())
2671 });
2672
2673 let mut error_handler = BoxCloneSyncService::new(mock_handler);
2674
2675 let error = Error::internal("Test error");
2676 let error_response = ErrorResponse::ErrorReturned(error);
2677
2678 let (request_head, _) = Request::new(Body::empty()).into_parts();
2679 let response =
2680 build_custom_error_page(&mut error_handler, error_response, request_head).await;
2681
2682 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
2683 let body_string = axum_response_into_body(response).await;
2684 assert!(body_string.contains("Custom error page content"));
2685 }
2686
2687 #[cot::test]
2688 async fn build_custom_error_page_panic_conversion() {
2689 let mock_handler = service_fn(|request: Request| async {
2690 let (parts, _body) = request.into_parts();
2691 let error = RequestError::from_request_head(&parts).await.unwrap();
2692 assert!(error.is::<UncaughtPanic>());
2693
2694 let html = Html::new("Panic error handled");
2695 Ok::<Response, Error>(html.into_response().unwrap())
2696 });
2697
2698 let mut error_handler = BoxCloneSyncService::new(mock_handler);
2699
2700 let panic_payload = Box::new("Test panic message".to_string());
2701 let error_response = ErrorResponse::Panic(panic_payload);
2702
2703 let (request_head, _) = Request::new(Body::empty()).into_parts();
2704 let response =
2705 build_custom_error_page(&mut error_handler, error_response, request_head).await;
2706
2707 assert_eq!(response.status(), StatusCode::OK);
2708 let body_string = axum_response_into_body(response).await;
2709 assert!(body_string.contains("Panic error handled"));
2710 }
2711
2712 /// Test that last-resort error page (500.html) is returned
2713 async fn test_last_resort_error(response: axum::response::Response) {
2714 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
2715
2716 let body_string = axum_response_into_body(response).await;
2717
2718 assert!(body_string.contains("Server Error"));
2719 assert!(
2720 body_string.contains("Sorry, the page you are looking for is currently unavailable")
2721 );
2722 }
2723
2724 async fn axum_response_into_body(response: axum::response::Response) -> String {
2725 let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
2726 .await
2727 .unwrap();
2728 let body_string = String::from_utf8_lossy(&body_bytes);
2729 body_string.into_owned()
2730 }
2731
2732 #[cot::test]
2733 async fn build_request_for_error_handler() {
2734 let error = Error::internal("test error");
2735
2736 let (mut head, _) = Request::new(Body::empty()).into_parts();
2737 prepare_request_for_error_handler(&mut head, error);
2738
2739 let request_error = head.extensions.get::<RequestOuterError>().unwrap();
2740 assert_eq!(request_error.to_string(), "test error");
2741 }
2742}