1use apcore::StrategyInfo;
5use clap::{Arg, Command};
6use serde_json::Value;
7use std::io::IsTerminal;
8
9fn get_strategy_info(strategy: &str) -> Option<StrategyInfo> {
20 match strategy {
21 "standard" => Some(apcore::build_standard_strategy().info()),
22 "internal" => Some(apcore::build_internal_strategy().info()),
23 "testing" => Some(apcore::build_testing_strategy().info()),
24 "performance" => Some(apcore::build_performance_strategy().info()),
25 "minimal" => Some(apcore::build_minimal_strategy().info()),
26 _ => None,
27 }
28}
29
30pub fn describe_pipeline_command() -> Command {
36 Command::new("describe-pipeline")
37 .about("Show the execution pipeline steps for a strategy")
38 .arg(
39 Arg::new("strategy")
40 .long("strategy")
41 .value_parser(["standard", "internal", "testing", "performance", "minimal"])
42 .default_value("standard")
43 .value_name("STRATEGY")
44 .help("Strategy to describe (default: standard)."),
45 )
46 .arg(
47 Arg::new("format")
48 .long("format")
49 .value_parser(["table", "json"])
50 .value_name("FORMAT")
51 .help("Output format."),
52 )
53}
54
55pub(crate) fn register_pipeline_command(cli: Command) -> Command {
57 cli.subcommand(describe_pipeline_command())
58}
59
60pub fn dispatch_describe_pipeline(matches: &clap::ArgMatches) {
66 let strategy = matches
67 .get_one::<String>("strategy")
68 .map(|s| s.as_str())
69 .unwrap_or("standard");
70 let format = matches.get_one::<String>("format").map(|s| s.as_str());
71 let fmt = crate::output::resolve_format(format);
72
73 let info = match get_strategy_info(strategy) {
74 Some(info) => info,
75 None => {
76 eprintln!("Error: Unknown strategy: {strategy}");
77 std::process::exit(2);
78 }
79 };
80
81 let pure_steps = [
83 "context_creation",
84 "call_chain_guard",
85 "module_lookup",
86 "acl_check",
87 "input_validation",
88 ];
89 let non_removable = [
90 "context_creation",
91 "module_lookup",
92 "execute",
93 "return_result",
94 ];
95
96 if fmt == "json" || !std::io::stdout().is_terminal() {
97 let steps_json: Vec<Value> = info
98 .step_names
99 .iter()
100 .enumerate()
101 .map(|(i, s)| {
102 serde_json::json!({
103 "index": i + 1,
104 "name": s,
105 "pure": pure_steps.contains(&s.as_str()),
106 "removable": !non_removable.contains(&s.as_str()),
107 })
108 })
109 .collect();
110 let payload = serde_json::json!({
111 "strategy": info.name,
112 "step_count": info.step_count,
113 "steps": steps_json,
114 });
115 println!(
116 "{}",
117 serde_json::to_string_pretty(&payload).unwrap_or_else(|_| "{}".to_string())
118 );
119 } else {
120 println!("Pipeline: {} ({} steps)\n", info.name, info.step_count);
121 println!(" # Step Pure Removable Timeout");
122 println!(" ---- ---------------------------- ------ ----------- --------");
123 for (i, s) in info.step_names.iter().enumerate() {
124 let pure = if pure_steps.contains(&s.as_str()) {
125 "yes"
126 } else {
127 "no"
128 };
129 let removable = if non_removable.contains(&s.as_str()) {
130 "no"
131 } else {
132 "yes"
133 };
134 println!(" {:<4} {:<28} {:<6} {:<11} --", i + 1, s, pure, removable);
135 }
136 }
137
138 std::process::exit(0);
139}
140
141#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_get_strategy_info_standard() {
151 let info = get_strategy_info("standard").expect("standard strategy must exist");
152 assert_eq!(info.step_count, 11);
153 assert_eq!(info.step_names[0], "context_creation");
154 assert!(info.step_names.contains(&"execute".to_string()));
155 assert_eq!(info.name, "standard");
156 }
157
158 #[test]
159 fn test_get_strategy_info_internal() {
160 let info = get_strategy_info("internal").expect("internal strategy must exist");
161 assert_eq!(info.step_count, 9);
162 assert!(!info.step_names.contains(&"acl_check".to_string()));
163 }
164
165 #[test]
166 fn test_get_strategy_info_testing() {
167 let info = get_strategy_info("testing").expect("testing strategy must exist");
168 assert_eq!(info.step_count, 8);
169 assert!(!info.step_names.contains(&"call_chain_guard".to_string()));
170 }
171
172 #[test]
173 fn test_get_strategy_info_performance() {
174 let info = get_strategy_info("performance").expect("performance strategy must exist");
175 assert_eq!(info.step_count, 9);
176 assert!(!info.step_names.contains(&"middleware_before".to_string()));
177 }
178
179 #[test]
180 fn test_get_strategy_info_minimal() {
181 let info = get_strategy_info("minimal").expect("minimal strategy must exist");
182 assert!(info.step_count <= 4);
183 assert!(info.step_names.contains(&"execute".to_string()));
184 }
185
186 #[test]
187 fn test_get_strategy_info_unknown_returns_none() {
188 assert!(get_strategy_info("unknown").is_none());
189 assert!(get_strategy_info("").is_none());
190 }
191
192 #[test]
193 fn test_describe_pipeline_command_builder() {
194 let cmd = describe_pipeline_command();
195 assert_eq!(cmd.get_name(), "describe-pipeline");
196 let opts: Vec<&str> = cmd.get_opts().filter_map(|a| a.get_long()).collect();
197 assert!(opts.contains(&"strategy"));
198 assert!(opts.contains(&"format"));
199 }
200
201 #[test]
202 fn test_register_pipeline_command() {
203 let root = Command::new("test");
204 let root = register_pipeline_command(root);
205 let subs: Vec<&str> = root.get_subcommands().map(|c| c.get_name()).collect();
206 assert!(subs.contains(&"describe-pipeline"));
207 }
208}