aperture_cli/
docs.rs

1//! Documentation and help system for improved CLI discoverability
2
3use crate::cache::models::{CachedCommand, CachedSpec};
4use crate::error::Error;
5use crate::utils::to_kebab_case;
6use std::collections::BTreeMap;
7use std::fmt::Write;
8
9/// Documentation generator for API operations
10pub struct DocumentationGenerator {
11    specs: BTreeMap<String, CachedSpec>,
12}
13
14impl DocumentationGenerator {
15    /// Create a new documentation generator
16    #[must_use]
17    pub const fn new(specs: BTreeMap<String, CachedSpec>) -> Self {
18        Self { specs }
19    }
20
21    /// Generate comprehensive help for a specific command
22    /// Generate command help documentation
23    ///
24    /// # Errors
25    /// Returns an error if the API or operation is not found
26    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        // Build help sections
50        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    /// Add command header with title and description
63    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    /// Add usage section with command syntax
82    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    /// Add parameters section if parameters exist
92    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(&param.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    /// Add request body section if present
117    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    /// Add examples section with command examples
128    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    /// Add responses section if responses exist
157    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    /// Add authentication section if security requirements exist
174    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    /// Add metadata section with deprecation and external docs
186    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    /// Generate a basic example for a command
197    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        // Add required parameters
206        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(&param.name),
214                    example_value
215                )
216                .ok();
217            }
218        }
219
220        // Add request body if required
221        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    /// Generate example values for different parameter types
232    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    /// Generate API overview with statistics
243    ///
244    /// # Errors
245    /// Returns an error if the API is not found
246    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        // API header
255        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        // Statistics
264        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        // Quick start examples
291        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        // Show first few operations as examples
304        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    /// Generate interactive help menu
327    #[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        // Available APIs
335        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        // Common commands
354        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        // Tips
362        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
372/// Enhanced help formatter for better readability
373pub struct HelpFormatter;
374
375impl HelpFormatter {
376    /// Format command list with enhanced styling
377    #[must_use]
378    pub fn format_command_list(spec: &CachedSpec) -> String {
379        let mut output = String::new();
380
381        // Header with API info
382        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        // Group by tags
398        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                // Show path as subdued text
433                writeln!(output, "     Path: {}", command.path).ok();
434            }
435        }
436
437        output.push('\n');
438        output
439    }
440
441    /// Format HTTP method with color/styling
442    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}