48 releases (7 breaking)
new 0.9.4 | Nov 11, 2024 |
---|---|
0.9.2 | Oct 10, 2024 |
0.8.2 | Jul 27, 2024 |
0.5.2 | Mar 29, 2024 |
0.2.0 | Jul 26, 2023 |
#219 in Development tools
1,427 downloads per month
Used in 2 crates
38KB
755 lines
starbase
Application framework for building performant command line applications and developer tools.
Usage
An application uses a session based approach, where a session object contains data required for the entire application lifecycle.
Create an App
, optionally setup diagnostics (miette
) and tracing (tracing
), and then run the
application with the provided session. A mutable session is required, as the session can be mutated
for each phase.
use starbase::{App, MainResult};
use std::process::ExitCode;
use crate::CustomSession;
#[tokio::main]
async fn main() -> MainResult {
let app = App::default();
app.setup_diagnostics();
let exit_code = app.run(CustomSession::default(), |session| async {
// Run CLI
Ok(None)
}).await?;
Ok(ExitCode::from(exit_code))
}
Session
A session must implement the AppSession
trait. This trait provides 4 optional methods, each
representing a different phase in the application life cycle.
use starbase::{AppSession, AppResult};
use std::path::PathBuf;
use async_trait::async_trait;
#[derive(Clone)]
pub struct CustomSession {
pub workspace_root: PathBuf,
}
#[async_trait]
impl AppSession for CustomSession {
async fn startup(&mut self) -> AppResult {
self.workspace_root = detect_workspace_root()?;
Ok(None)
}
}
Sessions must be cloneable and be
Send + Sync
compatible. We clone the session when spawning tokio tasks. If you want to persist data across threads, wrap session properties inArc
,RwLock
, and other mechanisms.
Phases
An application is divided into phases, where each phase will be processed and completed before moving onto the next phase. The following phases are available:
- Startup - Register, setup, or load initial session state.
- Example: load configuration, detect workspace root, load plugins
- Analyze - Analyze the current environment, update state, and prepare for execution.
- Example: generate project graph, load cache, signin to service
- Execute - Execute primary business logic (
App#run
).- Example: process dependency graph, run generator, check for new version
- Shutdown - Cleanup and shutdown on success of the entire lifecycle, or on failure of a
specific phase.
- Example: cleanup temporary files, shutdown server
If a session implements the
AppSession#execute
trait method, it will run in parallel with theApp#run
method.
How to
Error handling
Errors and diagnostics are provided by the miette
crate. All
layers of the application return the miette::Result
type (via AppResult
). This allows for errors
to be easily converted to diagnostics, and for miette to automatically render to the terminal for
errors and panics.
To benefit from this, update your main
function to return MainResult
.
use starbase::{App, MainResult};
#[tokio::main]
async fn main() -> MainResult {
let app = App::default();
app.setup_diagnostics();
app.setup_tracing_with_defaults();
// ...
Ok(())
}
To make the most out of errors, and in turn diagnostics, it's best (also suggested) to use the
thiserror
crate.
use miette::Diagnostic;
use thiserror::Error;
#[derive(Debug, Diagnostic, Error)]
pub enum AppError {
#[error(transparent)]
#[diagnostic(code(app::io_error))]
IoError(#[from] std::io::Error),
#[error("Systems offline!")]
#[diagnostic(code(app::bad_code))]
SystemsOffline,
}
Caveats
A returned Err
must be converted to a diagnostic first. There are 2 approaches to achieve this:
#[system]
async fn could_fail() {
// Convert error using into()
Err(AppError::SystemsOffline.into())
// OR use ? operator on Err()
Err(AppError::SystemsOffline)?
}
Dependencies
~8–22MB
~238K SLoC