Skip to main content

ralph_workflow/cli/reducer/
parser.rs

1//! Parse Args into CLI events.
2//!
3//! This module converts the clap-parsed Args struct into a sequence
4//! of `CliEvents` that can be processed by the reducer.
5
6use super::event::CliEvent;
7
8/// Convert CLI arguments into a sequence of events (functional pattern with iterator pipeline).
9///
10/// This function maps each relevant field in the Args struct to a
11/// corresponding `CliEvent`. Events are generated in a deterministic order,
12/// with later events taking precedence over earlier ones (last-wins semantics).
13///
14/// # Event Ordering
15///
16/// Events are generated in this order:
17/// 1. Verbosity flags (--quiet, --full, --debug, -v)
18/// 2. Preset flags (--quick, --rapid, --long, --standard, --thorough)
19/// 3. Explicit iteration counts (-D, -R)
20/// 4. Agent selection (-a, -r, model flags)
21/// 5. Configuration flags (--no-isolation, --review-depth, etc.)
22/// 6. Finalization event
23///
24/// This ordering ensures that:
25/// - Explicit overrides (like -D) come after presets and override them
26/// - Last-specified preset wins if multiple are given
27///
28/// # Arguments
29///
30/// * `args` - The parsed CLI arguments from clap
31///
32/// # Returns
33///
34/// A vector of `CliEvents` representing all specified CLI arguments.
35#[must_use]
36pub fn args_to_events(args: &super::super::Args) -> Vec<CliEvent> {
37    let verbosity_events = std::iter::empty()
38        .chain(
39            args.verbosity_shorthand
40                .quiet
41                .then_some(CliEvent::QuietModeEnabled),
42        )
43        .chain(
44            args.verbosity_shorthand
45                .full
46                .then_some(CliEvent::FullModeEnabled),
47        )
48        .chain(
49            args.debug_verbosity
50                .debug
51                .then_some(CliEvent::DebugModeEnabled),
52        )
53        .chain(args.verbosity.map(|level| CliEvent::VerbositySet { level }));
54
55    let preset_events = std::iter::empty()
56        .chain(
57            args.quick_presets
58                .quick
59                .then_some(CliEvent::QuickPresetApplied),
60        )
61        .chain(
62            args.quick_presets
63                .rapid
64                .then_some(CliEvent::RapidPresetApplied),
65        )
66        .chain(
67            args.quick_presets
68                .long
69                .then_some(CliEvent::LongPresetApplied),
70        )
71        .chain(
72            args.standard_presets
73                .standard
74                .then_some(CliEvent::StandardPresetApplied),
75        )
76        .chain(
77            args.standard_presets
78                .thorough
79                .then_some(CliEvent::ThoroughPresetApplied),
80        );
81
82    let iteration_events = std::iter::empty()
83        .chain(
84            args.developer_iters
85                .map(|v| CliEvent::DeveloperItersSet { value: v }),
86        )
87        .chain(
88            args.reviewer_reviews
89                .map(|v| CliEvent::ReviewerReviewsSet { value: v }),
90        );
91
92    let agent_events = std::iter::empty()
93        .chain(
94            args.developer_agent
95                .clone()
96                .map(|a| CliEvent::DeveloperAgentSet { agent: a }),
97        )
98        .chain(
99            args.reviewer_agent
100                .clone()
101                .map(|a| CliEvent::ReviewerAgentSet { agent: a }),
102        )
103        .chain(
104            args.developer_model
105                .clone()
106                .map(|m| CliEvent::DeveloperModelSet { model: m }),
107        )
108        .chain(
109            args.reviewer_model
110                .clone()
111                .map(|m| CliEvent::ReviewerModelSet { model: m }),
112        )
113        .chain(
114            args.developer_provider
115                .clone()
116                .map(|p| CliEvent::DeveloperProviderSet { provider: p }),
117        )
118        .chain(
119            args.reviewer_provider
120                .clone()
121                .map(|p| CliEvent::ReviewerProviderSet { provider: p }),
122        )
123        .chain(
124            args.reviewer_json_parser
125                .clone()
126                .map(|p| CliEvent::ReviewerJsonParserSet { parser: p }),
127        );
128
129    let preset_selection_events = args
130        .preset
131        .as_ref()
132        .map(|p| CliEvent::AgentPresetSet {
133            preset: format!("{p:?}"),
134        })
135        .into_iter();
136
137    let config_events = std::iter::empty()
138        .chain(args.no_isolation.then_some(CliEvent::IsolationModeDisabled))
139        .chain(
140            args.review_depth
141                .clone()
142                .map(|d| CliEvent::ReviewDepthSet { depth: d }),
143        )
144        .chain(
145            args.git_user_name
146                .as_ref()
147                .map(|n| CliEvent::GitUserNameSet {
148                    name: n.trim().to_string(),
149                }),
150        )
151        .chain(
152            args.git_user_email
153                .as_ref()
154                .map(|e| CliEvent::GitUserEmailSet {
155                    email: e.trim().to_string(),
156                }),
157        )
158        .chain(
159            args.show_streaming_metrics
160                .then_some(CliEvent::StreamingMetricsEnabled),
161        );
162
163    verbosity_events
164        .into_iter()
165        .chain(preset_events)
166        .chain(iteration_events)
167        .chain(agent_events)
168        .chain(preset_selection_events)
169        .chain(config_events)
170        .chain(std::iter::once(CliEvent::CliProcessingComplete))
171        .collect()
172}
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177    use crate::cli::Args;
178    use clap::Parser;
179
180    #[test]
181    fn test_args_to_events_empty() {
182        let args = Args::parse_from(["ralph"]);
183        let events = args_to_events(&args);
184
185        // Should have at least the completion event
186        assert!(
187            events.contains(&CliEvent::CliProcessingComplete),
188            "Should always have completion event"
189        );
190
191        // Should not have any other events
192        assert!(
193            !events.iter().any(|e| e != &CliEvent::CliProcessingComplete),
194            "Should have no other events for empty args"
195        );
196    }
197
198    #[test]
199    fn test_args_to_events_quick_preset() {
200        let args = Args::parse_from(["ralph", "-Q"]);
201        let events = args_to_events(&args);
202
203        assert!(
204            events.contains(&CliEvent::QuickPresetApplied),
205            "Should have quick preset event"
206        );
207        assert!(events.contains(&CliEvent::CliProcessingComplete));
208    }
209
210    #[test]
211    fn test_args_to_events_rapid_preset() {
212        let args = Args::parse_from(["ralph", "-U"]);
213        let events = args_to_events(&args);
214
215        assert!(
216            events.contains(&CliEvent::RapidPresetApplied),
217            "Should have rapid preset event"
218        );
219        assert!(events.contains(&CliEvent::CliProcessingComplete));
220    }
221
222    #[test]
223    fn test_args_to_events_long_preset() {
224        let args = Args::parse_from(["ralph", "-L"]);
225        let events = args_to_events(&args);
226
227        assert!(
228            events.contains(&CliEvent::LongPresetApplied),
229            "Should have long preset event"
230        );
231        assert!(events.contains(&CliEvent::CliProcessingComplete));
232    }
233
234    #[test]
235    fn test_args_to_events_standard_preset() {
236        let args = Args::parse_from(["ralph", "-S"]);
237        let events = args_to_events(&args);
238
239        assert!(
240            events.contains(&CliEvent::StandardPresetApplied),
241            "Should have standard preset event"
242        );
243        assert!(events.contains(&CliEvent::CliProcessingComplete));
244    }
245
246    #[test]
247    fn test_args_to_events_thorough_preset() {
248        let args = Args::parse_from(["ralph", "-T"]);
249        let events = args_to_events(&args);
250
251        assert!(
252            events.contains(&CliEvent::ThoroughPresetApplied),
253            "Should have thorough preset event"
254        );
255        assert!(events.contains(&CliEvent::CliProcessingComplete));
256    }
257
258    #[test]
259    fn test_args_to_events_explicit_iters() {
260        let args = Args::parse_from(["ralph", "-D", "7", "-R", "3"]);
261        let events = args_to_events(&args);
262
263        assert!(
264            events.contains(&CliEvent::DeveloperItersSet { value: 7 }),
265            "Should have developer iters event"
266        );
267        assert!(
268            events.contains(&CliEvent::ReviewerReviewsSet { value: 3 }),
269            "Should have reviewer reviews event"
270        );
271    }
272
273    #[test]
274    fn test_args_to_events_preset_plus_explicit_override() {
275        let args = Args::parse_from(["ralph", "-Q", "-D", "10", "-R", "5"]);
276        let events = args_to_events(&args);
277
278        // Should have both preset and explicit values
279        assert!(events.contains(&CliEvent::QuickPresetApplied));
280        assert!(events.contains(&CliEvent::DeveloperItersSet { value: 10 }));
281        assert!(events.contains(&CliEvent::ReviewerReviewsSet { value: 5 }));
282
283        // Verify order: preset comes before explicit override
284        let preset_idx = events
285            .iter()
286            .position(|e| e == &CliEvent::QuickPresetApplied)
287            .expect("Should have quick preset");
288        let iters_idx = events
289            .iter()
290            .position(|e| e == &CliEvent::DeveloperItersSet { value: 10 })
291            .expect("Should have developer iters");
292
293        assert!(
294            preset_idx < iters_idx,
295            "Preset should come before explicit override"
296        );
297    }
298
299    #[test]
300    fn test_args_to_events_agent_selection() {
301        let args = Args::parse_from(["ralph", "-a", "claude", "-r", "gpt"]);
302        let events = args_to_events(&args);
303
304        assert!(
305            events.contains(&CliEvent::DeveloperAgentSet {
306                agent: "claude".to_string()
307            }),
308            "Should have developer agent event"
309        );
310        assert!(
311            events.contains(&CliEvent::ReviewerAgentSet {
312                agent: "gpt".to_string()
313            }),
314            "Should have reviewer agent event"
315        );
316    }
317
318    #[test]
319    fn test_args_to_events_verbose_mode() {
320        let args = Args::parse_from(["ralph", "-v", "3"]);
321        let events = args_to_events(&args);
322
323        assert!(
324            events.contains(&CliEvent::VerbositySet { level: 3 }),
325            "Should have verbosity set event"
326        );
327    }
328
329    #[test]
330    fn test_args_to_events_debug_mode() {
331        let args = Args::parse_from(["ralph", "--debug"]);
332        let events = args_to_events(&args);
333
334        assert!(
335            events.contains(&CliEvent::DebugModeEnabled),
336            "Should have debug mode event"
337        );
338    }
339
340    #[test]
341    fn test_args_to_events_no_isolation() {
342        let args = Args::parse_from(["ralph", "--no-isolation"]);
343        let events = args_to_events(&args);
344
345        assert!(
346            events.contains(&CliEvent::IsolationModeDisabled),
347            "Should have isolation mode disabled event"
348        );
349    }
350
351    #[test]
352    fn test_args_to_events_git_identity() {
353        let args = Args::parse_from([
354            "ralph",
355            "--git-user-name",
356            "John Doe",
357            "--git-user-email",
358            "john@example.com",
359        ]);
360        let events = args_to_events(&args);
361
362        assert!(
363            events.contains(&CliEvent::GitUserNameSet {
364                name: "John Doe".to_string()
365            }),
366            "Should have git user name event"
367        );
368        assert!(
369            events.contains(&CliEvent::GitUserEmailSet {
370                email: "john@example.com".to_string()
371            }),
372            "Should have git user email event"
373        );
374    }
375
376    #[test]
377    fn test_args_to_events_streaming_metrics() {
378        let args = Args::parse_from(["ralph", "--show-streaming-metrics"]);
379        let events = args_to_events(&args);
380
381        assert!(
382            events.contains(&CliEvent::StreamingMetricsEnabled),
383            "Should have streaming metrics event"
384        );
385    }
386
387    #[test]
388    fn test_args_parses_pause_on_exit_mode() {
389        let args = Args::try_parse_from(["ralph", "--pause-on-exit", "always"])
390            .expect("pause-on-exit should parse");
391
392        assert_eq!(args.pause_on_exit, crate::cli::PauseOnExitMode::Always);
393    }
394}