starbase 0.12.0

Framework for building performant command line applications and developer tools.
docs.rs failed to build starbase-0.12.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: starbase-0.11.4

starbase

Crates.io Crates.io

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 outcome = app.run(CustomSession::default(), |session| async {
    // Run CLI
    Ok(None)
  }).await;

  // `run` returns an `AppRunOutcome`; `into_result()` collapses it into a
  // `Result<u8, E>` so the error (if any) and exit code can bubble up.
  Ok(ExitCode::from(outcome.into_result()?))
}

Session

A session must implement the AppSession trait. This trait provides an associated Error type and 4 optional methods, each representing a different phase in the application life cycle.

The Error type can be anything that implements Debug + Display + Send + 'static. This includes concrete std::error::Error types, Box<dyn Error>, and the type-erased reporters anyhow::Error and miette::Report. Each phase returns AppResult<Self::Error>, an alias for Result<Option<u8>, Self::Error>, where the optional u8 is an exit code.

use starbase::{AppSession, AppResult};
use miette::Report;
use std::path::PathBuf;
use async_trait::async_trait;

#[derive(Clone)]
pub struct CustomSession {
  pub workspace_root: PathBuf,
}

#[async_trait]
impl AppSession for CustomSession {
  // Use any error type that is `Debug + Display + Send + 'static`.
  type Error = Report;

  async fn startup(&mut self) -> AppResult<Self::Error> {
    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 in Arc, 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 the App#run method.

How to

Error handling

Each session chooses its own error type through the AppSession::Error associated type, so starbase itself is not tied to any specific error library. The only requirement is that the type implements Debug + Display + Send + 'static.

miette remains the default diagnostics and panic reporter: App::setup_diagnostics registers miette's hooks, and MainResult is a miette::Result<ExitCode>. Because miette::Report satisfies the error bound, it's the recommended error type when you want rich diagnostics rendered to the terminal. Note that Report intentionally does not implement std::error::Error, which is why the bound is Display-based rather than Error-based. miette is also re-exported as starbase::diagnostics for convenience.

App::run returns an AppRunOutcome, which carries the last phase, an optional error, and the resolved exit code. Call into_result() to collapse it into a Result<u8, Self::Error> for your main function.

use starbase::{App, MainResult};
use std::process::ExitCode;

#[tokio::main]
async fn main() -> MainResult {
  let app = App::default();
  app.setup_diagnostics();

  let _guard = app.setup_tracing_with_defaults()?;

  let outcome = app.run(CustomSession::default(), |session| async {
    // Run CLI
    Ok(None)
  }).await;

  outcome.into_exit_result()
}

OpenTelemetry tracing

When the otel feature is enabled, TracingOptions can also export traces and metrics over OTLP.

use starbase::tracing::{OtelOptions, TracingOptions};

let _guard = app.setup_tracing(TracingOptions {
    otel: OtelOptions {
        enabled: true,
        logs_enabled: false,
        service_name: Some("my-app".into()),
    },
    ..TracingOptions::default()
})?;

This wires the OTLP tracing and metrics bridge. Endpoint, protocol, headers, and other exporter behavior should still be configured through the standard OpenTelemetry environment variables.

Custom error types

To make the most out of errors, and in turn diagnostics, it's best (also suggested) to use the thiserror crate to define a concrete error type, optionally deriving miette's Diagnostic for rich output.

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,
}

You can then use this type directly as your session's error, and return failures from any phase with the ? operator or by constructing the error.

#[async_trait]
impl AppSession for CustomSession {
    type Error = AppError;

    async fn startup(&mut self) -> AppResult<Self::Error> {
        // Bubble up with `?`...
        could_fail()?;

        // ...or return the error directly.
        Err(AppError::SystemsOffline)
    }
}

If you instead set type Error = miette::Report, convert errors with .into() or the ? operator, which turns any Diagnostic into a Report.