cmdparse 0.1.1

Parsing user's commands into arbitrary Rust types
Documentation
use cmdparse::error::{ParseError, UnrecognizedToken};
use cmdparse::tokens::{Token, TokenStream};
use cmdparse::{complete_parser, parse, CompletionResult, Parsable, ParseResult, Parser};
use rustyline::error::ReadlineError;
use std::collections::HashMap;

#[derive(Debug, Eq, PartialEq, Hash)]
struct Key(String);

type ParsingContext<'a> = &'a HashMap<Key, String>;

#[derive(Default)]
struct KeyParser;

impl<'c> Parser<ParsingContext<'c>> for KeyParser {
    type Value = Key;

    fn parse<'a>(
        &self,
        input: TokenStream<'a>,
        _ctx: ParsingContext<'c>,
    ) -> ParseResult<'a, Self::Value> {
        match input.take().transpose()? {
            Some((Token::Text(text), remaining)) => {
                let key = Key(text.parse_string().to_string());
                Ok((key, remaining))
            }
            Some((token @ Token::Attribute(_), remaining)) => {
                Err(UnrecognizedToken::new(token, remaining).into())
            }
            None => Err(ParseError::token_required().expected("key").into()),
        }
    }

    fn complete<'a>(
        &self,
        input: TokenStream<'a>,
        ctx: ParsingContext<'c>,
    ) -> CompletionResult<'a> {
        match input.take() {
            Some(Ok((Token::Text(text), remaining))) if remaining.is_all_consumed() => {
                let text = text.parse_string();
                CompletionResult::new_final(true).add_suggestions(ctx.keys().filter_map(|key| {
                    key.0
                        .strip_prefix(&text as &str)
                        .and_then(|key| match key.is_empty() {
                            true => None,
                            false => Some(key.to_string().into()),
                        })
                }))
            }
            Some(Ok((Token::Text(_), remaining))) => CompletionResult::new(remaining, true),
            Some(Ok((Token::Attribute(_), _))) => CompletionResult::new(input, false),
            Some(Err(_)) => CompletionResult::new_final(false),
            None => CompletionResult::new_final(false)
                .add_suggestions(ctx.keys().map(|key| key.0.to_string().into())),
        }
    }
}

impl<'a> Parsable<ParsingContext<'a>> for Key {
    type Parser = KeyParser;
}

#[derive(Debug, Parsable)]
#[cmd(ctx = "ParsingContext<'_>")]
enum Command {
    Set(Key, String),
    Get(Key),
    Del(Vec<Key>),
    GetSubstr {
        key: Key,
        length: usize,
        #[cmd(attr(start))]
        start: usize,
    },
    GetStrlen(Key),
}

struct CmdParserHelper<'a> {
    context: ParsingContext<'a>,
}

impl<'a> CmdParserHelper<'a> {
    fn new(context: ParsingContext<'a>) -> Self {
        CmdParserHelper { context }
    }
}

impl<'a> rustyline::completion::Completer for CmdParserHelper<'a> {
    type Candidate = String;

    fn complete(
        &self,
        line: &str,
        pos: usize,
        _ctx: &rustyline::Context<'_>,
    ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
        let input = &line[..pos];
        let suggestions: Vec<_> =
            complete_parser::<ParsingContext, CommandParser>(input, self.context)
                .into_iter()
                .map(|suggestion| suggestion.to_string())
                .collect();
        Ok((pos, suggestions))
    }
}

impl<'a> rustyline::Helper for CmdParserHelper<'a> {}
impl<'a> rustyline::highlight::Highlighter for CmdParserHelper<'a> {}
impl<'a> rustyline::validate::Validator for CmdParserHelper<'a> {}
impl<'a> rustyline::hint::Hinter for CmdParserHelper<'a> {
    type Hint = String;
}

fn main() {
    let mut data = HashMap::new();

    loop {
        let mut rl = rustyline::Editor::new();
        rl.set_helper(Some(CmdParserHelper::new(&data)));
        let readline = rl.readline("storage> ");

        match readline {
            Ok(line) => match parse::<_, Command>(&line, &data) {
                Ok(Command::Set(key, value)) => {
                    data.insert(key, value);
                    println!("OK");
                }
                Ok(Command::Get(key)) => {
                    let value: Option<&str> = data.get(&key).map(String::as_str);
                    println!("{}", value.unwrap_or("(nil)"));
                }
                Ok(Command::Del(keys)) => {
                    for key in keys {
                        data.remove(&key);
                    }
                    println!("OK");
                }
                Ok(Command::GetSubstr { key, length, start }) => match data.get(&key) {
                    Some(value) => {
                        for ch in value.chars().skip(start).take(length) {
                            print!("{}", ch);
                        }
                        println!();
                    }
                    None => println!("(nil)"),
                },
                Ok(Command::GetStrlen(key)) => match data.get(&key) {
                    Some(value) => println!("{}", value.chars().count()),
                    None => println!("(nil)"),
                },
                Err(err) => println!("Error: {}", err),
            },
            Err(ReadlineError::Interrupted | ReadlineError::Eof) => break,
            Err(err) => {
                println!("Error: {}", err);
                break;
            }
        }
    }
}