Crate narrate

source ·
Expand description

githubcrates-iodocs-rs

This library provides Rust CLI applications with console reporting and error-handling utilities. Console output is modeled after Cargo, and the Error type is similar to anyhow’s Error, but with optional help messages.

Minimum supported Rust version: 1.61.1

Features

Cargo Feature Flags

All features are enabled by default, but they can be imported individually using Cargo feature flags:

  • error: Enables error-handling with Error, Result and ErrorWrap.
  • cli-error: Enables set of CliErrors with their associated exit_code.
  • report: Enables reporting errors and statuses to the console with the report module.
Example Cargo.toml
...
[dependencies]
narrate = { version = "0.4.0", default-features = false, features = ["report"] }
...

Error Handling

Use Result<T> as a return type for any fallible function. Within the function, use ? to propagate any error that implements the std::error::Error trait. Same as anyhow::Result<T>.

use narrate::Result;

fn get_user() -> Result<User> {
    let json = std::fs::read_to_string("user.json")?;
    let user: User = serde_json::from_str(&json)?;
    Ok(user)
}
Returning from main()

Result<T> can be used to return from main(). If any errors occur it prints the Debug implementation for Error.

use narrate::{bail, Result};

fn main() -> Result<()> {
    inner_fn()?;
    Ok(())
}

fn inner_fn() -> Result<()> {
    bail!("internal error")
}

Console output:

Error: internal error

Error Wrap

Wrap an error with more context by importing ErrorWrap. Similar to anyhow::Context. Just add .wrap(context) after any function call that returns a Result.

Context can be anything that implements Debug, Display, Sync and Send – including &str, String and errors.

use narrate::{ErrorWrap, Result, CliError};

fn run() -> Result<()> {
    ...
    // wrap with contextual &str
    acquire().wrap("unable to acquire data")?;

    // or wrap with another error
    config.load().wrap(CliError::Config)?;
    ...
}

fn acquire() -> Result<(), io::Error> {
    ...
}

Console output:

Error: unable to acquire data
Cause: oh no!
Lazy evaluation

If your context requires some work to create/format, you should use wrap_with instead.

use narrate::{ErrorWrap, Result, CliError};

fn run() -> Result<()> {
    ...
    // wrap with a formatted string
    data.store(path).wrap_with(|| format!("can't save to: {path}"))?;

    // wrap with a computed error
    data.store(path)
        .wrap_with(|| CliError::WriteFile(PathBuf::from(path)))?;
    ...
}

Help Message Wrap

Add separate help text to an error. By importing ErrorWrap you also get the add_help method and its lazy version add_help_with.

use narrate::{ErrorWrap, Result};

fn run() -> Result<()> {
    Project::new(path).add_help("try using `project init`")?;
    ...
}

Console output:

Error: directory already exists: '/home/dev/cool-project'

try using `project init`
Combination

Mix and match the ErrorWrap methods throughout your application to make sure the user gets all the information they need.

use narrate::{ErrorWrap, Result};

fn run() -> Result<()> {
    ...
    new_project(&path).wrap("cannot create project")?;
    ...
}

fn new_project(path: &Path) -> Result<()> {
    ...
    create_dir(path)
        .wrap_with(|| format!(
            "unable to create directory: '{}'",
            path.display()
        ))
        .add_help(
            "try using `project init` inside your existing directory"
        )?;
    ...
}

Console output:

Error: cannot create project
Cause: unable to create directory: '/home/dev/cool-project'
Cause: Is a directory (os error 20)

try using `project init` inside your existing directory

Convenience Macros

Use the error_from macro to create an ad-hoc Error from a string or another error. Similar to anyhow!.

let val = map.get(key).ok_or(error_from!("unknown key"))?;

Use bail to return early with an error. Equivalent to return Err(error_from!(...)).

if !map.contains_key(key) {
    bail!("unknown key");
}

CLI Errors

Use CliError for a set of common errors that can occur in a command-line application. These can be used to avoid adding repetitive context for IO errors.

use narrate::{bail, ErrorWrap, Result, CliError};

fn run() -> Result<()> {
    ...
    match args.operation {
        Op::Get => fetch_data(res_name).wrap_with(|| CliError::ResourceNotFound(res_name))?,
        Op::Set(data) => set_data(res_name, data).wrap(CliError::InputData)?,
        _ => bail!(CliError::Protocol),
    }
    ...
}

Exit Codes

As this selection of errors can often be fatal for an application, this library provides access to a set of standard program exit codes via the ExitCode trait. These adhere to sysexits.h.

Both anyhow::Error and narrate::Error implement this trait, thus can provide exit codes. If no CliError is found as an underlying error, the code will be 70 (for internal software error).

Import the ExitCode trait to use the exit_code function, and use std::process::exit to exit the program with the appropriate code.

use narrate::ExitCode;

if let Err(err) = run() {
    std::process::exit(err.exit_code());
}

Re-exports

pub use anyhow;
pub use colored;

Modules

Functions for printing status and error messages to stderr.

Macros

Return early with an error.
Construct an ad-hoc error from a string or existing non-narrate error value.

Structs

Iterator of a chain of source errors.
Wrapper around a dynamic error type with an optional help message.

Enums

Standard command line application error
The 8 standard colors.

Traits

Provides wrap and add_help methods for Result.
Provide exit_code method for CliError. Intended to be passed to std::process::exit.

Type Definitions

Result<T, Error>