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 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(¶m.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 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 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 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 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 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 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 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(¶m.name),
219 example_value
220 )
221 .ok();
222 }
223 }
224
225 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 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 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 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 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 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 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 #[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 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 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 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
378pub struct HelpFormatter;
380
381impl HelpFormatter {
382 #[must_use]
384 pub fn format_command_list(spec: &CachedSpec) -> String {
385 let mut output = String::new();
386
387 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 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 writeln!(output, " Path: {}", command.path).ok();
440 }
441 }
442
443 output.push('\n');
444 output
445 }
446
447 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}