Skip to content

Commit

Permalink
Set up roadster CLI and custom app CLI
Browse files Browse the repository at this point in the history
  • Loading branch information
spencewenski committed Apr 7, 2024
1 parent b087812 commit 6b2fa49
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 54 deletions.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[features]
default = ["sidekiq", "db-sql", "open-api", "jwt-ietf"]
default = ["sidekiq", "db-sql", "open-api", "jwt-ietf", "cli"]
sidekiq = ["dep:rusty-sidekiq", "dep:bb8", "dep:num_cpus"]
db-sql = ["dep:sea-orm", "dep:sea-orm-migration"]
open-api = ["dep:aide", "dep:schemars"]
jwt = ["dep:jsonwebtoken"]
jwt-ietf = ["jwt"]
jwt-openid = ["jwt"]
cli = ["dep:clap"]

[dependencies]
# Config
Expand Down Expand Up @@ -57,6 +58,9 @@ async-trait = "0.1.77"
# Auth
jsonwebtoken = { version = "9.3.0", optional = true }

# CLI
clap = { version = "4.5.4", features = ["derive", "string"], optional = true }

# Others
anyhow = "1.0.81"
serde = "1.0.197"
Expand All @@ -74,6 +78,7 @@ futures-core = "0.3.30"
chrono = { version = "0.4.35", features = ["serde"] }
byte-unit = { version = "5.1.4", features = ["serde"] }
convert_case = "0.6.0"
const_format = "0.2.32"

[dev-dependencies]
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
Expand Down
1 change: 1 addition & 0 deletions examples/minimal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ axum = "0.7.5"
# DB
entity = { path = "entity" }
migration = { path = "migration" }
clap = { version = "4.5.4", features = ["derive"] }

[dev-dependencies]
cargo-husky = { version = "1.5.0", features = ["default", "run-cargo-check", "run-cargo-clippy", "run-cargo-fmt", "run-cargo-test"] }
Expand Down
4 changes: 4 additions & 0 deletions examples/minimal/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
use aide::axum::ApiRouter;
use migration::Migrator;
use roadster::app::App as RoadsterApp;
use roadster::config::app_config::AppConfig;
use roadster::controller::default_routes;

use crate::app_state::AppState;
use crate::cli::AppCli;

const BASE: &str = "/api";

