Skip to main content

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            return;
95        }
96
97        help.push_str("## Parameters\n\n");
98        for param in &command.parameters {
99            let required_badge = if param.required {
100                " **(required)**"
101            } else {
102                ""
103            };
104            let param_type = param.schema_type.as_deref().unwrap_or("string");
105            writeln!(
106                help,
107                "- `--{}` ({}){}  - {}",
108                to_kebab_case(&param.name),
109                param_type,
110                required_badge,
111                param.description.as_deref().unwrap_or("No description")
112            )
113            .ok();
114        }
115        help.push('\n');
116    }
117
118    /// Add request body section if present
119    fn add_request_body_section(help: &mut String, command: &CachedCommand) {
120        let Some(ref body) = command.request_body else {
121            return;
122        };
123
124        help.push_str("## Request Body\n\n");
125        if let Some(ref description) = body.description {
126            write!(help, "{description}\n\n").ok();
127        }
128        write!(help, "Required: {}\n\n", body.required).ok();
129    }
130
131    /// Add examples section with command examples
132    fn add_examples_section(
133        help: &mut String,
134        api_name: &str,
135        tag: &str,
136        operation_id: &str,
137        command: &CachedCommand,
138    ) {
139        if command.examples.is_empty() {
140            help.push_str("## Example\n\n");
141            help.push_str(&Self::generate_basic_example(
142                api_name,
143                tag,
144                operation_id,
145                command,
146            ));
147            return;
148        }
149
150        help.push_str("## Examples\n\n");
151        for (i, example) in command.examples.iter().enumerate() {
152            write!(help, "### Example {}\n\n", i + 1).ok();
153            write!(help, "**{}**\n\n", example.description).ok();
154            if let Some(ref explanation) = example.explanation {
155                write!(help, "{explanation}\n\n").ok();
156            }
157            write!(help, "```bash\n{}\n```\n\n", example.command_line).ok();
158        }
159    }
160
161    /// Add responses section if responses exist
162    fn add_responses_section(help: &mut String, command: &CachedCommand) {
163        if !command.responses.is_empty() {
164            help.push_str("## Responses\n\n");
165            for response in &command.responses {
166                writeln!(
167                    help,
168                    "- **{}**: {}",
169                    response.status_code,
170                    response.description.as_deref().unwrap_or("No description")
171                )
172                .ok();
173            }
174            help.push('\n');
175        }
176    }
177
178    /// Add authentication section if security requirements exist
179    fn add_authentication_section(help: &mut String, command: &CachedCommand) {
180        if !command.security_requirements.is_empty() {
181            help.push_str("## Authentication\n\n");
182            help.push_str("This operation requires authentication. Available schemes:\n\n");
183            for scheme_name in &command.security_requirements {
184                writeln!(help, "- {scheme_name}").ok();
185            }
186            help.push('\n');
187        }
188    }
189
190    /// Add metadata section with deprecation and external docs
191    fn add_metadata_section(help: &mut String, command: &CachedCommand) {
192        if command.deprecated {
193            help.push_str("āš ļø  **This operation is deprecated**\n\n");
194        }
195
196        if let Some(ref docs_url) = command.external_docs_url {
197            write!(help, "šŸ“– **External Documentation**: {docs_url}\n\n").ok();
198        }
199    }
200
201    /// Generate a basic example for a command
202    fn generate_basic_example(
203        api_name: &str,
204        tag: &str,
205        operation_id: &str,
206        command: &CachedCommand,
207    ) -> String {
208        let mut example = format!("```bash\naperture api {api_name} {tag} {operation_id}");
209
210        // Add required parameters
211        for param in &command.parameters {
212            if param.required {
213                let param_type = param.schema_type.as_deref().unwrap_or("string");
214                let example_value = Self::generate_example_value(param_type);
215                write!(
216                    example,
217                    " --{} {}",
218                    to_kebab_case(&param.name),
219                    example_value
220                )
221                .ok();
222            }
223        }
224
225        // Add request body if required
226        match command.request_body {
227            Some(ref body) if body.required => {
228                example.push_str(" --body '{\"key\": \"value\"}'");
229            }
230            _ => {}
231        }
232
233        example.push_str("\n```\n\n");
234        example
235    }
236
237    /// Generate example values for different parameter types
238    fn generate_example_value(param_type: &str) -> &'static str {
239        match param_type.to_lowercase().as_str() {
240            "string" => "\"example\"",
241            "integer" | "number" => "123",
242            "boolean" => "true",
243            "array" => "[\"item1\",\"item2\"]",
244            _ => "\"value\"",
245        }
246    }
247
248    /// Generate API overview with statistics
249    ///
250    /// # Errors
251    /// Returns an error if the API is not found
252    pub fn generate_api_overview(&self, api_name: &str) -> Result<String, Error> {
253        let spec = self
254            .specs
255            .get(api_name)
256            .ok_or_else(|| Error::spec_not_found(api_name))?;
257
258        let mut overview = String::new();
259
260        // API header
261        write!(overview, "# {} API\n\n", spec.name).ok();
262        writeln!(overview, "**Version**: {}", spec.version).ok();
263
264        if let Some(ref base_url) = spec.base_url {
265            writeln!(overview, "**Base URL**: {base_url}").ok();
266        }
267        overview.push('\n');
268
269        // Statistics
270        let total_operations = spec.commands.len();
271        let mut method_counts = BTreeMap::new();
272        let mut tag_counts = BTreeMap::new();
273
274        for command in &spec.commands {
275            *method_counts.entry(command.method.clone()).or_insert(0) += 1;
276
277            let primary_tag = command
278                .tags
279                .first()
280                .map_or_else(|| "untagged".to_string(), |t| to_kebab_case(t));
281            *tag_counts.entry(primary_tag).or_insert(0) += 1;
282        }
283
284        overview.push_str("## Statistics\n\n");
285        writeln!(overview, "- **Total Operations**: {total_operations}").ok();
286        overview.push_str("- **Methods**:\n");
287        for (method, count) in method_counts {
288            writeln!(overview, "  - {method}: {count}").ok();
289        }
290        overview.push_str("- **Categories**:\n");
291        for (tag, count) in tag_counts {
292            writeln!(overview, "  - {tag}: {count}").ok();
293        }
294        overview.push('\n');
295
296        // Quick start examples
297        overview.push_str("## Quick Start\n\n");
298        write!(
299            overview,
300            "List all available commands:\n```bash\naperture list-commands {api_name}\n```\n\n"
301        )
302        .ok();
303
304        write!(
305            overview,
306            "Search for specific operations:\n```bash\naperture search \"keyword\" --api {api_name}\n```\n\n"
307        ).ok();
308
309        // Show first few operations as examples
310        if !spec.commands.is_empty() {
311            overview.push_str("## Sample Operations\n\n");
312            for (i, command) in spec.commands.iter().take(3).enumerate() {
313                let tag = command
314                    .tags
315                    .first()
316                    .map_or_else(|| "api".to_string(), |t| to_kebab_case(t));
317                let operation_kebab = to_kebab_case(&command.operation_id);
318                write!(
319                    overview,
320                    "{}. **{}** ({})\n   ```bash\n   aperture api {api_name} {tag} {operation_kebab}\n   ```\n   {}\n\n",
321                    i + 1,
322                    command.summary.as_deref().unwrap_or(&command.operation_id),
323                    command.method.to_uppercase(),
324                    command.description.as_deref().unwrap_or("No description")
325                ).ok();
326            }
327        }
328
329        Ok(overview)
330    }
331
332    /// Generate interactive help menu
333    #[must_use]
334    pub fn generate_interactive_menu(&self) -> String {
335        let mut menu = String::new();
336
337        menu.push_str("# Aperture Interactive Help\n\n");
338        menu.push_str("Welcome to Aperture! Here are some ways to get started:\n\n");
339
340        // Available APIs
341        if self.specs.is_empty() {
342            menu.push_str("## No APIs Configured\n\n");
343            menu.push_str("Get started by adding an API specification:\n");
344            menu.push_str("```bash\naperture config add myapi ./openapi.yaml\n```\n\n");
345        } else {
346            menu.push_str("## Your APIs\n\n");
347            for (api_name, spec) in &self.specs {
348                let operation_count = spec.commands.len();
349                writeln!(
350                    menu,
351                    "- **{api_name}** ({operation_count} operations) - Version {}",
352                    spec.version
353                )
354                .ok();
355            }
356            menu.push('\n');
357        }
358
359        // Common commands
360        menu.push_str("## Common Commands\n\n");
361        menu.push_str("- `aperture config list` - List all configured APIs\n");
362        menu.push_str("- `aperture search <term>` - Search across all APIs\n");
363        menu.push_str("- `aperture list-commands <api>` - Show available commands for an API\n");
364        menu.push_str("- `aperture exec <shortcut>` - Execute using shortcuts\n");
365        menu.push_str("- `aperture api <api> --help` - Get help for an API\n\n");
366
367        // Tips
368        menu.push_str("## Tips\n\n");
369        menu.push_str("- Use `--describe-json` for machine-readable capability information\n");
370        menu.push_str("- Use `--dry-run` to see what request would be made without executing\n");
371        menu.push_str("- Use `--json-errors` for structured error output\n");
372        menu.push_str("- Environment variables can be used for authentication (see config)\n\n");
373
374        menu
375    }
376}
377
378/// Enhanced help formatter for better readability
379pub struct HelpFormatter;
380
381impl HelpFormatter {
382    /// Format command list with enhanced styling
383    #[must_use]
384    pub fn format_command_list(spec: &CachedSpec) -> String {
385        let mut output = String::new();
386
387        // Header with API info
388        writeln!(output, "šŸ“‹ {} API Commands", spec.name).ok();
389        writeln!(
390            output,
391            "   Version: {} | Operations: {}",
392            spec.version,
393            spec.commands.len()
394        )
395        .ok();
396
397        if let Some(ref base_url) = spec.base_url {
398            writeln!(output, "   Base URL: {base_url}").ok();
399        }
400        output.push_str(&"═".repeat(60));
401        output.push('\n');
402
403        // Group by tags
404        let mut tag_groups = BTreeMap::new();
405        for command in &spec.commands {
406            let tag = command
407                .tags
408                .first()
409                .map_or_else(|| "General".to_string(), |t| to_kebab_case(t));
410            tag_groups.entry(tag).or_insert_with(Vec::new).push(command);
411        }
412
413        for (tag, commands) in tag_groups {
414            writeln!(output, "\nšŸ“ {tag}").ok();
415            output.push_str(&"─".repeat(40));
416            output.push('\n');
417
418            for command in commands {
419                let operation_kebab = to_kebab_case(&command.operation_id);
420                let method_badge = Self::format_method_badge(&command.method);
421                let description = command
422                    .summary
423                    .as_ref()
424                    .or(command.description.as_ref())
425                    .map(|s| format!(" - {}", s.lines().next().unwrap_or(s)))
426                    .unwrap_or_default();
427
428                writeln!(
429                    output,
430                    "  {} {} {}{}",
431                    method_badge,
432                    operation_kebab,
433                    if command.deprecated { "āš ļø" } else { "" },
434                    description
435                )
436                .ok();
437
438                // Show path as subdued text
439                writeln!(output, "     Path: {}", command.path).ok();
440            }
441        }
442
443        output.push('\n');
444        output
445    }
446
447    /// Format HTTP method with color/styling
448    fn format_method_badge(method: &str) -> String {
449        match method.to_uppercase().as_str() {
450            "GET" => "šŸ” GET   ".to_string(),
451            "POST" => "šŸ“ POST  ".to_string(),
452            "PUT" => "āœļø  PUT   ".to_string(),
453            "DELETE" => "šŸ—‘ļø  DELETE".to_string(),
454            "PATCH" => "šŸ”§ PATCH ".to_string(),
455            "HEAD" => "šŸ‘ļø  HEAD  ".to_string(),
456            "OPTIONS" => "āš™ļø  OPTIONS".to_string(),
457            _ => format!("šŸ“‹ {:<7}", method.to_uppercase()),
458        }
459    }
460}