Skip to content

Commit

Permalink
Add custom error type using thiserror
Browse files Browse the repository at this point in the history
- Add a custom error type using `thiserror` and replace uses of
  `anyhow` throughout the code with the error.
- Add a type alias (`RoadsterResult`) to make it easy to use our custom
  error type.
- Add an `HttpError` type to help with creating errors with http status
  codes
- Implement traits to allow returning the errors as responses in HTTP
  APIs defined with axum and/or aide

Closes #8
  • Loading branch information
spencewenski committed May 26, 2024
1 parent 78ddddc commit 27d5607
Show file tree
Hide file tree
Showing 56 changed files with 774 additions and 210 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ num-traits = "0.2.19"
log = "0.4.21"
futures = "0.3.30"
validator = { version = "0.18.1", features = ["derive"] }
thiserror = "1.0.61"

[dev-dependencies]
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
Expand Down
5 changes: 3 additions & 2 deletions examples/minimal/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use async_trait::async_trait;
use migration::Migrator;
use roadster::app::App as RoadsterApp;
use roadster::app_context::AppContext;
use roadster::error::RoadsterResult;
use roadster::service::http::service::HttpService;
use roadster::service::registry::ServiceRegistry;
use roadster::service::worker::sidekiq::app_worker::AppWorker;
Expand All @@ -23,14 +24,14 @@ impl RoadsterApp for App {
type Cli = AppCli;
type M = Migrator;

async fn with_state(_context: &AppContext) -> anyhow::Result<Self::State> {
async fn with_state(_context: &AppContext) -> RoadsterResult<Self::State> {
Ok(())
}

async fn services(
registry: &mut ServiceRegistry<Self>,
context: &AppContext<Self::State>,
) -> anyhow::Result<()> {
) -> RoadsterResult<()> {
registry
.register_builder(
HttpService::builder(Some(BASE), context).api_router(controller::routes(BASE)),
Expand Down
5 changes: 3 additions & 2 deletions examples/minimal/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::{Parser, Subcommand};
use roadster::app_context::AppContext;

use roadster::cli::RunCommand;
use roadster::error::RoadsterResult;

use crate::app::App;
use crate::app_state::CustomAppContext;
Expand All @@ -24,7 +25,7 @@ impl RunCommand<App> for AppCli {
app: &App,
cli: &AppCli,
context: &AppContext<CustomAppContext>,
) -> anyhow::Result<bool> {
) -> RoadsterResult<bool> {
if let Some(command) = self.command.as_ref() {
command.run(app, cli, context).await
} else {
Expand All @@ -46,7 +47,7 @@ impl RunCommand<App> for AppCommand {
_app: &App,
_cli: &AppCli,
_context: &AppContext<CustomAppContext>,
) -> anyhow::Result<bool> {
) -> RoadsterResult<bool> {
Ok(false)
}
}
6 changes: 3 additions & 3 deletions examples/minimal/src/controller/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use aide::axum::ApiRouter;
use aide::transform::TransformOperation;
use axum::extract::State;
use axum::Json;
use roadster::controller::http::build_path;
use roadster::api::http::build_path;
use roadster::error::RoadsterResult;
use roadster::service::worker::sidekiq::app_worker::AppWorker;
use roadster::view::http::app_error::AppError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::instrument;
Expand All @@ -26,7 +26,7 @@ pub fn routes(parent: &str) -> ApiRouter<AppState> {
pub struct ExampleResponse {}

#[instrument(skip_all)]
async fn example_get(State(state): State<AppState>) -> Result<Json<ExampleResponse>, AppError> {
async fn example_get(State(state): State<AppState>) -> RoadsterResult<Json<ExampleResponse>> {
ExampleWorker::enqueue(&state, "Example".to_string()).await?;
Ok(Json(ExampleResponse {}))
}
Expand Down
3 changes: 2 additions & 1 deletion examples/minimal/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use minimal::app::App;
use roadster::app;
use roadster::error::RoadsterResult;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
async fn main() -> RoadsterResult<()> {
app::run(App).await?;

Ok(())
Expand Down
Empty file removed foo
Empty file.
10 changes: 4 additions & 6 deletions src/controller/http/docs.rs → src/api/http/docs.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::api::http::build_path;
use crate::app_context::AppContext;
use crate::controller::http::build_path;
use aide::axum::routing::get_with;
use aide::axum::{ApiRouter, IntoApiResponse};
use aide::openapi::OpenApi;
Expand All @@ -10,16 +10,14 @@ use axum::{Extension, Json};
use std::ops::Deref;
use std::sync::Arc;

const BASE: &str = "_docs";
const TAG: &str = "Docs";

/// This API is only available when using Aide.
pub fn routes<S>(parent: &str, context: &AppContext<S>) -> ApiRouter<AppContext<S>>
where
S: Clone + Send + Sync + 'static,
{
let parent = build_path(parent, BASE);
let open_api_schema_path = build_path(&parent, api_schema_route(context));
let open_api_schema_path = build_path(parent, api_schema_route(context));

let router = ApiRouter::new();
if !api_schema_enabled(context) {
Expand All @@ -33,7 +31,7 @@ where

let router = if scalar_enabled(context) {
router.api_route_with(
&build_path(&parent, scalar_route(context)),
&build_path(parent, scalar_route(context)),
get_with(
Scalar::new(&open_api_schema_path)
.with_title(&context.config().app.name)
Expand All @@ -48,7 +46,7 @@ where

let router = if redoc_enabled(context) {
router.api_route_with(
&build_path(&parent, redoc_route(context)),
&build_path(parent, redoc_route(context)),
get_with(
Redoc::new(&open_api_schema_path)
.with_title(&context.config().app.name)
Expand Down
14 changes: 7 additions & 7 deletions src/controller/http/health.rs → src/api/http/health.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use crate::api::http::build_path;
use crate::app_context::AppContext;
use crate::controller::http::build_path;
use crate::view::http::app_error::AppError;
use crate::error::RoadsterResult;
#[cfg(feature = "open-api")]
use aide::axum::routing::get_with;
#[cfg(feature = "open-api")]
use aide::axum::ApiRouter;
#[cfg(feature = "open-api")]
use aide::transform::TransformOperation;
#[cfg(feature = "sidekiq")]
use anyhow::bail;
use anyhow::anyhow;
#[cfg(any(feature = "sidekiq", feature = "db-sql"))]
use axum::extract::State;
use axum::routing::get;
Expand Down Expand Up @@ -122,7 +122,7 @@ pub enum Status {
#[instrument(skip_all)]
async fn health_get<S>(
#[cfg(any(feature = "sidekiq", feature = "db-sql"))] State(state): State<AppContext<S>>,
) -> Result<Json<HeathCheckResponse>, AppError>
) -> RoadsterResult<Json<HeathCheckResponse>>
where
S: Clone + Send + Sync + 'static,
{
Expand Down Expand Up @@ -167,7 +167,7 @@ where

#[cfg(feature = "db-sql")]
#[instrument(skip_all)]
async fn ping_db(db: &DatabaseConnection) -> anyhow::Result<()> {
async fn ping_db(db: &DatabaseConnection) -> RoadsterResult<()> {
db.ping().await?;
Ok(())
}
Expand All @@ -191,7 +191,7 @@ async fn redis_health(redis: &sidekiq::RedisPool) -> ResourceHealth {

#[cfg(feature = "sidekiq")]
#[instrument(skip_all)]
async fn ping_redis(redis: &sidekiq::RedisPool) -> anyhow::Result<(Duration, Duration)> {
async fn ping_redis(redis: &sidekiq::RedisPool) -> RoadsterResult<(Duration, Duration)> {
let timer = Instant::now();
let mut conn = timeout(Duration::from_secs(1), redis.get()).await??;
let acquire_conn_latency = timer.elapsed();
Expand All @@ -207,7 +207,7 @@ async fn ping_redis(redis: &sidekiq::RedisPool) -> anyhow::Result<(Duration, Dur
if pong == msg {
Ok((acquire_conn_latency, ping_latency))
} else {
bail!("Ping response does not match input.")
Err(anyhow!("Ping response does not match input.").into())
}
}

Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions src/controller/http/ping.rs → src/api/http/ping.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::api::http::build_path;
use crate::app_context::AppContext;
use crate::controller::http::build_path;
use crate::view::http::app_error::AppError;
use crate::error::RoadsterResult;
#[cfg(feature = "open-api")]
use aide::axum::routing::get_with;
#[cfg(feature = "open-api")]
Expand Down Expand Up @@ -71,7 +71,7 @@ fn route<S>(context: &AppContext<S>) -> &str {
pub struct PingResponse {}

#[instrument(skip_all)]
async fn ping_get() -> Result<Json<PingResponse>, AppError> {
async fn ping_get() -> RoadsterResult<Json<PingResponse>> {
Ok(Json(PingResponse::default()))
}

Expand Down
File renamed without changes.
19 changes: 11 additions & 8 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::cli::RunCommand;
use crate::config::app_config::AppConfig;
#[cfg(not(feature = "cli"))]
use crate::config::environment::Environment;
use crate::error::RoadsterResult;
use crate::service::registry::ServiceRegistry;
use crate::tracing::init_tracing;
use async_trait::async_trait;
Expand All @@ -26,7 +27,7 @@ use tracing::{instrument, warn};
pub async fn run<A>(
// This parameter is (currently) not used when no features are enabled.
#[allow(unused_variables)] app: A,
) -> anyhow::Result<()>
) -> RoadsterResult<()>
where
A: App + Default + Send + Sync + 'static,
{
Expand All @@ -47,10 +48,12 @@ where
#[cfg(feature = "cli")]
config.validate(!roadster_cli.skip_validate_config)?;

// The `config.clone()` here is technically not necessary. However, without it, RustRover
// is giving a "value used after move" error when creating an actual `AppContext` below.
#[cfg(test)]
let context = AppContext::<()>::test(Some(config.clone()), None)?;
#[cfg(not(test))]
let context = AppContext::<()>::new::<A>(config).await?;
#[cfg(test)]
let context = AppContext::<()>::test(Some(config), None)?;

let state = A::with_state(&context).await?;
let context = context.with_custom(state);
Expand Down Expand Up @@ -95,14 +98,14 @@ pub trait App: Send + Sync {
#[cfg(feature = "db-sql")]
type M: MigratorTrait;

fn init_tracing(config: &AppConfig) -> anyhow::Result<()> {
fn init_tracing(config: &AppConfig) -> RoadsterResult<()> {
init_tracing(config)?;

Ok(())
}

#[cfg(feature = "db-sql")]
fn db_connection_options(config: &AppConfig) -> anyhow::Result<ConnectOptions> {
fn db_connection_options(config: &AppConfig) -> RoadsterResult<ConnectOptions> {
let mut options = ConnectOptions::new(config.database.uri.to_string());
options
.connect_timeout(config.database.connect_timeout)
Expand All @@ -124,13 +127,13 @@ pub trait App: Send + Sync {
/// method is provided in case there's any additional work that needs to be done that the
/// consumer can't put in a [`From<AppContext>`] implementation. For example, any
/// configuration that needs to happen in an async method.
async fn with_state(context: &AppContext<()>) -> anyhow::Result<Self::State>;
async fn with_state(context: &AppContext<()>) -> RoadsterResult<Self::State>;

/// Provide the services to run in the app.
async fn services(
_registry: &mut ServiceRegistry<Self>,
_context: &AppContext<Self::State>,
) -> anyhow::Result<()> {
) -> RoadsterResult<()> {
Ok(())
}

Expand All @@ -144,7 +147,7 @@ pub trait App: Send + Sync {
/// Override to provide custom graceful shutdown logic to clean up any resources created by
/// the app. Roadster will take care of cleaning up the resources it created.
#[instrument(skip_all)]
async fn graceful_shutdown(_context: &AppContext<Self::State>) -> anyhow::Result<()> {
async fn graceful_shutdown(_context: &AppContext<Self::State>) -> RoadsterResult<()> {
Ok(())
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/app_context.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::app::App;
use crate::config::app_config::AppConfig;
use crate::error::RoadsterResult;
#[cfg(feature = "db-sql")]
use sea_orm::DatabaseConnection;
use std::sync::Arc;
Expand All @@ -20,7 +21,7 @@ impl<T> AppContext<T> {
#[cfg_attr(test, allow(dead_code))]
// The `A` type parameter isn't used in some feature configurations
#[allow(clippy::extra_unused_type_parameters)]
pub(crate) async fn new<A>(config: AppConfig) -> anyhow::Result<AppContext<()>>
pub(crate) async fn new<A>(config: AppConfig) -> RoadsterResult<AppContext<()>>
where
A: App,
{
Expand Down Expand Up @@ -90,7 +91,7 @@ impl<T> AppContext<T> {
config: Option<AppConfig>,
#[cfg(not(feature = "sidekiq"))] _redis: Option<()>,
#[cfg(feature = "sidekiq")] redis: Option<sidekiq::RedisPool>,
) -> anyhow::Result<AppContext<()>> {
) -> RoadsterResult<AppContext<()>> {
let mut inner = MockAppContextInner::default();
inner
.expect_config()
Expand Down
9 changes: 5 additions & 4 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::app::App;
use crate::app::MockApp;
use crate::app_context::AppContext;
use crate::cli::roadster::{RoadsterCli, RunRoadsterCommand};
use crate::error::RoadsterResult;
use async_trait::async_trait;
use clap::{Args, Command, FromArgMatches};
use std::ffi::OsString;
Expand All @@ -29,10 +30,10 @@ where
app: &A,
cli: &A::Cli,
context: &AppContext<A::State>,
) -> anyhow::Result<bool>;
) -> RoadsterResult<bool>;
}

pub(crate) fn parse_cli<A, I, T>(args: I) -> anyhow::Result<(RoadsterCli, A::Cli)>
pub(crate) fn parse_cli<A, I, T>(args: I) -> RoadsterResult<(RoadsterCli, A::Cli)>
where
A: App,
I: IntoIterator<Item = T>,
Expand Down Expand Up @@ -82,7 +83,7 @@ pub(crate) async fn handle_cli<A>(
roadster_cli: &RoadsterCli,
app_cli: &A::Cli,
context: &AppContext<A::State>,
) -> anyhow::Result<bool>
) -> RoadsterResult<bool>
where
A: App,
{
Expand All @@ -106,7 +107,7 @@ mockall::mock! {
app: &MockApp,
cli: &<MockApp as App>::Cli,
context: &AppContext<<MockApp as App>::State>,
) -> anyhow::Result<bool>;
) -> RoadsterResult<bool>;
}

impl clap::FromArgMatches for Cli {
Expand Down
9 changes: 5 additions & 4 deletions src/cli/roadster/migrate.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::bail;
use anyhow::anyhow;
use async_trait::async_trait;
use clap::{Parser, Subcommand};
use sea_orm_migration::MigratorTrait;
Expand All @@ -8,6 +8,7 @@ use tracing::warn;
use crate::app::App;
use crate::app_context::AppContext;
use crate::cli::roadster::{RoadsterCli, RunRoadsterCommand};
use crate::error::RoadsterResult;

#[derive(Debug, Parser, Serialize)]
pub struct MigrateArgs {
Expand All @@ -25,7 +26,7 @@ where
app: &A,
cli: &RoadsterCli,
context: &AppContext<A::State>,
) -> anyhow::Result<bool> {
) -> RoadsterResult<bool> {
self.command.run(app, cli, context).await
}
}
Expand Down Expand Up @@ -57,9 +58,9 @@ where
_app: &A,
cli: &RoadsterCli,
context: &AppContext<A::State>,
) -> anyhow::Result<bool> {
) -> RoadsterResult<bool> {
if is_destructive(self) && !cli.allow_dangerous(context) {
bail!("Running destructive command `{:?}` is not allowed in environment `{:?}`. To override, provide the `--allow-dangerous` CLI arg.", self, context.config().environment);
return Err(anyhow!("Running destructive command `{:?}` is not allowed in environment `{:?}`. To override, provide the `--allow-dangerous` CLI arg.", self, context.config().environment).into());
} else if is_destructive(self) {
warn!(
"Running destructive command `{:?}` in environment `{:?}`",
Expand Down
Loading

0 comments on commit 27d5607

Please sign in to comment.