Skip to main content

apcore_cli/
strategy.rs

1// apcore-cli -- Pipeline strategy commands (FE-11).
2// Provides describe-pipeline subcommand showing execution pipeline steps.
3
4use clap::{Arg, Command};
5use serde_json::Value;
6use std::io::IsTerminal;
7
8// ---------------------------------------------------------------------------
9// Preset strategy step definitions
10// ---------------------------------------------------------------------------
11
12/// Known preset pipeline strategies and their steps.
13fn preset_steps(strategy: &str) -> Vec<&'static str> {
14    match strategy {
15        "standard" => vec![
16            "context_creation",
17            "call_chain_guard",
18            "module_lookup",
19            "acl_check",
20            "approval_gate",
21            "middleware_before",
22            "input_validation",
23            "execute",
24            "output_validation",
25            "middleware_after",
26            "return_result",
27        ],
28        "internal" => vec![
29            "context_creation",
30            "call_chain_guard",
31            "module_lookup",
32            "middleware_before",
33            "input_validation",
34            "execute",
35            "output_validation",
36            "middleware_after",
37            "return_result",
38        ],
39        "testing" => vec![
40            "context_creation",
41            "module_lookup",
42            "middleware_before",
43            "input_validation",
44            "execute",
45            "output_validation",
46            "middleware_after",
47            "return_result",
48        ],
49        "performance" => vec![
50            "context_creation",
51            "call_chain_guard",
52            "module_lookup",
53            "acl_check",
54            "approval_gate",
55            "input_validation",
56            "execute",
57            "output_validation",
58            "return_result",
59        ],
60        "minimal" => vec![
61            "context_creation",
62            "module_lookup",
63            "execute",
64            "return_result",
65        ],
66        _ => vec![],
67    }
68}
69
70// ---------------------------------------------------------------------------
71// Command builder
72// ---------------------------------------------------------------------------
73
74/// Build the `describe-pipeline` clap subcommand.
75pub fn describe_pipeline_command() -> Command {
76    Command::new("describe-pipeline")
77        .about("Show the execution pipeline steps for a strategy")
78        .arg(
79            Arg::new("strategy")
80                .long("strategy")
81                .value_parser(["standard", "internal", "testing", "performance", "minimal"])
82                .default_value("standard")
83                .value_name("STRATEGY")
84                .help("Strategy to describe (default: standard)."),
85        )
86        .arg(
87            Arg::new("format")
88                .long("format")
89                .value_parser(["table", "json"])
90                .value_name("FORMAT")
91                .help("Output format."),
92        )
93}
94
95/// Register the describe-pipeline subcommand on the root command.
96pub fn register_pipeline_command(cli: Command) -> Command {
97    cli.subcommand(describe_pipeline_command())
98}
99
100// ---------------------------------------------------------------------------
101// Dispatch
102// ---------------------------------------------------------------------------
103
104/// Dispatch the `describe-pipeline` subcommand.
105pub fn dispatch_describe_pipeline(matches: &clap::ArgMatches) {
106    let strategy = matches
107        .get_one::<String>("strategy")
108        .map(|s| s.as_str())
109        .unwrap_or("standard");
110    let format = matches.get_one::<String>("format").map(|s| s.as_str());
111    let fmt = crate::output::resolve_format(format);
112
113    let steps = preset_steps(strategy);
114
115    // Step metadata: which steps are pure and which are non-removable.
116    let pure_steps = [
117        "context_creation",
118        "call_chain_guard",
119        "module_lookup",
120        "acl_check",
121        "input_validation",
122    ];
123    let non_removable = [
124        "context_creation",
125        "module_lookup",
126        "execute",
127        "return_result",
128    ];
129
130    if fmt == "json" || !std::io::stdout().is_terminal() {
131        let steps_json: Vec<Value> = steps
132            .iter()
133            .enumerate()
134            .map(|(i, s)| {
135                serde_json::json!({
136                    "index": i + 1,
137                    "name": s,
138                    "pure": pure_steps.contains(s),
139                    "removable": !non_removable.contains(s),
140                })
141            })
142            .collect();
143        let payload = serde_json::json!({
144            "strategy": strategy,
145            "step_count": steps.len(),
146            "steps": steps_json,
147        });
148        println!(
149            "{}",
150            serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())
151        );
152    } else {
153        println!("Pipeline: {strategy} ({} steps)\n", steps.len());
154        println!("  #    Step                         Pure   Removable   Timeout");
155        println!("  ---- ---------------------------- ------ ----------- --------");
156        for (i, s) in steps.iter().enumerate() {
157            let pure = if pure_steps.contains(s) { "yes" } else { "no" };
158            let removable = if non_removable.contains(s) {
159                "no"
160            } else {
161                "yes"
162            };
163            println!("  {:<4} {:<28} {:<6} {:<11} --", i + 1, s, pure, removable);
164        }
165    }
166
167    std::process::exit(0);
168}
169
170// ---------------------------------------------------------------------------
171// Unit tests
172// ---------------------------------------------------------------------------
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn test_preset_steps_standard() {
180        let steps = preset_steps("standard");
181        assert_eq!(steps.len(), 11);
182        assert_eq!(steps[0], "context_creation");
183        assert_eq!(steps[7], "execute");
184    }
185
186    #[test]
187    fn test_preset_steps_internal() {
188        let steps = preset_steps("internal");
189        assert_eq!(steps.len(), 9);
190        assert!(!steps.contains(&"acl_check"));
191    }
192
193    #[test]
194    fn test_preset_steps_testing() {
195        let steps = preset_steps("testing");
196        assert_eq!(steps.len(), 8);
197        assert!(!steps.contains(&"call_chain_guard"));
198    }
199
200    #[test]
201    fn test_preset_steps_performance() {
202        let steps = preset_steps("performance");
203        assert_eq!(steps.len(), 9);
204        assert!(!steps.contains(&"middleware_before"));
205    }
206
207    #[test]
208    fn test_preset_steps_unknown() {
209        let steps = preset_steps("unknown");
210        assert!(steps.is_empty());
211    }
212
213    #[test]
214    fn test_describe_pipeline_command_builder() {
215        let cmd = describe_pipeline_command();
216        assert_eq!(cmd.get_name(), "describe-pipeline");
217        let opts: Vec<&str> = cmd.get_opts().filter_map(|a| a.get_long()).collect();
218        assert!(opts.contains(&"strategy"));
219        assert!(opts.contains(&"format"));
220    }
221
222    #[test]
223    fn test_register_pipeline_command() {
224        let root = Command::new("test");
225        let root = register_pipeline_command(root);
226        let subs: Vec<&str> = root.get_subcommands().map(|c| c.get_name()).collect();
227        assert!(subs.contains(&"describe-pipeline"));
228    }
229}