1use clap::{Arg, Command};
5use serde_json::Value;
6use std::io::IsTerminal;
7
8fn 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
70pub 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
95pub fn register_pipeline_command(cli: Command) -> Command {
97 cli.subcommand(describe_pipeline_command())
98}
99
100pub 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 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#[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}