#match #command #interpreter #command-handler

nightly cmdmat

Command matcher for matching lists of strings against handlers

5 releases

0.1.4 Feb 25, 2023
0.1.3 Feb 25, 2023
0.1.2 Feb 3, 2020
0.1.1 Sep 15, 2019
0.1.0 Sep 15, 2019

#778 in Command-line interface

27 downloads per month
Used in gameshell

LGPL-3.0-or-later

31KB
568 lines

A command matching engine

This library matches a list of input parameters such as: ["example", "input", "123"] to a handler that is able to handle these inputs.

The handlers are registered using the Spec (specification) format:

use cmdmat::{Decider, Decision, Spec, SVec};

type Accept = i32;
type Deny = String;
type Context = i32;

fn handler(_ctx: &mut Context, _args: &[Accept]) -> Result<String, String> {
    Ok("".into())
}

fn accept_integer(input: &[&str], out: &mut SVec<Accept>) -> Decision<Deny> {
    if input.len() != 1 {
        return Decision::Deny("Require exactly 1 input".into());
    }
    if let Ok(num) = input[0].parse::<Accept>() {
        out.push(num);
        Decision::Accept(1)
    } else {
        Decision::Deny("Unable to get number".into())
    }
}

const DEC: Decider<Accept, Deny> = Decider {
    description: "<i32>",
    decider: accept_integer,
};

const SPEC: Spec<Accept, Deny, Context> = (&[("example", None), ("input", Some(&DEC))], handler);

In the above the SPEC variable defines a path to get to the handler, requiring first "example" with no validator None, then followed by "input" which takes a single integer.

If the validator accept_integer fails, then the command lookup will also fail.

The Specs will be collected inside a Mapping, where lookup will happen among a tree of merged Specs.

The reason we have a separate literal string and validator is to make it easy and unambiguous to find the next node in the search tree. If we only used validators (which can be completely arbitrary), then we can not sort a tree to make searching O(log n). These fixed literal search points also give us a good way to debug commands if they happen to not match anything.

Here is an example with actual lookup where we call a handler: (Unfortunately, a bit of setup is required.)

use cmdmat::{Decider, Decision, Mapping, Spec, SVec};

// The accept type is the type enum containing accepted tokens, parsed into useful formats
// the list of accepted input is at last passed to the finalizer
#[derive(Debug)]
enum Accept {
    I32(i32),
}

// Deny is the type returned by a decider when it denies an input (the input is invalid)
type Deny = String;

// The context is the type on which "finalizers" (the actual command handler) will run
type Context = i32;

// This is a `spec` (command specification)
const SPEC: Spec<Accept, Deny, Context> = (&[("my-command-name", Some(&DEC))], print_hello);

fn print_hello(_ctx: &mut Context, args: &[Accept]) -> Result<String, String> {
    println!("Hello world!");
    assert_eq!(1, args.len());
    println!("The args I got: {:?}", args);
    Ok("".into())
}

// This decider accepts only a single number
fn decider_function(input: &[&str], out: &mut SVec<Accept>) -> Decision<Deny> {
    if input.is_empty() {
        return Decision::Deny("No argument provided".into());
    }
    let num = input[0].parse::<i32>();
    if let Ok(num) = num {
        out.push(Accept::I32(num));
        Decision::Accept(1)
    } else {
        Decision::Deny("Number is not a valid i32".into())
    }
}

const DEC: Decider<Accept, Deny> = Decider {
    description: "<i32>",
    decider: decider_function,
};

let mut mapping = Mapping::default();
mapping.register(SPEC).unwrap();

let handler = mapping.lookup(&["my-command-name", "123"]);

match handler {
    Ok((func, buf)) => {
        let mut ctx = 0i32;
        func(&mut ctx, &buf); // prints hello world
    }
    Err(look_err) => {
        println!("{:?}", look_err);
        assert!(false);
    }
}

This library also allows partial lookups and iterating over the direct descendants in order to make autocomplete easy to implement for terminal interfaces.

use cmdmat::{Decider, Decision, Mapping, MappingEntry, Spec, SVec};

#[derive(Debug)]
enum Accept {
    I32(i32),
}
type Deny = String;
type Context = i32;

const SPEC: Spec<Accept, Deny, Context> =
    (&[("my-command-name", Some(&DEC)), ("something", None)], print_hello);

fn print_hello(_ctx: &mut Context, args: &[Accept]) -> Result<String, String> {
    Ok("".into())
}

fn decider_function(input: &[&str], out: &mut SVec<Accept>) -> Decision<Deny> {
    if input.is_empty() {
        return Decision::Deny("No argument provided".into());
    }
    let num = input[0].parse::<i32>();
    if let Ok(num) = num {
        out.push(Accept::I32(num));
        Decision::Accept(1)
    } else {
        Decision::Deny("Number is not a valid i32".into())
    }
}

const DEC: Decider<Accept, Deny> = Decider {
    description: "<i32>",
    decider: decider_function,
};

let mut mapping = Mapping::default();
mapping.register(SPEC).unwrap();

// When a decider is "next-up", we get its description
// We can't know in advance what the decider will consume because it is arbitrary code,
// so we will have to trust its description to be valuable.
let decider_desc = mapping.partial_lookup(&["my-command-name"]).unwrap().right().unwrap();
assert_eq!("<i32>", decider_desc);

// In this case the decider succeeded during the partial lookup, so the next step in the
// tree is the "something" node.
let mapping = mapping.partial_lookup(&["my-command-name", "123"]).unwrap().left().unwrap();
let MappingEntry { literal, decider, finalizer, submap } = mapping.get_direct_keys().next().unwrap();
assert_eq!("something", literal);
assert!(decider.is_none());
assert!(finalizer.is_some());

Dependencies

~73KB