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    GitPublishMode, GitRevertMode, Runner, RunnerApprovalMode, RunnerCliOptionsPatch,
19    RunnerOutputFormat, RunnerPlanMode, RunnerSandboxMode, RunnerVerbosity,
20    UnsupportedOptionPolicy,
21};
22use anyhow::{Result, anyhow, bail};
23
24use super::args::RunnerCliArgs;
25
26/// Parse a runner string into a Runner enum.
27pub fn parse_runner(value: &str) -> Result<Runner> {
28    let normalized = value.trim().to_lowercase();
29    match normalized.as_str() {
30        "codex" => Ok(Runner::Codex),
31        "opencode" => Ok(Runner::Opencode),
32        "gemini" => Ok(Runner::Gemini),
33        "claude" => Ok(Runner::Claude),
34        "cursor" => Ok(Runner::Cursor),
35        "kimi" => Ok(Runner::Kimi),
36        "pi" => Ok(Runner::Pi),
37        _ => bail!(
38            "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.",
39            value.trim()
40        ),
41    }
42}
43
44/// Parse git revert mode from a CLI string.
45pub fn parse_git_revert_mode(value: &str) -> Result<GitRevertMode> {
46    value.parse().map_err(|err: &str| anyhow!(err))
47}
48
49/// Parse git publish mode from a CLI string.
50pub fn parse_git_publish_mode(value: &str) -> Result<GitPublishMode> {
51    value.parse().map_err(|err: &str| anyhow!(err))
52}
53
54/// Parse runner CLI arguments into a patch struct.
55pub(crate) fn parse_runner_cli_patch(args: &RunnerCliArgs) -> Result<RunnerCliOptionsPatch> {
56    let output_format = match args.output_format.as_deref() {
57        Some(value) => Some(
58            value
59                .parse::<RunnerOutputFormat>()
60                .map_err(|err| anyhow!(err))?,
61        ),
62        None => None,
63    };
64    let verbosity = match args.verbosity.as_deref() {
65        Some(value) => Some(
66            value
67                .parse::<RunnerVerbosity>()
68                .map_err(|err| anyhow!(err))?,
69        ),
70        None => None,
71    };
72    let approval_mode = match args.approval_mode.as_deref() {
73        Some(value) => Some(
74            value
75                .parse::<RunnerApprovalMode>()
76                .map_err(|err| anyhow!(err))?,
77        ),
78        None => None,
79    };
80    let sandbox = match args.sandbox.as_deref() {
81        Some(value) => Some(
82            value
83                .parse::<RunnerSandboxMode>()
84                .map_err(|err| anyhow!(err))?,
85        ),
86        None => None,
87    };
88    let plan_mode = match args.plan_mode.as_deref() {
89        Some(value) => Some(
90            value
91                .parse::<RunnerPlanMode>()
92                .map_err(|err| anyhow!(err))?,
93        ),
94        None => None,
95    };
96    let unsupported_option_policy = match args.unsupported_option_policy.as_deref() {
97        Some(value) => Some(
98            value
99                .parse::<UnsupportedOptionPolicy>()
100                .map_err(|err| anyhow!(err))?,
101        ),
102        None => None,
103    };
104
105    Ok(RunnerCliOptionsPatch {
106        output_format,
107        verbosity,
108        approval_mode,
109        sandbox,
110        plan_mode,
111        unsupported_option_policy,
112    })
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn parse_runner_accepts_valid_runners() {
121        assert!(matches!(parse_runner("codex"), Ok(Runner::Codex)));
122        assert!(matches!(parse_runner("opencode"), Ok(Runner::Opencode)));
123        assert!(matches!(parse_runner("gemini"), Ok(Runner::Gemini)));
124        assert!(matches!(parse_runner("claude"), Ok(Runner::Claude)));
125        assert!(matches!(parse_runner("cursor"), Ok(Runner::Cursor)));
126        assert!(matches!(parse_runner("kimi"), Ok(Runner::Kimi)));
127        assert!(matches!(parse_runner("pi"), Ok(Runner::Pi)));
128        assert!(matches!(parse_runner("CODEX"), Ok(Runner::Codex)));
129    }
130
131    #[test]
132    fn parse_runner_rejects_invalid_runners() {
133        assert!(parse_runner("invalid").is_err());
134        assert!(parse_runner("").is_err());
135    }
136}