#[derive(Default)]
pub struct App;
impl RoadsterApp for App {
type State = AppState;
type Cli = AppCli;
type M = Migrator;

fn router(config: &AppConfig) -> ApiRouter<Self::State> {
default_routes(BASE, config)
Expand Down
53 changes: 53 additions & 0 deletions examples/minimal/src/cli/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use async_trait::async_trait;
use clap::{Parser, Subcommand};

use roadster::app_context::AppContext;
use roadster::cli::RunCommand;

use crate::app_state::AppState;

/// Custom version of [RunCommand] that removes the `C` and `S` generics because we know what they
/// are so we don't need to provide them every time we want to implement a command.
#[async_trait]
pub trait RunAppCommand {
async fn run(&self, cli: &AppCli, state: &AppState) -> anyhow::Result<bool>;
}

/// Minimal Example: Commands specific to managing the `minimal` app are provided in the CLI
/// as well. Subcommands not listed under the `roadster` subcommand are specific to `minimal`.
#[derive(Debug, Parser)]
#[command(version, about)]
pub struct AppCli {
#[command(subcommand)]
pub command: Option<AppCommand>,
}

#[async_trait]
impl RunCommand<AppCli, AppState> for AppCli {
#[allow(clippy::disallowed_types)]
async fn run(
&self,
cli: &AppCli,
_context: &AppContext,
state: &AppState,
) -> anyhow::Result<bool> {
if let Some(command) = self.command.as_ref() {
command.run(cli, state).await
} else {
Ok(false)
}
}
}

/// App specific subcommands
///
/// Note: This doc comment doesn't appear in the CLI `--help` message.
#[derive(Debug, Subcommand)]
pub enum AppCommand {}

#[async_trait]
impl RunAppCommand for AppCommand {
async fn run(&self, _cli: &AppCli, _state: &AppState) -> anyhow::Result<bool> {
Ok(false)
}
}
1 change: 1 addition & 0 deletions examples/minimal/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod app;
pub mod app_state;
pub mod cli;
3 changes: 1 addition & 2 deletions examples/minimal/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
use migration::Migrator;
use minimal::app::App;
use roadster::app;

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

Ok(())
}
112 changes: 72 additions & 40 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,29 @@ use aide::axum::ApiRouter;
use aide::openapi::OpenApi;
#[cfg(feature = "open-api")]
use aide::transform::TransformOpenApi;

use async_trait::async_trait;
#[cfg(feature = "open-api")]
use axum::Extension;
use axum::Router;
#[cfg(feature = "cli")]
use clap::{Args, Command, FromArgMatches};
use itertools::Itertools;
#[cfg(feature = "db-sql")]
use sea_orm::DatabaseConnection;
#[cfg(feature = "db-sql")]
use sea_orm::{ConnectOptions, Database};
#[cfg(feature = "db-sql")]
use sea_orm_migration::MigratorTrait;
#[cfg(feature = "sidekiq")]
use sidekiq::{periodic, Processor};
use tokio::task::JoinSet;
use tokio_util::sync::CancellationToken;

use tracing::{debug, error, info, instrument};

use crate::app_context::AppContext;
#[cfg(feature = "cli")]
use crate::cli::{RoadsterCli, RunCommand};
use crate::config::app_config::AppConfig;
#[cfg(not(feature = "cli"))]
use crate::config::environment::Environment;
use crate::controller::middleware::default::default_middleware;
use crate::controller::middleware::Middleware;
use crate::initializer::default::default_initializers;
Expand All @@ -37,55 +39,71 @@ use crate::tracing::init_tracing;
#[cfg(feature = "sidekiq")]
use crate::worker::queue_names;

#[cfg(not(feature = "db-sql"))]
// todo: this method is getting unweildy, we should break it up
pub async fn start<A>() -> anyhow::Result<()>
where
A: App + Default + Send + Sync + 'static,
{
let config = get_app_config::<A>()?;
run_app::<A>(config).await
}
#[cfg(feature = "cli")]
let (roadster_cli, app_cli) = {
// Build the CLI by augmenting a default Command with both the roadster and app-specific CLIs
let cli = Command::default();
// Add the roadster CLI. Save the shared attributes to use after adding the app-specific CLI
let cli = RoadsterCli::augment_args(cli);
let about = cli.get_about().cloned();
let long_about = cli.get_long_about().cloned();
let version = cli.get_version().map(|x| x.to_string());
let long_version = cli.get_long_version().map(|x| x.to_string());
// Add the app-specific CLI. This will override the shared attributes, so we need to
// combine them with the roadster CLI attributes.
let cli = A::Cli::augment_args(cli);
let cli = if let Some((a, b)) = about.zip(cli.get_about().cloned()) {
cli.about(format!("roadster: {a}, app: {b}"))
} else {
cli
};
let cli = if let Some((a, b)) = long_about.zip(cli.get_long_about().cloned()) {
cli.long_about(format!("roadster: {a}, app: {b}"))
} else {
cli
};
let cli = if let Some((a, b)) = version.zip(cli.get_version().map(|x| x.to_string())) {
cli.version(format!("roadster: {a}, app: {b}"))
} else {
cli
};
let cli =
if let Some((a, b)) = long_version.zip(cli.get_long_version().map(|x| x.to_string())) {
cli.long_version(format!("roadster: {a}\n\napp: {b}"))
} else {
cli
};
// Build each CLI from the CLI args
let matches = cli.get_matches();
let roadster_cli = RoadsterCli::from_arg_matches(&matches)?;
let app_cli = A::Cli::from_arg_matches(&matches)?;
(roadster_cli, app_cli)
};

#[cfg(feature = "db-sql")]
pub async fn start<A, M>() -> anyhow::Result<()>
where
A: App + Default + Send + Sync + 'static,
M: MigratorTrait,
{
let config = get_app_config::<A>()?;
#[cfg(feature = "cli")]
let environment = roadster_cli.environment.clone();
#[cfg(not(feature = "cli"))]
let environment: Option<Environment> = None;

let config = AppConfig::new(environment)?;

A::init_tracing(&config)?;

debug!("{config:?}");

#[cfg(feature = "db-sql")]
let db = Database::connect(A::db_connection_options(&config)?).await?;
// Todo: enable manual migrations
#[cfg(feature = "db-sql")]
if config.database.auto_migrate {
M::up(&db, None).await?;
A::M::up(&db, None).await?;
}

run_app::<A>(config, db).await
}

fn get_app_config<A>() -> anyhow::Result<AppConfig>
where
A: App + Default + Send + Sync + 'static,
{
let config = AppConfig::new()?;

A::init_tracing(&config)?;

debug!("{config:?}");

Ok(config)
}

// todo: this method is getting unweildy, we should break it up
async fn run_app<A>(
config: AppConfig,
#[cfg(feature = "db-sql")] db: DatabaseConnection,
) -> anyhow::Result<()>
where
A: App + Default + Send + Sync + 'static,
{
#[cfg(feature = "sidekiq")]
let redis = {
let redis_config = &config.worker.sidekiq.redis;
Expand Down Expand Up @@ -123,6 +141,16 @@ where
let router = router.with_state::<()>(state.clone());
let state = Arc::new(state);

#[cfg(feature = "cli")]
{
if roadster_cli.run(&roadster_cli, &context, &state).await? {
return Ok(());
}
if app_cli.run(&app_cli, &context, &state).await? {
return Ok(());
}
}

let initializers = default_initializers()
.into_iter()
.chain(A::initializers(&context))
Expand Down Expand Up @@ -263,6 +291,10 @@ where
#[async_trait]
pub trait App {
type State: From<Arc<AppContext>> + Into<Arc<AppContext>> + Clone + Send + Sync + 'static;
#[cfg(feature = "cli")]
type Cli: clap::Args + RunCommand<Self::Cli, Self::State>;
#[cfg(feature = "db-sql")]
type M: MigratorTrait;

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

0 comments on commit 6b2fa49

Please sign in to comment.