stackless_stripe_projects/
surface.rs1use crate::error::ProjectsError;
9use crate::stripe::{CommandRunner, StripeProjects};
10
11const TRAVERSAL: &[&[&str]] = &[
18 &[],
19 &["init"],
21 &["build"],
22 &["list"],
23 &["pull"],
24 &["status"],
25 &["services"],
26 &["services", "list"],
27 &["catalog"],
28 &["search"],
29 &["switch-account"],
30 &["add"],
32 &["update"],
33 &["upgrade"],
34 &["downgrade"],
35 &["remove"],
36 &["rotate"],
37 &["link"],
38 &["unlink"],
39 &["open"],
40 &["env"],
42 &["env", "list"],
43 &["env", "show"],
44 &["env", "create"],
45 &["env", "use"],
46 &["env", "update"],
47 &["env", "delete"],
48 &["env", "add"],
49 &["env", "remove"],
50 &["variables"],
51 &["variables", "set"],
52 &["variables", "list"],
53 &["variables", "delete"],
54 &["llm-context"],
55 &["billing"],
57 &["billing", "show"],
58 &["billing", "add"],
59 &["billing", "update"],
60 &["spend"],
61 &["feedback"],
63];
64
65const HEADER_VERSION_PREFIX: &str = "# plugin-version:";
66
67pub async fn plugin_version<R: CommandRunner>(
70 stripe: &StripeProjects<R>,
71) -> Result<String, ProjectsError> {
72 let out = stripe.plain(&["--version"]).await?;
73 out.stdout
74 .lines()
75 .map(str::trim)
76 .find(|line| line.starts_with(|c: char| c.is_ascii_digit()))
77 .map(str::to_owned)
78 .ok_or_else(|| ProjectsError::Unavailable {
79 detail: format!(
80 "`stripe projects --version` printed no version line: {:?}",
81 out.stdout
82 ),
83 })
84}
85
86pub async fn command_surface<R: CommandRunner>(
92 stripe: &StripeProjects<R>,
93) -> Result<String, ProjectsError> {
94 let mut out = String::new();
95 for path in TRAVERSAL {
96 let mut args: Vec<&str> = path.to_vec();
97 args.extend_from_slice(&["--help", "--color", "off"]);
98 let result = stripe.plain(&args).await?;
99 out.push_str(&format!("===== {} =====\n", label(path)));
100 out.push_str(&normalize_block(&result.stdout));
101 out.push_str("\n\n");
102 }
103 Ok(out.trim_end().to_owned())
104}
105
106pub fn surface_header(version: &str) -> String {
109 format!(
110 "# stripe projects command surface\n\
111 {HEADER_VERSION_PREFIX} {version}\n\
112 # DO NOT EDIT — regenerated by CI (mise run stripe-refresh)\n\n"
113 )
114}
115
116pub fn render_surface(version: &str, body: &str) -> String {
118 format!("{}{}\n", surface_header(version), body)
119}
120
121pub fn parse_header_version(text: &str) -> Option<String> {
123 text.lines()
124 .find_map(|line| line.trim().strip_prefix(HEADER_VERSION_PREFIX))
125 .map(|value| value.trim().to_owned())
126}
127
128pub fn command_separators() -> Vec<String> {
132 TRAVERSAL
133 .iter()
134 .map(|path| format!("===== {} =====", label(path)))
135 .collect()
136}
137
138fn label(path: &[&str]) -> String {
139 if path.is_empty() {
140 "stripe projects --help".to_owned()
141 } else {
142 format!("stripe projects {} --help", path.join(" "))
143 }
144}
145
146fn normalize_block(raw: &str) -> String {
147 let mut lines: Vec<&str> = raw.lines().map(str::trim_end).collect();
148 while lines.first().is_some_and(|line| line.is_empty()) {
149 lines.remove(0);
150 }
151 while lines.last().is_some_and(|line| line.is_empty()) {
152 lines.pop();
153 }
154 lines.join("\n")
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::stripe::CommandOutput;
161 use async_trait::async_trait;
162 use std::path::Path;
163
164 struct EchoRunner;
167
168 #[async_trait]
169 impl CommandRunner for EchoRunner {
170 async fn run(&self, args: &[String], _cwd: &Path) -> Result<CommandOutput, ProjectsError> {
171 Ok(CommandOutput {
172 status: 0,
173 stdout: format!("\n\nhelp for: {} \n\n", args.join(" ")),
175 stderr: String::new(),
176 })
177 }
178 }
179
180 fn driver() -> StripeProjects<EchoRunner> {
181 StripeProjects::new(EchoRunner, std::env::temp_dir())
182 }
183
184 #[test]
185 fn label_handles_top_level_and_nested() {
186 assert_eq!(label(&[]), "stripe projects --help");
187 assert_eq!(label(&["add"]), "stripe projects add --help");
188 assert_eq!(
189 label(&["billing", "update"]),
190 "stripe projects billing update --help"
191 );
192 }
193
194 #[test]
195 fn normalize_strips_trailing_ws_and_blank_edges() {
196 assert_eq!(normalize_block("\n\n body \n\n"), " body");
197 assert_eq!(normalize_block("a \nb\t\n"), "a\nb");
198 }
199
200 #[test]
201 fn header_round_trips_version() {
202 let text = render_surface("0.19.0", "===== stripe projects --help =====\nbody");
203 assert_eq!(parse_header_version(&text).as_deref(), Some("0.19.0"));
204 assert!(text.ends_with("body\n"));
205 }
206
207 #[tokio::test]
208 async fn surface_has_one_block_per_traversal_entry_in_order() {
209 let body = command_surface(&driver()).await.unwrap();
210 let seps: Vec<&str> = body.lines().filter(|l| l.starts_with("===== ")).collect();
211 assert_eq!(seps, command_separators());
212 assert!(body.starts_with("===== stripe projects --help =====\n"));
214 assert!(body.contains("help for: --help --color off"));
215 assert!(body.contains("help for: env list --help --color off"));
216 assert!(!body.ends_with('\n'));
218 }
219}