Skip to main content

slash_lang/parser/
chain.rs

1use crate::parser::ast::Arg;
2
3/// Result of parsing a builder chain: command name, optional primary arg, and method args.
4pub struct ChainParts {
5    pub name: String,
6    pub primary: Option<String>,
7    pub args: Vec<Arg>,
8}
9
10/// Splits a bare command token (urgency/optional already stripped) into the command
11/// name, optional primary argument, and builder-chain arguments.
12///
13/// `/read(src/main.rs)` → name=`read`, primary=`Some("src/main.rs")`, args=`[]`
14/// `/edit(path).find(old).replace(new)` → name=`edit`, primary=`Some("path")`, args=`[find(old), replace(new)]`
15/// `/echo.text(hello)` → name=`echo`, primary=`None`, args=`[text(hello)]`
16/// `/build` → name=`build`, primary=`None`, args=`[]`
17#[allow(clippy::result_unit_err)]
18pub fn parse_builder_chain(bare: &str) -> Result<ChainParts, ()> {
19    let s = bare.trim_start_matches('/');
20
21    // Check for primary arg: `/cmd(value)` or `/cmd(value).method(...)`.
22    // Find `(` before any `.` at depth 0 to detect primary arg.
23    let first_open = s.find('(');
24    let first_dot = find_dot_at_depth_zero(s);
25
26    if let Some(paren_pos) = first_open {
27        if first_dot.is_none() || paren_pos < first_dot.unwrap() {
28            // Primary arg present: `/cmd(value)...`
29            let cmd_name = &s[..paren_pos];
30            let rest = &s[paren_pos..];
31
32            // Extract the balanced primary arg value.
33            let (primary_val, remainder) = extract_balanced_parens(rest)?;
34
35            let primary = if primary_val.is_empty() {
36                None
37            } else {
38                Some(primary_val.to_string())
39            };
40
41            let args = if let Some(after_dot) = remainder.strip_prefix('.') {
42                parse_chain(after_dot)?
43            } else if remainder.is_empty() {
44                vec![]
45            } else {
46                return Err(());
47            };
48
49            return Ok(ChainParts {
50                name: cmd_name.to_string(),
51                primary,
52                args,
53            });
54        }
55    }
56
57    // No primary arg — original logic.
58    match s.split_once('.') {
59        Some((cmd_raw, chain)) => Ok(ChainParts {
60            name: cmd_raw.to_string(),
61            primary: None,
62            args: parse_chain(chain)?,
63        }),
64        None => Ok(ChainParts {
65            name: s.to_string(),
66            primary: None,
67            args: vec![],
68        }),
69    }
70}
71
72/// Find the position of the first `.` that is not inside parentheses.
73fn find_dot_at_depth_zero(s: &str) -> Option<usize> {
74    let mut depth: usize = 0;
75    for (i, ch) in s.char_indices() {
76        match ch {
77            '(' => depth += 1,
78            ')' => {
79                if depth == 0 {
80                    return None;
81                }
82                depth -= 1;
83            }
84            '.' if depth == 0 => return Some(i),
85            _ => {}
86        }
87    }
88    None
89}
90
91/// Given a string starting with `(`, extract the balanced content and return
92/// (inner_value, remainder_after_close_paren).
93fn extract_balanced_parens(s: &str) -> Result<(&str, &str), ()> {
94    debug_assert!(s.starts_with('('));
95    let mut depth: usize = 0;
96    for (i, ch) in s.char_indices() {
97        match ch {
98            '(' => depth += 1,
99            ')' => {
100                depth -= 1;
101                if depth == 0 {
102                    let inner = &s[1..i];
103                    let remainder = &s[i + 1..];
104                    return Ok((inner, remainder));
105                }
106            }
107            _ => {}
108        }
109    }
110    Err(())
111}
112
113fn parse_chain(chain: &str) -> Result<Vec<Arg>, ()> {
114    split_segments(chain)?
115        .into_iter()
116        .filter(|s| !s.is_empty())
117        .map(parse_arg)
118        .collect()
119}
120
121/// Splits a chain string on `.` only when not inside parentheses.
122/// Handles values containing dots: `flag(1.0)` is one segment.
123/// Returns `Err(())` for unmatched parentheses.
124fn split_segments(chain: &str) -> Result<Vec<&str>, ()> {
125    let mut segments = Vec::new();
126    let mut depth: usize = 0;
127    let mut start = 0;
128    for (i, ch) in chain.char_indices() {
129        match ch {
130            '(' => depth += 1,
131            ')' => {
132                if depth == 0 {
133                    return Err(());
134                }
135                depth -= 1;
136            }
137            '.' if depth == 0 => {
138                segments.push(&chain[start..i]);
139                start = i + 1;
140            }
141            _ => {}
142        }
143    }
144    if depth != 0 {
145        return Err(());
146    }
147    segments.push(&chain[start..]);
148    Ok(segments)
149}
150
151/// Parses a single chain segment.
152/// `flag(val)` → `Arg { name: "flag", value: Some("val") }`
153/// `flag()`   → `Arg { name: "flag", value: None }`
154/// `flag`     → `Arg { name: "flag", value: None }`
155/// Returns `Err(())` if the segment contains `(` without a matching `)`.
156fn parse_arg(segment: &str) -> Result<Arg, ()> {
157    if let Some((name, rest)) = segment.split_once('(') {
158        let value = rest.strip_suffix(')').ok_or(())?;
159        Ok(Arg {
160            name: name.to_string(),
161            value: if value.is_empty() {
162                None
163            } else {
164                Some(value.to_string())
165            },
166        })
167    } else {
168        Ok(Arg {
169            name: segment.to_string(),
170            value: None,
171        })
172    }
173}