rituals 0.2.0

Ritual — what a task is, and how a composed command line dispatches to one.
Documentation
  • Coverage
  • 100%
    15 out of 15 items documented9 out of 10 items with examples
  • Size
  • Source code size: 131.5 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 913.0 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 5s Average build duration of successful builds.
  • all releases: 6s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • hexlace/ritual
    2 0 11
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • beeauvin github:hexlace:ritualists

rituals

What a ritual task is, and how a composed command line dispatches to one. This is the only crate a task author depends on.

A task crate

A task is an ordinary library crate. Its manifest depends on rituals and marks the crate as a task:

[package]
name = "hello"
version = "0.1.0"
edition = "2024"

[dependencies]
rituals = "0.2"

[package.metadata.ritual]
task = true

Its library exposes one function, task(), built from a clap argument struct and the function that runs it:

use rituals::{Outcome, Task, clap, report};

/// What this task accepts on the command line.
#[derive(clap::Args)]
struct Arguments {
    /// who to greet
    who: String,
}

fn run(arguments: Arguments) -> Outcome {
    report(format!("hello, {}", arguments.who));
    Ok(())
}

#[must_use]
pub fn task() -> Task {
    Task::new("say hello to somebody", run)
}

The first argument to Task::new is the one-line description --help shows, whatever the arguments struct's doc comment says, and not the task's name. A task never names itself: the command it answers to is the dependency key of whichever project imports it.

report writes one line to stdout, where a caller reads results; warn writes one to stderr, for something the person should heed that is not the result. Arguments are ordinary clap, re-exported as rituals::clap, so a task crate needs no parser dependency of its own.

Refusing

A task that cannot do its job returns a Failure. The command line prints it after its own name and exits with status 1, unless the task chose another:

use std::path::PathBuf;

use rituals::{Failure, Outcome, clap, report};

#[derive(clap::Args)]
struct Arguments {
    /// the file to count lines in
    path: PathBuf,
}

fn run(arguments: Arguments) -> Outcome {
    let text = std::fs::read_to_string(&arguments.path).map_err(|error| {
        Failure::new(format!("reading {} failed", arguments.path.display())).caused_by(error)
    })?;
    report(format!("{} lines", text.lines().count()));
    Ok(())
}
$ cargo ritual lines missing.txt
ritual: reading missing.txt failed: No such file or directory (os error 2)

The line names the cause after the message, then the cause's own source(), and so on down the chain, each joined with : . A Failure follows the usual convention for an error: its Display is the message alone, and the cause is its source(). To write a Failure into text of your own, with every cause, use failure.with_causes().

A cause whose own Display already includes its source shows that source twice in the refusal line, as thiserror's #[error("failed to start: {0}")] Io(#[from] io::Error) does, since #[from] makes the io error its source too; for such an error, use Failure::new(error.to_string()) in place of caused_by, or take the source out of the error's message.

A task with more than one way to refuse can give each its own exit status, so a caller such as a CI job can tell "the check found differences" from "the check could not run" without reading stderr:

use rituals::{Failure, Outcome, RefusalStatus};

const COULD_NOT_RUN: RefusalStatus = match RefusalStatus::new(4) {
    Ok(status) => status,
    Err(_) => panic!("4 is a refusal status"),
};

fn sync() -> Outcome {
    Err(Failure::new("the registry did not answer").exiting_with(COULD_NOT_RUN))
}

A status is 1, or anything from 3 to 125 but 101. RefusalStatus::new refuses 0, which is success, 2, which a usage error exits with, 101, which a panic exits with, and 126 and above, which a shell uses for itself. When one Failure wraps another, the outer one's status is the one the process exits with. new is const, so a status named as a constant, as above, is checked when the task compiles.

Running a task

A task runs inside a project's command line. The ritual binary, from rituals-cli, makes both:

cargo install --locked rituals-cli
ritual new demo
cd demo
cargo ritual create hello
cargo ritual hello

create writes a task crate into .rituals/hello, imports it, and regenerates the command line. Its task() is a placeholder, so cargo ritual hello answers hello has nothing to do yet. Replace .rituals/hello/src/lib.rs with the greeting task above, and cargo ritual hello world answers hello, world. Run outside any project, ritual create hello writes a task crate of its own instead, for several projects to share; import it.

More than one command

  • Task::group builds a bundle: a task whose command is a group of named child tasks. A bundle is imported like any other task, and bundles nest.
  • Task::receiving_command_line builds a task that is handed the command line it runs in: its name and version, the commands at its top level, the command path that reached the task (command_path()), and the command a person types to run it again (cargo_command()).

The ritual readme covers the concepts, and the design explains why ritual is shaped this way.