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