help_probe/
builder.rs

1use crate::model::{OptionType, ProbeResult};
2
3/// Programming languages for which we can generate command builders.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum Language {
6    /// Rust programming language
7    Rust,
8    /// Python programming language
9    Python,
10    /// JavaScript programming language
11    JavaScript,
12    /// TypeScript programming language
13    TypeScript,
14}
15
16impl Language {
17    /// Parse language name from string (case-insensitive).
18    ///
19    /// # Examples
20    ///
21    /// ```
22    /// use help_probe::builder::Language;
23    ///
24    /// assert_eq!(Language::from_str("rust"), Some(Language::Rust));
25    /// assert_eq!(Language::from_str("RUST"), Some(Language::Rust));
26    /// assert_eq!(Language::from_str("python"), Some(Language::Python));
27    /// assert_eq!(Language::from_str("py"), Some(Language::Python));
28    /// assert_eq!(Language::from_str("javascript"), Some(Language::JavaScript));
29    /// assert_eq!(Language::from_str("js"), Some(Language::JavaScript));
30    /// assert_eq!(Language::from_str("typescript"), Some(Language::TypeScript));
31    /// assert_eq!(Language::from_str("ts"), Some(Language::TypeScript));
32    /// assert_eq!(Language::from_str("invalid"), None);
33    /// ```
34    pub fn from_str(s: &str) -> Option<Self> {
35        match s.to_lowercase().as_str() {
36            "rust" => Some(Language::Rust),
37            "python" | "py" => Some(Language::Python),
38            "javascript" | "js" => Some(Language::JavaScript),
39            "typescript" | "ts" => Some(Language::TypeScript),
40            _ => None,
41        }
42    }
43}
44
45/// Generate type-safe command builder code for a command.
46///
47/// This generates code that allows programmatic construction of commands
48/// with type safety and validation.
49///
50/// # Examples
51///
52/// ```
53/// use help_probe::{builder::{generate_command_builder, Language}, model::ProbeResult};
54///
55/// let result = ProbeResult {
56///     command: "mytool".to_string(),
57///     args: vec![],
58///     exit_code: Some(0),
59///     timed_out: false,
60///     help_flag_detected: true,
61///     usage_blocks: vec![],
62///     options: vec![],
63///     subcommands: vec![],
64///     arguments: vec![],
65///     examples: vec![],
66///     environment_variables: vec![],
67///     validation_rules: vec![],
68///     raw_stdout: String::new(),
69///     raw_stderr: String::new(),
70/// };
71///
72/// let rust_code = generate_command_builder(&result, Language::Rust);
73/// assert!(rust_code.contains("MytoolBuilder"));
74/// assert!(rust_code.contains("pub fn build"));
75/// ```
76pub fn generate_command_builder(result: &ProbeResult, language: Language) -> String {
77    match language {
78        Language::Rust => generate_rust_builder(result),
79        Language::Python => generate_python_builder(result),
80        Language::JavaScript => generate_javascript_builder(result),
81        Language::TypeScript => generate_typescript_builder(result),
82    }
83}
84
85/// Generate Rust command builder.
86fn generate_rust_builder(result: &ProbeResult) -> String {
87    let cmd_name = sanitize_identifier(&result.command);
88    let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
89
90    let mut code = String::new();
91    code.push_str(&format!(
92        "// Generated command builder for {}\n",
93        result.command
94    ));
95    code.push_str("// Generated by help-probe\n\n");
96
97    code.push_str("use std::process::Command;\n\n");
98
99    code.push_str(&format!("pub struct {} {{\n", builder_name));
100    code.push_str("    command: String,\n");
101    code.push_str("    args: Vec<String>,\n");
102    code.push_str("}\n\n");
103
104    code.push_str(&format!("impl {} {{\n", builder_name));
105    code.push_str("    pub fn new() -> Self {\n");
106    code.push_str(&format!("        Self {{\n"));
107    code.push_str(&format!(
108        "            command: \"{}\".to_string(),\n",
109        result.command
110    ));
111    code.push_str("            args: Vec::new(),\n");
112    code.push_str("        }\n");
113    code.push_str("    }\n\n");
114
115    // Add option methods
116    for opt in &result.options {
117        for long_flag in &opt.long_flags {
118            let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
119
120            if opt.takes_argument {
121                let arg_type = match opt.option_type {
122                    OptionType::Number => "i64",
123                    OptionType::Path => "&str",
124                    OptionType::String => "&str",
125                    OptionType::Choice => "&str",
126                    OptionType::Boolean => "&str",
127                };
128
129                code.push_str(&format!(
130                    "    pub fn {}(mut self, value: {}) -> Self {{\n",
131                    method_name, arg_type
132                ));
133                code.push_str(&format!(
134                    "        self.args.push(\"{}\".to_string());\n",
135                    long_flag
136                ));
137                code.push_str(&format!("        self.args.push(value.to_string());\n"));
138                code.push_str("        self\n");
139                code.push_str("    }\n\n");
140            } else {
141                code.push_str(&format!(
142                    "    pub fn {}(mut self) -> Self {{\n",
143                    method_name
144                ));
145                code.push_str(&format!(
146                    "        self.args.push(\"{}\".to_string());\n",
147                    long_flag
148                ));
149                code.push_str("        self\n");
150                code.push_str("    }\n\n");
151            }
152        }
153    }
154
155    // Add argument methods
156    for (idx, arg) in result.arguments.iter().enumerate() {
157        let method_name = if arg.variadic {
158            format!("arg{}", idx + 1)
159        } else {
160            sanitize_identifier(&arg.name.to_lowercase())
161        };
162
163        let arg_type = match arg.arg_type {
164            Some(crate::model::ArgumentType::Number) => "i64",
165            Some(crate::model::ArgumentType::Path) => "&str",
166            Some(crate::model::ArgumentType::Url) => "&str",
167            Some(crate::model::ArgumentType::Email) => "&str",
168            _ => "&str",
169        };
170
171        if arg.variadic {
172            code.push_str(&format!(
173                "    pub fn {}(mut self, values: Vec<{}>) -> Self {{\n",
174                method_name, arg_type
175            ));
176            code.push_str("        for value in values {\n");
177            code.push_str("            self.args.push(value.to_string());\n");
178            code.push_str("        }\n");
179            code.push_str("        self\n");
180            code.push_str("    }\n\n");
181        } else {
182            code.push_str(&format!(
183                "    pub fn {}(mut self, value: {}) -> Self {{\n",
184                method_name, arg_type
185            ));
186            code.push_str(&format!("        self.args.push(value.to_string());\n"));
187            code.push_str("        self\n");
188            code.push_str("    }\n\n");
189        }
190    }
191
192    // Add subcommand methods
193    for subcmd in &result.subcommands {
194        let method_name = sanitize_identifier(&subcmd.name);
195        code.push_str(&format!(
196            "    pub fn {}(mut self) -> Self {{\n",
197            method_name
198        ));
199        code.push_str(&format!(
200            "        self.args.push(\"{}\".to_string());\n",
201            subcmd.name
202        ));
203        code.push_str("        self\n");
204        code.push_str("    }\n\n");
205    }
206
207    // Add build/execute method
208    code.push_str("    pub fn build(&self) -> Command {\n");
209    code.push_str("        let mut cmd = Command::new(&self.command);\n");
210    code.push_str("        cmd.args(&self.args);\n");
211    code.push_str("        cmd\n");
212    code.push_str("    }\n\n");
213
214    code.push_str("    pub fn execute(&self) -> std::io::Result<std::process::Output> {\n");
215    code.push_str("        self.build().output()\n");
216    code.push_str("    }\n");
217
218    code.push_str("}\n");
219
220    code
221}
222
223/// Generate Python command builder.
224fn generate_python_builder(result: &ProbeResult) -> String {
225    let cmd_name = sanitize_identifier(&result.command);
226    let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
227
228    let mut code = String::new();
229    code.push_str(&format!(
230        "# Generated command builder for {}\n",
231        result.command
232    ));
233    code.push_str("# Generated by help-probe\n\n");
234
235    code.push_str("import subprocess\nfrom typing import List, Optional, Union\n\n");
236
237    code.push_str(&format!("class {}:\n", builder_name));
238    code.push_str("    def __init__(self):\n");
239    code.push_str(&format!("        self.command = \"{}\"\n", result.command));
240    code.push_str("        self.args: List[str] = []\n\n");
241
242    // Add option methods
243    for opt in &result.options {
244        for long_flag in &opt.long_flags {
245            let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
246
247            if opt.takes_argument {
248                code.push_str(&format!(
249                    "    def {}(self, value: str) -> '{}':\n",
250                    method_name, builder_name
251                ));
252                code.push_str(&format!("        self.args.append(\"{}\")\n", long_flag));
253                code.push_str("        self.args.append(str(value))\n");
254                code.push_str("        return self\n\n");
255            } else {
256                code.push_str(&format!(
257                    "    def {}(self) -> '{}':\n",
258                    method_name, builder_name
259                ));
260                code.push_str(&format!("        self.args.append(\"{}\")\n", long_flag));
261                code.push_str("        return self\n\n");
262            }
263        }
264    }
265
266    // Add argument methods
267    for (idx, arg) in result.arguments.iter().enumerate() {
268        let method_name = if arg.variadic {
269            format!("arg{}", idx + 1)
270        } else {
271            sanitize_identifier(&arg.name.to_lowercase())
272        };
273
274        if arg.variadic {
275            code.push_str(&format!(
276                "    def {}(self, values: List[str]) -> '{}':\n",
277                method_name, builder_name
278            ));
279            code.push_str("        for value in values:\n");
280            code.push_str("            self.args.append(str(value))\n");
281            code.push_str("        return self\n\n");
282        } else {
283            code.push_str(&format!(
284                "    def {}(self, value: str) -> '{}':\n",
285                method_name, builder_name
286            ));
287            code.push_str("        self.args.append(str(value))\n");
288            code.push_str("        return self\n\n");
289        }
290    }
291
292    // Add subcommand methods
293    for subcmd in &result.subcommands {
294        let method_name = sanitize_identifier(&subcmd.name);
295        code.push_str(&format!(
296            "    def {}(self) -> '{}':\n",
297            method_name, builder_name
298        ));
299        code.push_str(&format!("        self.args.append(\"{}\")\n", subcmd.name));
300        code.push_str("        return self\n\n");
301    }
302
303    // Add build/execute methods
304    code.push_str("    def build(self) -> List[str]:\n");
305    code.push_str("        return [self.command] + self.args\n\n");
306
307    code.push_str("    def execute(self) -> subprocess.CompletedProcess:\n");
308    code.push_str("        return subprocess.run(self.build(), capture_output=True, text=True)\n");
309
310    code
311}
312
313/// Generate JavaScript command builder.
314fn generate_javascript_builder(result: &ProbeResult) -> String {
315    let cmd_name = sanitize_identifier(&result.command);
316    let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
317
318    let mut code = String::new();
319    code.push_str(&format!(
320        "// Generated command builder for {}\n",
321        result.command
322    ));
323    code.push_str("// Generated by help-probe\n\n");
324
325    code.push_str("const { spawn } = require('child_process');\n\n");
326
327    code.push_str(&format!("class {} {{\n", builder_name));
328    code.push_str("    constructor() {\n");
329    code.push_str(&format!("        this.command = \"{}\";\n", result.command));
330    code.push_str("        this.args = [];\n");
331    code.push_str("    }\n\n");
332
333    // Add option methods
334    for opt in &result.options {
335        for long_flag in &opt.long_flags {
336            let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
337
338            if opt.takes_argument {
339                code.push_str(&format!("    {}(value) {{\n", method_name));
340                code.push_str(&format!("        this.args.push(\"{}\");\n", long_flag));
341                code.push_str("        this.args.push(String(value));\n");
342                code.push_str("        return this;\n");
343                code.push_str("    }\n\n");
344            } else {
345                code.push_str(&format!("    {}() {{\n", method_name));
346                code.push_str(&format!("        this.args.push(\"{}\");\n", long_flag));
347                code.push_str("        return this;\n");
348                code.push_str("    }\n\n");
349            }
350        }
351    }
352
353    // Add argument methods
354    for (idx, arg) in result.arguments.iter().enumerate() {
355        let method_name = if arg.variadic {
356            format!("arg{}", idx + 1)
357        } else {
358            sanitize_identifier(&arg.name.to_lowercase())
359        };
360
361        if arg.variadic {
362            code.push_str(&format!("    {}(values) {{\n", method_name));
363            code.push_str("        for (const value of values) {\n");
364            code.push_str("            this.args.push(String(value));\n");
365            code.push_str("        }\n");
366            code.push_str("        return this;\n");
367            code.push_str("    }\n\n");
368        } else {
369            code.push_str(&format!("    {}(value) {{\n", method_name));
370            code.push_str("        this.args.push(String(value));\n");
371            code.push_str("        return this;\n");
372            code.push_str("    }\n\n");
373        }
374    }
375
376    // Add subcommand methods
377    for subcmd in &result.subcommands {
378        let method_name = sanitize_identifier(&subcmd.name);
379        code.push_str(&format!("    {}() {{\n", method_name));
380        code.push_str(&format!("        this.args.push(\"{}\");\n", subcmd.name));
381        code.push_str("        return this;\n");
382        code.push_str("    }\n\n");
383    }
384
385    // Add build/execute methods
386    code.push_str("    build() {\n");
387    code.push_str("        return [this.command, ...this.args];\n");
388    code.push_str("    }\n\n");
389
390    code.push_str("    execute() {\n");
391    code.push_str("        return new Promise((resolve, reject) => {\n");
392    code.push_str("            const proc = spawn(this.command, this.args);\n");
393    code.push_str("            let stdout = '';\n");
394    code.push_str("            let stderr = '';\n");
395    code.push_str("            proc.stdout.on('data', (data) => { stdout += data; });\n");
396    code.push_str("            proc.stderr.on('data', (data) => { stderr += data; });\n");
397    code.push_str("            proc.on('close', (code) => {\n");
398    code.push_str("                resolve({ code, stdout, stderr });\n");
399    code.push_str("            });\n");
400    code.push_str("            proc.on('error', reject);\n");
401    code.push_str("        });\n");
402    code.push_str("    }\n");
403
404    code.push_str("}\n\n");
405    code.push_str(&format!("module.exports = {};\n", builder_name));
406
407    code
408}
409
410/// Generate TypeScript command builder.
411fn generate_typescript_builder(result: &ProbeResult) -> String {
412    let cmd_name = sanitize_identifier(&result.command);
413    let builder_name = format!("{}Builder", capitalize_first(&cmd_name));
414
415    let mut code = String::new();
416    code.push_str(&format!(
417        "// Generated command builder for {}\n",
418        result.command
419    ));
420    code.push_str("// Generated by help-probe\n\n");
421
422    code.push_str("import { spawn } from 'child_process';\n\n");
423
424    code.push_str(&format!("export class {} {{\n", builder_name));
425    code.push_str("    private command: string;\n");
426    code.push_str("    private args: string[];\n\n");
427
428    code.push_str("    constructor() {\n");
429    code.push_str(&format!("        this.command = \"{}\";\n", result.command));
430    code.push_str("        this.args = [];\n");
431    code.push_str("    }\n\n");
432
433    // Add option methods
434    for opt in &result.options {
435        for long_flag in &opt.long_flags {
436            let method_name = sanitize_identifier(&long_flag.trim_start_matches("--"));
437
438            if opt.takes_argument {
439                code.push_str(&format!(
440                    "    {}(value: string): {} {{\n",
441                    method_name, builder_name
442                ));
443                code.push_str(&format!("        this.args.push(\"{}\");\n", long_flag));
444                code.push_str("        this.args.push(value);\n");
445                code.push_str("        return this;\n");
446                code.push_str("    }\n\n");
447            } else {
448                code.push_str(&format!("    {}(): {} {{\n", method_name, builder_name));
449                code.push_str(&format!("        this.args.push(\"{}\");\n", long_flag));
450                code.push_str("        return this;\n");
451                code.push_str("    }\n\n");
452            }
453        }
454    }
455
456    // Add argument methods
457    for (idx, arg) in result.arguments.iter().enumerate() {
458        let method_name = if arg.variadic {
459            format!("arg{}", idx + 1)
460        } else {
461            sanitize_identifier(&arg.name.to_lowercase())
462        };
463
464        if arg.variadic {
465            code.push_str(&format!(
466                "    {}(values: string[]): {} {{\n",
467                method_name, builder_name
468            ));
469            code.push_str("        this.args.push(...values);\n");
470            code.push_str("        return this;\n");
471            code.push_str("    }\n\n");
472        } else {
473            code.push_str(&format!(
474                "    {}(value: string): {} {{\n",
475                method_name, builder_name
476            ));
477            code.push_str("        this.args.push(value);\n");
478            code.push_str("        return this;\n");
479            code.push_str("    }\n\n");
480        }
481    }
482
483    // Add subcommand methods
484    for subcmd in &result.subcommands {
485        let method_name = sanitize_identifier(&subcmd.name);
486        code.push_str(&format!("    {}(): {} {{\n", method_name, builder_name));
487        code.push_str(&format!("        this.args.push(\"{}\");\n", subcmd.name));
488        code.push_str("        return this;\n");
489        code.push_str("    }\n\n");
490    }
491
492    // Add build/execute methods
493    code.push_str("    build(): string[] {\n");
494    code.push_str("        return [this.command, ...this.args];\n");
495    code.push_str("    }\n\n");
496
497    code.push_str(
498        "    execute(): Promise<{ code: number | null; stdout: string; stderr: string }> {\n",
499    );
500    code.push_str("        return new Promise((resolve, reject) => {\n");
501    code.push_str("            const proc = spawn(this.command, this.args);\n");
502    code.push_str("            let stdout = '';\n");
503    code.push_str("            let stderr = '';\n");
504    code.push_str(
505        "            proc.stdout?.on('data', (data) => { stdout += data.toString(); });\n",
506    );
507    code.push_str(
508        "            proc.stderr?.on('data', (data) => { stderr += data.toString(); });\n",
509    );
510    code.push_str("            proc.on('close', (code) => {\n");
511    code.push_str("                resolve({ code, stdout, stderr });\n");
512    code.push_str("            });\n");
513    code.push_str("            proc.on('error', reject);\n");
514    code.push_str("        });\n");
515    code.push_str("    }\n");
516
517    code.push_str("}\n");
518
519    code
520}
521
522/// Sanitize a string to be a valid identifier.
523fn sanitize_identifier(s: &str) -> String {
524    s.chars()
525        .map(|c| {
526            if c.is_alphanumeric() || c == '_' {
527                c
528            } else {
529                '_'
530            }
531        })
532        .collect::<String>()
533        .trim_start_matches('_')
534        .to_string()
535}
536
537/// Capitalize the first character of a string.
538fn capitalize_first(s: &str) -> String {
539    let mut chars = s.chars();
540    match chars.next() {
541        None => String::new(),
542        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
543    }
544}