toon_format/tui/
repl_command.rs

1//! REPL command parser with inline data support
2
3use anyhow::{
4    bail,
5    Result,
6};
7
8/// Parsed REPL command with inline data
9#[derive(Debug, Clone)]
10pub struct ReplCommand {
11    pub name: String,
12    pub inline_data: Option<String>,
13    pub args: Vec<String>,
14}
15
16impl ReplCommand {
17    /// Parse command input, extracting inline data if present.
18    ///
19    /// Handles patterns like:
20    /// - `encode {"data": true}` - JSON inline
21    /// - `decode name: Alice` - TOON inline
22    /// - `encode $var` - Variable reference
23    pub fn parse(input: &str) -> Result<Self> {
24        let input = input.trim();
25        if input.is_empty() {
26            bail!("Empty command");
27        }
28
29        let parts: Vec<&str> = input.splitn(2, ' ').collect();
30        let cmd_name = parts[0].to_string();
31
32        let (inline_data, remaining_args) = if parts.len() > 1 {
33            let rest = parts[1].trim();
34
35            // Check if input looks like data rather than flags/args
36            if rest.starts_with('{')
37                || rest.starts_with('"')
38                || rest.starts_with('$')
39                || rest.contains(':')
40            {
41                let data_end = if rest.starts_with('{') {
42                    find_matching_brace(rest) // Handle nested braces
43                } else if rest.starts_with('$') {
44                    rest.find(' ').unwrap_or(rest.len()) // Variable name
45                } else {
46                    rest.find(" --").unwrap_or(rest.len()) // Until flag or end
47                };
48
49                let data = rest[..data_end].trim().to_string();
50                let remaining = rest[data_end..].trim();
51
52                (
53                    Some(data),
54                    if remaining.is_empty() {
55                        vec![]
56                    } else {
57                        remaining
58                            .split_whitespace()
59                            .map(|s| s.to_string())
60                            .collect()
61                    },
62                )
63            } else {
64                (
65                    None,
66                    rest.split_whitespace().map(|s| s.to_string()).collect(),
67                )
68            }
69        } else {
70            (None, vec![])
71        };
72
73        Ok(ReplCommand {
74            name: cmd_name,
75            inline_data,
76            args: remaining_args,
77        })
78    }
79
80    pub fn has_flag(&self, flag: &str) -> bool {
81        self.args.iter().any(|a| a == flag)
82    }
83
84    pub fn get_option(&self, option: &str) -> Option<&str> {
85        self.args
86            .iter()
87            .position(|a| a == option)
88            .and_then(|i| self.args.get(i + 1))
89            .map(|s| s.as_str())
90    }
91}
92
93fn find_matching_brace(s: &str) -> usize {
94    let mut depth = 0;
95    for (i, ch) in s.chars().enumerate() {
96        match ch {
97            '{' => depth += 1,
98            '}' => {
99                depth -= 1;
100                if depth == 0 {
101                    return i + 1;
102                }
103            }
104            _ => {}
105        }
106    }
107    s.len()
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_inline_json() {
116        let cmd = ReplCommand::parse(r#"encode {"test": true}"#).unwrap();
117        assert_eq!(cmd.name, "encode");
118        assert_eq!(cmd.inline_data, Some(r#"{"test": true}"#.to_string()));
119    }
120
121    #[test]
122    fn test_inline_toon() {
123        let cmd = ReplCommand::parse("decode name: Alice").unwrap();
124        assert_eq!(cmd.name, "decode");
125        assert_eq!(cmd.inline_data, Some("name: Alice".to_string()));
126    }
127
128    #[test]
129    fn test_with_flags() {
130        let cmd = ReplCommand::parse(r#"encode {"test": true} --fold-keys"#).unwrap();
131        assert_eq!(cmd.name, "encode");
132        assert!(cmd.inline_data.is_some());
133        assert!(cmd.has_flag("--fold-keys"));
134    }
135}