baobao_codegen_typescript/adapters/
boune.rs

1//! Boune CLI framework adapter for TypeScript/Bun.
2
3use baobao_codegen::{
4    adapters::{
5        CliAdapter, CliInfo, CommandMeta, Dependency, DispatchInfo, ImportSpec,
6        input_type_to_arg_type,
7    },
8    builder::CodeFragment,
9};
10use baobao_core::ArgType;
11use baobao_ir::{Input, InputKind};
12use baobao_manifest::ArgType as ManifestArgType;
13
14use crate::{
15    BOUNE_VERSION,
16    ast::{ArrowFn, JsArray, JsObject},
17};
18
19/// Boune adapter for generating TypeScript CLI code targeting Bun runtime.
20#[derive(Debug, Clone, Default)]
21pub struct BouneAdapter;
22
23impl BouneAdapter {
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// Convert manifest ArgType to core ArgType.
29    fn convert_manifest_arg_type(arg_type: &ManifestArgType) -> ArgType {
30        match arg_type {
31            ManifestArgType::String => ArgType::String,
32            ManifestArgType::Int => ArgType::Int,
33            ManifestArgType::Float => ArgType::Float,
34            ManifestArgType::Bool => ArgType::Bool,
35            ManifestArgType::Path => ArgType::Path,
36        }
37    }
38
39    /// Build an argument object schema for boune's declarative API using manifest types.
40    pub fn build_argument_schema_manifest(
41        &self,
42        arg_type: &ManifestArgType,
43        required: bool,
44        default: Option<&toml::Value>,
45        description: Option<&str>,
46        choices: Option<&[String]>,
47    ) -> JsObject {
48        self.build_argument_schema(
49            Self::convert_manifest_arg_type(arg_type),
50            required,
51            default,
52            description,
53            choices,
54        )
55    }
56
57    /// Build an argument object schema for boune's declarative API.
58    ///
59    /// Generates: `{ type: "string", required: true, description: "...", choices: [...] as const }`
60    pub fn build_argument_schema(
61        &self,
62        arg_type: ArgType,
63        required: bool,
64        default: Option<&toml::Value>,
65        description: Option<&str>,
66        choices: Option<&[String]>,
67    ) -> JsObject {
68        let boune_type = self.map_arg_type(arg_type);
69
70        JsObject::new()
71            .string("type", boune_type)
72            .raw_if(required && default.is_none(), "required", "true")
73            .toml_opt("default", default)
74            .string_opt("description", description)
75            .array_opt(
76                "choices",
77                choices.map(|c| JsArray::from_strings(c).as_const()),
78            )
79    }
80
81    /// Build an option object schema for boune's declarative API using manifest types.
82    pub fn build_option_schema_manifest(
83        &self,
84        flag_type: &ManifestArgType,
85        short: Option<char>,
86        default: Option<&toml::Value>,
87        description: Option<&str>,
88        choices: Option<&[String]>,
89    ) -> JsObject {
90        self.build_option_schema(
91            Self::convert_manifest_arg_type(flag_type),
92            short,
93            default,
94            description,
95            choices,
96        )
97    }
98
99    /// Build an option object schema for boune's declarative API.
100    ///
101    /// Generates: `{ type: "string", short: "x", default: ..., description: "...", choices: [...] as const }`
102    pub fn build_option_schema(
103        &self,
104        flag_type: ArgType,
105        short: Option<char>,
106        default: Option<&toml::Value>,
107        description: Option<&str>,
108        choices: Option<&[String]>,
109    ) -> JsObject {
110        let boune_type = self.map_arg_type(flag_type);
111
112        JsObject::new()
113            .string("type", boune_type)
114            .string_opt("short", short.map(|c| c.to_string()))
115            .toml_opt("default", default)
116            .string_opt("description", description)
117            .array_opt(
118                "choices",
119                choices.map(|c| JsArray::from_strings(c).as_const()),
120            )
121    }
122
123    /// Map manifest argument type to TypeScript boune type.
124    pub fn map_manifest_arg_type(&self, arg_type: &ManifestArgType) -> &'static str {
125        self.map_arg_type(Self::convert_manifest_arg_type(arg_type))
126    }
127
128    /// Build action handler arrow function.
129    pub fn build_action_handler(&self, has_args: bool, has_options: bool) -> ArrowFn {
130        // Build destructuring pattern based on what's available
131        let params = match (has_args, has_options) {
132            (true, true) => "{ args, options }",
133            (true, false) => "{ args }",
134            (false, true) => "{ options }",
135            (false, false) => "{}",
136        };
137
138        // Build run() call based on what's available
139        let run_call = match (has_args, has_options) {
140            (true, true) => "await run(args, options);",
141            (true, false) => "await run(args);",
142            (false, true) => "await run(options);",
143            (false, false) => "await run();",
144        };
145
146        ArrowFn::new(params).async_().body_line(run_call)
147    }
148
149    // ========================================================================
150    // IR-based methods
151    // ========================================================================
152
153    /// Build an argument object schema from IR Input.
154    pub fn build_argument_schema_ir(&self, input: &Input) -> JsObject {
155        let boune_type = self.map_arg_type(input_type_to_arg_type(input.ty));
156
157        JsObject::new()
158            .string("type", boune_type)
159            .raw_if(
160                input.required && input.default.is_none(),
161                "required",
162                "true",
163            )
164            .default_value_opt("default", input.default.as_ref())
165            .string_opt("description", input.description.as_deref())
166            .array_opt(
167                "choices",
168                input
169                    .choices
170                    .as_ref()
171                    .map(|c| JsArray::from_strings(c).as_const()),
172            )
173    }
174
175    /// Build an option object schema from IR Input.
176    pub fn build_option_schema_ir(&self, input: &Input) -> JsObject {
177        let boune_type = self.map_arg_type(input_type_to_arg_type(input.ty));
178        let short = if let InputKind::Flag { short } = &input.kind {
179            *short
180        } else {
181            None
182        };
183
184        JsObject::new()
185            .string("type", boune_type)
186            .string_opt("short", short.map(|c| c.to_string()))
187            .default_value_opt("default", input.default.as_ref())
188            .string_opt("description", input.description.as_deref())
189            .array_opt(
190                "choices",
191                input
192                    .choices
193                    .as_ref()
194                    .map(|c| JsArray::from_strings(c).as_const()),
195            )
196    }
197}
198
199impl CliAdapter for BouneAdapter {
200    fn name(&self) -> &'static str {
201        "boune"
202    }
203
204    fn dependencies(&self) -> Vec<Dependency> {
205        vec![Dependency::new("boune", BOUNE_VERSION)]
206    }
207
208    fn generate_cli(&self, info: &CliInfo) -> Vec<CodeFragment> {
209        // Build CLI schema object
210        let mut schema = JsObject::new()
211            .string("name", &info.name)
212            .string("version", info.version.to_string());
213
214        if let Some(desc) = &info.description {
215            schema = schema.string("description", desc);
216        }
217
218        // Build commands object
219        let mut commands_obj = JsObject::new();
220        for cmd in &info.commands {
221            commands_obj =
222                commands_obj.raw(&cmd.pascal_name, format!("{}Command", cmd.pascal_name));
223        }
224        schema = schema.object("commands", commands_obj);
225
226        // Generate the defineCli call
227        let code = format!("const app = defineCli({});", schema.build());
228        vec![CodeFragment::raw(code)]
229    }
230
231    fn generate_command(&self, info: &CommandMeta) -> Vec<CodeFragment> {
232        // This generates a leaf command definition
233        let action = self.build_action_handler(!info.args.is_empty(), !info.flags.is_empty());
234
235        let schema = JsObject::new()
236            .string("name", &info.name)
237            .string("description", &info.description)
238            .raw_if(!info.args.is_empty(), "arguments", "args")
239            .raw_if(!info.flags.is_empty(), "options", "options")
240            .arrow_fn("action", action);
241
242        let code = format!(
243            "export const {}Command = defineCommand({});",
244            info.pascal_name,
245            schema.build()
246        );
247
248        vec![CodeFragment::raw(code)]
249    }
250
251    fn generate_subcommands(&self, info: &CommandMeta) -> Vec<CodeFragment> {
252        // Build subcommands object
253        let mut subcommands = JsObject::new();
254        for sub in &info.subcommands {
255            subcommands = subcommands.raw(&sub.pascal_name, format!("{}Command", sub.pascal_name));
256        }
257
258        let schema = JsObject::new()
259            .string("name", &info.name)
260            .string("description", &info.description)
261            .object("subcommands", subcommands);
262
263        let code = format!(
264            "export const {}Command = defineCommand({});",
265            info.pascal_name,
266            schema.build()
267        );
268
269        vec![CodeFragment::raw(code)]
270    }
271
272    fn generate_dispatch(&self, _info: &DispatchInfo) -> Vec<CodeFragment> {
273        // Boune handles dispatch internally via subcommands object
274        // No explicit dispatch code needed
275        Vec::new()
276    }
277
278    fn imports(&self) -> Vec<ImportSpec> {
279        vec![ImportSpec::new("boune").symbol("defineCli")]
280    }
281
282    fn command_imports(&self, info: &CommandMeta) -> Vec<ImportSpec> {
283        let mut imports = vec![ImportSpec::new("boune").symbol("defineCommand")];
284
285        // No longer need to import `argument` or `option` builders
286        // Type inference helpers are still needed
287        if !info.args.is_empty() {
288            imports.push(ImportSpec::new("boune").symbol("InferArgs").type_only());
289        }
290
291        if !info.flags.is_empty() {
292            imports.push(ImportSpec::new("boune").symbol("InferOpts").type_only());
293        }
294
295        imports
296    }
297
298    fn map_arg_type(&self, arg_type: ArgType) -> &'static str {
299        match arg_type {
300            ArgType::String => "string",
301            ArgType::Int => "number",
302            ArgType::Float => "number",
303            ArgType::Bool => "boolean",
304            ArgType::Path => "string",
305        }
306    }
307
308    fn map_optional_type(&self, arg_type: ArgType) -> String {
309        format!("{} | undefined", self.map_arg_type(arg_type))
310    }
311}