1use crate::cache::models::{CachedCommand, CachedSpec};
4use crate::error::Error;
5use crate::utils::to_kebab_case;
6use std::collections::BTreeMap;
7use std::fmt::Write;
8
9pub struct DocumentationGenerator {
11 specs: BTreeMap<String, CachedSpec>,
12}
13
14impl DocumentationGenerator {
15 #[must_use]
17 pub const fn new(specs: BTreeMap<String, CachedSpec>) -> Self {
18 Self { specs }
19 }
20
21 pub fn generate_command_help(
27 &self,
28 api_name: &str,
29 tag: &str,
30 operation_id: &str,
31 ) -> Result<String, Error> {
32 let spec = self
33 .specs
34 .get(api_name)
35 .ok_or_else(|| Error::spec_not_found(api_name))?;
36
37 let command = spec
38 .commands
39 .iter()
40 .find(|cmd| to_kebab_case(&cmd.operation_id) == operation_id)
41 .ok_or_else(|| {
42 Error::spec_not_found(format!(
43 "Operation '{operation_id}' not found in API '{api_name}'"
44 ))
45 })?;
46
47 let mut help = String::new();
48
49 Self::add_command_header(&mut help, command);
51 Self::add_usage_section(&mut help, api_name, tag, operation_id);
52 Self::add_parameters_section(&mut help, command);
53 Self::add_request_body_section(&mut help, command);
54 Self::add_examples_section(&mut help, api_name, tag, operation_id, command);
55 Self::add_responses_section(&mut help, command);
56 Self::add_authentication_section(&mut help, command);
57 Self::add_metadata_section(&mut help, command);
58
59 Ok(help)
60 }
61
62 fn add_command_header(help: &mut String, command: &CachedCommand) {
64 write!(
65 help,
66 "# {} {}\n\n",
67 command.method.to_uppercase(),
68 command.path
69 )
70 .ok();
71
72 if let Some(summary) = &command.summary {
73 write!(help, "**{summary}**\n\n").ok();
74 }
75
76 if let Some(description) = &command.description {
77 write!(help, "{description}\n\n").ok();
78 }
79 }
80
81 fn add_usage_section(help: &mut String, api_name: &str, tag: &str, operation_id: &str) {
83 help.push_str("## Usage\n\n");
84 write!(
85 help,
86 "```bash\naperture api {api_name} {tag} {operation_id}\n```\n\n"
87 )
88 .ok();
89 }
90
91 fn add_parameters_section(help: &mut String, command: &CachedCommand) {
93 if !command.parameters.is_empty() {
94 help.push_str("## Parameters\n\n");
95 for param in &command.parameters {
96 let required_badge = if param.required {
97 " **(required)**"
98 } else {
99 ""
100 };
101 let param_type = param.schema_type.as_deref().unwrap_or("string");
102 writeln!(
103 help,
104 "- `--{}` ({}){} - {}",
105 to_kebab_case(¶m.name),
106 param_type,
107 required_badge,
108 param.description.as_deref().unwrap_or("No description")
109 )
110 .ok();
111 }
112 help.push('\n');
113 }
114 }
115
116 fn add_request_body_section(help: &mut String, command: &CachedCommand) {
118 if let Some(ref body) = command.request_body {
119 help.push_str("## Request Body\n\n");
120 if let Some(ref description) = body.description {
121 write!(help, "{description}\n\n").ok();
122 }
123 write!(help, "Required: {}\n\n", body.required).ok();
124 }
125 }
126
127 fn add_examples_section(
129 help: &mut String,
130 api_name: &str,
131 tag: &str,
132 operation_id: &str,
133 command: &CachedCommand,
134 ) {
135 if command.examples.is_empty() {
136 help.push_str("## Example\n\n");
137 help.push_str(&Self::generate_basic_example(
138 api_name,
139 tag,
140 operation_id,
141 command,
142 ));
143 } else {
144 help.push_str("## Examples\n\n");
145 for (i, example) in command.examples.iter().enumerate() {
146 write!(help, "### Example {}\n\n", i + 1).ok();
147 write!(help, "**{}**\n\n", example.description).ok();
148 if let Some(ref explanation) = example.explanation {
149 write!(help, "{explanation}\n\n").ok();
150 }
151 write!(help, "```bash\n{}\n```\n\n", example.command_line).ok();
152 }
153 }
154 }
155
156 fn add_responses_section(help: &mut String, command: &CachedCommand) {
158 if !command.responses.is_empty() {
159 help.push_str("## Responses\n\n");
160 for response in &command.responses {
161 writeln!(
162 help,
163 "- **{}**: {}",
164 response.status_code,
165 response.description.as_deref().unwrap_or("No description")
166 )
167 .ok();
168 }
169 help.push('\n');
170 }
171 }
172
173 fn add_authentication_section(help: &mut String, command: &CachedCommand) {
175 if !command.security_requirements.is_empty() {
176 help.push_str("## Authentication\n\n");
177 help.push_str("This operation requires authentication. Available schemes:\n\n");
178 for scheme_name in &command.security_requirements {
179 writeln!(help, "- {scheme_name}").ok();
180 }
181 help.push('\n');
182 }
183 }
184
185 fn add_metadata_section(help: &mut String, command: &CachedCommand) {
187 if command.deprecated {
188 help.push_str("ā ļø **This operation is deprecated**\n\n");
189 }
190
191 if let Some(ref docs_url) = command.external_docs_url {
192 write!(help, "š **External Documentation**: {docs_url}\n\n").ok();
193 }
194 }
195
196 fn generate_basic_example(
198 api_name: &str,
199 tag: &str,
200 operation_id: &str,
201 command: &CachedCommand,
202 ) -> String {
203 let mut example = format!("```bash\naperture api {api_name} {tag} {operation_id}");
204
205 for param in &command.parameters {
207 if param.required {
208 let param_type = param.schema_type.as_deref().unwrap_or("string");
209 let example_value = Self::generate_example_value(param_type);
210 write!(
211 example,
212 " --{} {}",
213 to_kebab_case(¶m.name),
214 example_value
215 )
216 .ok();
217 }
218 }
219
220 if let Some(ref body) = command.request_body {
222 if body.required {
223 example.push_str(" --body '{\"key\": \"value\"}'");
224 }
225 }
226
227 example.push_str("\n```\n\n");
228 example
229 }
230
231 fn generate_example_value(param_type: &str) -> &'static str {
233 match param_type.to_lowercase().as_str() {
234 "string" => "\"example\"",
235 "integer" | "number" => "123",
236 "boolean" => "true",
237 "array" => "[\"item1\",\"item2\"]",
238 _ => "\"value\"",
239 }
240 }
241
242 pub fn generate_api_overview(&self, api_name: &str) -> Result<String, Error> {
247 let spec = self
248 .specs
249 .get(api_name)
250 .ok_or_else(|| Error::spec_not_found(api_name))?;
251
252 let mut overview = String::new();
253
254 write!(overview, "# {} API\n\n", spec.name).ok();
256 writeln!(overview, "**Version**: {}", spec.version).ok();
257
258 if let Some(ref base_url) = spec.base_url {
259 writeln!(overview, "**Base URL**: {base_url}").ok();
260 }
261 overview.push('\n');
262
263 let total_operations = spec.commands.len();
265 let mut method_counts = BTreeMap::new();
266 let mut tag_counts = BTreeMap::new();
267
268 for command in &spec.commands {
269 *method_counts.entry(command.method.clone()).or_insert(0) += 1;
270
271 let primary_tag = command
272 .tags
273 .first()
274 .map_or_else(|| "untagged".to_string(), |t| to_kebab_case(t));
275 *tag_counts.entry(primary_tag).or_insert(0) += 1;
276 }
277
278 overview.push_str("## Statistics\n\n");
279 writeln!(overview, "- **Total Operations**: {total_operations}").ok();
280 overview.push_str("- **Methods**:\n");
281 for (method, count) in method_counts {
282 writeln!(overview, " - {method}: {count}").ok();
283 }
284 overview.push_str("- **Categories**:\n");
285 for (tag, count) in tag_counts {
286 writeln!(overview, " - {tag}: {count}").ok();
287 }
288 overview.push('\n');
289
290 overview.push_str("## Quick Start\n\n");
292 write!(
293 overview,
294 "List all available commands:\n```bash\naperture list-commands {api_name}\n```\n\n"
295 )
296 .ok();
297
298 write!(
299 overview,
300 "Search for specific operations:\n```bash\naperture search \"keyword\" --api {api_name}\n```\n\n"
301 ).ok();
302
303 if !spec.commands.is_empty() {
305 overview.push_str("## Sample Operations\n\n");
306 for (i, command) in spec.commands.iter().take(3).enumerate() {
307 let tag = command
308 .tags
309 .first()
310 .map_or_else(|| "api".to_string(), |t| to_kebab_case(t));
311 let operation_kebab = to_kebab_case(&command.operation_id);
312 write!(
313 overview,
314 "{}. **{}** ({})\n ```bash\n aperture api {api_name} {tag} {operation_kebab}\n ```\n {}\n\n",
315 i + 1,
316 command.summary.as_deref().unwrap_or(&command.operation_id),
317 command.method.to_uppercase(),
318 command.description.as_deref().unwrap_or("No description")
319 ).ok();
320 }
321 }
322
323 Ok(overview)
324 }
325
326 #[must_use]
328 pub fn generate_interactive_menu(&self) -> String {
329 let mut menu = String::new();
330
331 menu.push_str("# Aperture Interactive Help\n\n");
332 menu.push_str("Welcome to Aperture! Here are some ways to get started:\n\n");
333
334 if self.specs.is_empty() {
336 menu.push_str("## No APIs Configured\n\n");
337 menu.push_str("Get started by adding an API specification:\n");
338 menu.push_str("```bash\naperture config add myapi ./openapi.yaml\n```\n\n");
339 } else {
340 menu.push_str("## Your APIs\n\n");
341 for (api_name, spec) in &self.specs {
342 let operation_count = spec.commands.len();
343 writeln!(
344 menu,
345 "- **{api_name}** ({operation_count} operations) - Version {}",
346 spec.version
347 )
348 .ok();
349 }
350 menu.push('\n');
351 }
352
353 menu.push_str("## Common Commands\n\n");
355 menu.push_str("- `aperture config list` - List all configured APIs\n");
356 menu.push_str("- `aperture search <term>` - Search across all APIs\n");
357 menu.push_str("- `aperture list-commands <api>` - Show available commands for an API\n");
358 menu.push_str("- `aperture exec <shortcut>` - Execute using shortcuts\n");
359 menu.push_str("- `aperture api <api> --help` - Get help for an API\n\n");
360
361 menu.push_str("## Tips\n\n");
363 menu.push_str("- Use `--describe-json` for machine-readable capability information\n");
364 menu.push_str("- Use `--dry-run` to see what request would be made without executing\n");
365 menu.push_str("- Use `--json-errors` for structured error output\n");
366 menu.push_str("- Environment variables can be used for authentication (see config)\n\n");
367
368 menu
369 }
370}
371
372pub struct HelpFormatter;
374
375impl HelpFormatter {
376 #[must_use]
378 pub fn format_command_list(spec: &CachedSpec) -> String {
379 let mut output = String::new();
380
381 writeln!(output, "š {} API Commands", spec.name).ok();
383 writeln!(
384 output,
385 " Version: {} | Operations: {}",
386 spec.version,
387 spec.commands.len()
388 )
389 .ok();
390
391 if let Some(ref base_url) = spec.base_url {
392 writeln!(output, " Base URL: {base_url}").ok();
393 }
394 output.push_str(&"ā".repeat(60));
395 output.push('\n');
396
397 let mut tag_groups = BTreeMap::new();
399 for command in &spec.commands {
400 let tag = command
401 .tags
402 .first()
403 .map_or_else(|| "General".to_string(), |t| to_kebab_case(t));
404 tag_groups.entry(tag).or_insert_with(Vec::new).push(command);
405 }
406
407 for (tag, commands) in tag_groups {
408 writeln!(output, "\nš {tag}").ok();
409 output.push_str(&"ā".repeat(40));
410 output.push('\n');
411
412 for command in commands {
413 let operation_kebab = to_kebab_case(&command.operation_id);
414 let method_badge = Self::format_method_badge(&command.method);
415 let description = command
416 .summary
417 .as_ref()
418 .or(command.description.as_ref())
419 .map(|s| format!(" - {}", s.lines().next().unwrap_or(s)))
420 .unwrap_or_default();
421
422 writeln!(
423 output,
424 " {} {} {}{}",
425 method_badge,
426 operation_kebab,
427 if command.deprecated { "ā ļø" } else { "" },
428 description
429 )
430 .ok();
431
432 writeln!(output, " Path: {}", command.path).ok();
434 }
435 }
436
437 output.push('\n');
438 output
439 }
440
441 fn format_method_badge(method: &str) -> String {
443 match method.to_uppercase().as_str() {
444 "GET" => "š GET ".to_string(),
445 "POST" => "š POST ".to_string(),
446 "PUT" => "āļø PUT ".to_string(),
447 "DELETE" => "šļø DELETE".to_string(),
448 "PATCH" => "š§ PATCH ".to_string(),
449 "HEAD" => "šļø HEAD ".to_string(),
450 "OPTIONS" => "āļø OPTIONS".to_string(),
451 _ => format!("š {:<7}", method.to_uppercase()),
452 }
453 }
454}