Skip to main content

ralph/agent/
parse.rs

1//! Parsing functions for agent-related CLI inputs.
2//!
3//! Responsibilities:
4//! - Parse runner strings into Runner enum variants.
5//! - Parse git revert mode strings into GitRevertMode enum.
6//! - Parse runner CLI arguments into RunnerCliOptionsPatch structs.
7//!
8//! Not handled here:
9//! - Model parsing (see `crate::runner`).
10//! - Reasoning effort parsing (see `crate::runner`).
11//! - Override resolution (see `super::resolve`).
12//!
13//! Invariants/assumptions:
14//! - Parsing is case-insensitive for runner strings.
15//! - Invalid inputs return descriptive errors via anyhow.
16
17use crate::contracts::{
18    GitRevertMode, Runner, RunnerApprovalMode, RunnerCliOptionsPatch, RunnerOutputFormat,
19    RunnerPlanMode, RunnerSandboxMode, RunnerVerbosity, UnsupportedOptionPolicy,
20};
21use anyhow::{Result, anyhow, bail};
22
23use super::args::RunnerCliArgs;
24
25/// Parse a runner string into a Runner enum.
26pub fn parse_runner(value: &str) -> Result<Runner> {
27    let normalized = value.trim().to_lowercase();
28    match normalized.as_str() {
29        "codex" => Ok(Runner::Codex),
30        "opencode" => Ok(Runner::Opencode),
31        "gemini" => Ok(Runner::Gemini),
32        "claude" => Ok(Runner::Claude),
33        "cursor" => Ok(Runner::Cursor),
34        "kimi" => Ok(Runner::Kimi),
35        "pi" => Ok(Runner::Pi),
36        _ => bail!(
37            "Invalid runner: --runner must be 'codex', 'opencode', 'gemini', 'claude', 'cursor', 'kimi', or 'pi' (got: {}). Set a supported runner in .ralph/config.jsonc or via the --runner flag.",
38            value.trim()
39        ),
40    }
41}
42
43/// Parse git revert mode from a CLI string.
44pub fn parse_git_revert_mode(value: &str) -> Result<GitRevertMode> {
45    value.parse().map_err(|err: &str| anyhow!(err))
46}
47
48/// Parse runner CLI arguments into a patch struct.
49pub(crate) fn parse_runner_cli_patch(args: &RunnerCliArgs) -> Result<RunnerCliOptionsPatch> {
50    let output_format = match args.output_format.as_deref() {
51        Some(value) => Some(
52            value
53                .parse::<RunnerOutputFormat>()
54                .map_err(|err| anyhow!(err))?,
55        ),
56        None => None,
57    };
58    let verbosity = match args.verbosity.as_deref() {
59        Some(value) => Some(
60            value
61                .parse::<RunnerVerbosity>()
62                .map_err(|err| anyhow!(err))?,
63        ),
64        None => None,
65    };
66    let approval_mode = match args.approval_mode.as_deref() {
67        Some(value) => Some(
68            value
69                .parse::<RunnerApprovalMode>()
70                .map_err(|err| anyhow!(err))?,
71        ),
72        None => None,
73    };
74    let sandbox = match args.sandbox.as_deref() {
75        Some(value) => Some(
76            value
77                .parse::<RunnerSandboxMode>()
78                .map_err(|err| anyhow!(err))?,
79        ),
80        None => None,
81    };
82    let plan_mode = match args.plan_mode.as_deref() {
83        Some(value) => Some(
84            value
85                .parse::<RunnerPlanMode>()
86                .map_err(|err| anyhow!(err))?,
87        ),
88        None => None,
89    };
90    let unsupported_option_policy = match args.unsupported_option_policy.as_deref() {
91        Some(value) => Some(
92            value
93                .parse::<UnsupportedOptionPolicy>()
94                .map_err(|err| anyhow!(err))?,
95        ),
96        None => None,
97    };
98
99    Ok(RunnerCliOptionsPatch {
100        output_format,
101        verbosity,
102        approval_mode,
103        sandbox,
104        plan_mode,
105        unsupported_option_policy,
106    })
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn parse_runner_accepts_valid_runners() {
115        assert!(matches!(parse_runner("codex"), Ok(Runner::Codex)));
116        assert!(matches!(parse_runner("opencode"), Ok(Runner::Opencode)));
117        assert!(matches!(parse_runner("gemini"), Ok(Runner::Gemini)));
118        assert!(matches!(parse_runner("claude"), Ok(Runner::Claude)));
119        assert!(matches!(parse_runner("cursor"), Ok(Runner::Cursor)));
120        assert!(matches!(parse_runner("kimi"), Ok(Runner::Kimi)));
121        assert!(matches!(parse_runner("pi"), Ok(Runner::Pi)));
122        assert!(matches!(parse_runner("CODEX"), Ok(Runner::Codex)));
123    }
124
125    #[test]
126    fn parse_runner_rejects_invalid_runners() {
127        assert!(parse_runner("invalid").is_err());
128        assert!(parse_runner("").is_err());
129    }
130}