baobao_codegen_typescript/adapters/
boune.rs1use baobao_codegen::{
4 adapters::{CliAdapter, CliInfo, CommandMeta, Dependency, DispatchInfo, ImportSpec},
5 builder::CodeFragment,
6};
7use baobao_core::ArgType;
8use baobao_manifest::ArgType as ManifestArgType;
9
10use crate::ast::{ArrowFn, JsObject, MethodChain};
11
12#[derive(Debug, Clone, Default)]
14pub struct BouneAdapter;
15
16impl BouneAdapter {
17 pub fn new() -> Self {
18 Self
19 }
20
21 fn convert_arg_type(arg_type: &ManifestArgType) -> ArgType {
23 match arg_type {
24 ManifestArgType::String => ArgType::String,
25 ManifestArgType::Int => ArgType::Int,
26 ManifestArgType::Float => ArgType::Float,
27 ManifestArgType::Bool => ArgType::Bool,
28 ManifestArgType::Path => ArgType::Path,
29 }
30 }
31
32 pub fn build_argument_chain_manifest(
34 &self,
35 arg_type: &ManifestArgType,
36 required: bool,
37 has_default: bool,
38 default: Option<&toml::Value>,
39 description: Option<&str>,
40 ) -> String {
41 self.build_argument_chain(
42 Self::convert_arg_type(arg_type),
43 required,
44 has_default,
45 default,
46 description,
47 )
48 }
49
50 pub fn build_argument_chain(
52 &self,
53 arg_type: ArgType,
54 required: bool,
55 has_default: bool,
56 default: Option<&toml::Value>,
57 description: Option<&str>,
58 ) -> String {
59 let boune_type = self.map_arg_type(arg_type);
60 let mut chain = MethodChain::new(format!("argument.{}", boune_type));
61
62 if required && !has_default {
63 chain = chain.call_empty("required");
64 }
65
66 if let Some(default) = default {
67 chain = chain.call("default", toml_to_ts_literal(default));
68 }
69
70 if let Some(desc) = description {
71 chain = chain.call("describe", format!("\"{}\"", desc));
72 }
73
74 chain.build_inline()
75 }
76
77 pub fn build_option_chain_manifest(
79 &self,
80 flag_type: &ManifestArgType,
81 short: Option<char>,
82 default: Option<&toml::Value>,
83 description: Option<&str>,
84 ) -> String {
85 self.build_option_chain(
86 Self::convert_arg_type(flag_type),
87 short,
88 default,
89 description,
90 )
91 }
92
93 pub fn build_option_chain(
95 &self,
96 flag_type: ArgType,
97 short: Option<char>,
98 default: Option<&toml::Value>,
99 description: Option<&str>,
100 ) -> String {
101 let boune_type = self.map_arg_type(flag_type);
102 let mut chain = MethodChain::new(format!("option.{}", boune_type));
103
104 if let Some(short) = short {
105 chain = chain.call("short", format!("\"{}\"", short));
106 }
107
108 if let Some(default) = default {
109 chain = chain.call("default", toml_to_ts_literal(default));
110 }
111
112 if let Some(desc) = description {
113 chain = chain.call("describe", format!("\"{}\"", desc));
114 }
115
116 chain.build_inline()
117 }
118
119 pub fn map_manifest_arg_type(&self, arg_type: &ManifestArgType) -> &'static str {
121 self.map_arg_type(Self::convert_arg_type(arg_type))
122 }
123
124 pub fn build_action_handler(&self, has_args: bool, has_options: bool) -> ArrowFn {
126 let params = match (has_args, has_options) {
128 (true, true) => "{ args, options }",
129 (true, false) => "{ args }",
130 (false, true) => "{ options }",
131 (false, false) => "{}",
132 };
133
134 let run_call = match (has_args, has_options) {
136 (true, true) => "await run(args, options);",
137 (true, false) => "await run(args);",
138 (false, true) => "await run(options);",
139 (false, false) => "await run();",
140 };
141
142 ArrowFn::new(params).async_().body_line(run_call)
143 }
144}
145
146impl CliAdapter for BouneAdapter {
147 fn name(&self) -> &'static str {
148 "boune"
149 }
150
151 fn dependencies(&self) -> Vec<Dependency> {
152 vec![Dependency::new("boune", "^0.5.0")]
153 }
154
155 fn generate_cli(&self, info: &CliInfo) -> Vec<CodeFragment> {
156 let mut schema = JsObject::new()
158 .string("name", &info.name)
159 .string("version", info.version.to_string());
160
161 if let Some(desc) = &info.description {
162 schema = schema.string("description", desc);
163 }
164
165 let mut commands_obj = JsObject::new();
167 for cmd in &info.commands {
168 commands_obj =
169 commands_obj.raw(&cmd.pascal_name, format!("{}Command", cmd.pascal_name));
170 }
171 schema = schema.object("commands", commands_obj);
172
173 let code = format!("const app = defineCli({});", schema.build());
175 vec![CodeFragment::raw(code)]
176 }
177
178 fn generate_command(&self, info: &CommandMeta) -> Vec<CodeFragment> {
179 let action = self.build_action_handler(!info.args.is_empty(), !info.flags.is_empty());
181
182 let schema = JsObject::new()
183 .string("name", &info.name)
184 .string("description", &info.description)
185 .raw_if(!info.args.is_empty(), "arguments", "args")
186 .raw_if(!info.flags.is_empty(), "options", "options")
187 .arrow_fn("action", action);
188
189 let code = format!(
190 "export const {}Command = defineCommand({});",
191 info.pascal_name,
192 schema.build()
193 );
194
195 vec![CodeFragment::raw(code)]
196 }
197
198 fn generate_subcommands(&self, info: &CommandMeta) -> Vec<CodeFragment> {
199 let mut subcommands = JsObject::new();
201 for sub in &info.subcommands {
202 subcommands = subcommands.raw(&sub.pascal_name, format!("{}Command", sub.pascal_name));
203 }
204
205 let schema = JsObject::new()
206 .string("name", &info.name)
207 .string("description", &info.description)
208 .object("subcommands", subcommands);
209
210 let code = format!(
211 "export const {}Command = defineCommand({});",
212 info.pascal_name,
213 schema.build()
214 );
215
216 vec![CodeFragment::raw(code)]
217 }
218
219 fn generate_dispatch(&self, _info: &DispatchInfo) -> Vec<CodeFragment> {
220 Vec::new()
223 }
224
225 fn imports(&self) -> Vec<ImportSpec> {
226 vec![ImportSpec::new("boune").symbol("defineCli")]
227 }
228
229 fn command_imports(&self, info: &CommandMeta) -> Vec<ImportSpec> {
230 let mut imports = vec![ImportSpec::new("boune").symbol("defineCommand")];
231
232 if !info.args.is_empty() {
233 imports[0].symbols.push("argument".to_string());
234 imports.push(ImportSpec::new("boune").symbol("InferArgs").type_only());
235 }
236
237 if !info.flags.is_empty() {
238 imports[0].symbols.push("option".to_string());
239 imports.push(ImportSpec::new("boune").symbol("InferOptions").type_only());
240 }
241
242 imports
243 }
244
245 fn map_arg_type(&self, arg_type: ArgType) -> &'static str {
246 match arg_type {
247 ArgType::String => "string",
248 ArgType::Int => "number",
249 ArgType::Float => "number",
250 ArgType::Bool => "boolean",
251 ArgType::Path => "string",
252 }
253 }
254
255 fn map_optional_type(&self, arg_type: ArgType) -> String {
256 format!("{} | undefined", self.map_arg_type(arg_type))
257 }
258}
259
260fn toml_to_ts_literal(value: &toml::Value) -> String {
263 match value {
264 toml::Value::String(s) => format!("\"{}\"", s),
265 toml::Value::Integer(i) => i.to_string(),
266 toml::Value::Float(f) => f.to_string(),
267 toml::Value::Boolean(b) => b.to_string(),
268 _ => String::new(),
269 }
270}