rpg_chat_command_parser/
parser.rs

1//! # Command Parser
2//!
3//! This module defines the core functionality for parsing RPG-style chat commands. It uses the
4//! [Pest](https://docs.rs/pest) parser library for grammar-based parsing.
5//!
6//! ## Grammar
7//! The grammar is defined in the grammar.pest file and supports the following:
8//! - **Verbs**: The primary action of the command (e.g., /cast, /attack).
9//! - **Targets**: Optional targets for the action (e.g., /cast fireball).
10//! - **Flags**: Key-value pairs for additional parameters (e.g., --power=high).
11//!
12//! ## Example
13//! use rpg_chat_command_parser::parse_command;
14//!
15//! let input = "/cast fireball --power=high";
16//! let parsed = parse_command(input).unwrap();
17//! assert_eq!(parsed.verb, "cast");
18//! assert_eq!(parsed.target, Some("fireball".to_string()));
19//! assert_eq!(parsed.flags.get("power"), Some(&"high".to_string()));
20//!
21use pest::Parser;
22use std::collections::HashMap;
23
24use crate::CommandError;
25/// Pest parser definition for command parsing.
26#[derive(pest_derive::Parser)]
27#[grammar = "grammar.pest"] // Tells Pest where the grammar file is
28struct CommandParser;
29/// Represents a parsed command with its components.
30#[derive(Debug, PartialEq)]
31pub struct ParsedCommand {
32    /// The main verb of the command (e.g., "cast").
33    pub verb: String,
34    /// The optional target of the command (e.g., "fireball").
35    pub target: Option<String>,
36    /// A map of flag key-value pairs (e.g., --power=high).
37    pub flags: HashMap<String, String>,
38}
39/// Parses an input string into a ParsedCommand.
40///
41/// # Errors
42/// Returns a CommandError if the input does not match the grammar.
43///
44/// # Example
45/// let input = "/cast fireball --power=high";
46/// let parsed = parse_command(input).unwrap();
47/// assert_eq!(parsed.verb, "cast");
48/// assert_eq!(parsed.target, Some("fireball".to_string()));
49/// assert_eq!(parsed.flags.get("power"), Some(&"high".to_string()));
50
51pub fn parse_command(input: &str) -> Result<ParsedCommand, CommandError> {
52    let parsed = CommandParser::parse(Rule::command, input)
53        .map_err(|e| {
54            println!("Debug: Parsing failed with error: {:?}", e);
55            CommandError::InvalidSyntax
56        })?
57        .next()
58        .ok_or_else(|| {
59            println!("Debug: No top-level rule matched");
60            CommandError::InvalidSyntax
61        })?
62        .into_inner();
63
64    let mut verb = String::new();
65    let mut target = None;
66    let mut flags = HashMap::new();
67
68    for pair in parsed {
69        println!(
70            "Debug: Top-level Pair = {:?}, Rule = {:?}",
71            pair.as_str(),
72            pair.as_rule()
73        );
74        match pair.as_rule() {
75            Rule::verb => verb = pair.as_str().to_string(),
76            Rule::target => target = Some(pair.as_str().to_string()),
77            Rule::flag => {
78                println!(
79                    "Debug: Pair = {:?}, Rule = {:?}",
80                    pair.as_str(),
81                    pair.as_rule()
82                );
83                let mut inner = pair.into_inner(); // Extract nested key and value rules
84
85                let key = inner
86                    .next()
87                    .ok_or(CommandError::MissingFlagKey)? // Missing key
88                    .as_str()
89                    .to_string();
90                println!("Debug: Parsed key = {:?}", key);
91
92                // Explicitly validate the key
93                if key.is_empty() || key.starts_with("=") || key.contains("--") {
94                    println!("Debug: Invalid key detected = {:?}", key);
95                    return Err(CommandError::MissingFlagKey);
96                }
97
98                let value_pair = inner.next().ok_or(CommandError::MissingFlagValue)?; // Missing value
99                let value = value_pair.as_str().to_string();
100
101                if value.is_empty() {
102                    return Err(CommandError::MissingFlagValue);
103                }
104
105                println!("Debug: Parsed value = {:?}", value);
106                println!("Debug: Parsed key = {:?}, value = {:?}", key, value);
107                flags.insert(key, value);
108            }
109            Rule::bad_flag => {
110                println!("Debug: Malformed flag detected = {:?}", pair.as_str());
111                return Err(CommandError::InvalidSyntax);
112            }
113            _ => {}
114        }
115    }
116
117    if verb.is_empty() {
118        return Err(CommandError::MissingVerb);
119    }
120
121    Ok(ParsedCommand {
122        verb,
123        target,
124        flags,
125    })
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_missing_verb() {
134        let input = "--power=high"; // No verb at the start
135        let result = parse_command(input);
136        assert!(matches!(result, Err(CommandError::InvalidSyntax))); // Updated to match syntax error
137    }
138
139    #[test]
140    fn test_missing_flag_key() {
141        let input = "/cast fireball --=high"; // Missing key
142        let result = parse_command(input);
143        assert!(matches!(result, Err(CommandError::MissingFlagKey)));
144    }
145
146    #[test]
147    fn test_invalid_flag_key() {
148        let input = "/cast fireball --==high"; // Missing key
149        let result = parse_command(input);
150        assert!(matches!(result, Err(CommandError::MissingFlagKey)));
151    }
152
153    #[test]
154    fn test_missing_flag_value() {
155        let input = "/cast fireball --power="; // Missing value
156        let result = parse_command(input);
157        assert!(matches!(result, Err(CommandError::MissingFlagValue)));
158    }
159
160    #[test]
161    fn test_valid_command() {
162        let input = "/cast fireball --power=high";
163        let result = parse_command(input).unwrap();
164        assert_eq!(result.verb, "cast");
165        assert_eq!(result.target, Some("fireball".to_string()));
166        assert_eq!(result.flags.get("power"), Some(&"high".to_string()));
167    }
168}