baobao_codegen/pipeline/phases/
lower.rs1use std::{collections::HashMap, time::Duration};
7
8use baobao_ir::{
9 AppIR, AppMeta, CommandOp, DatabaseResource, DatabaseType, DefaultValue, HttpClientResource,
10 Input, InputKind, InputType, Operation, PoolConfig, Resource, SqliteOptions,
11};
12use baobao_manifest::{ArgType, Command, ContextField, Flag, Manifest};
13use eyre::Result;
14
15use crate::pipeline::{CompilationContext, Phase};
16
17pub struct LowerPhase;
21
22impl Phase for LowerPhase {
23 fn name(&self) -> &'static str {
24 "lower"
25 }
26
27 fn description(&self) -> &'static str {
28 "Transform Manifest to Application IR"
29 }
30
31 fn run(&self, ctx: &mut CompilationContext) -> Result<()> {
32 ctx.ir = Some(lower_manifest(&ctx.manifest));
33 Ok(())
34 }
35}
36
37fn lower_manifest(manifest: &Manifest) -> AppIR {
39 AppIR {
40 meta: lower_meta(manifest),
41 resources: lower_resources(manifest),
42 operations: lower_commands(&manifest.commands),
43 }
44}
45
46fn lower_meta(manifest: &Manifest) -> AppMeta {
48 AppMeta {
49 name: manifest.cli.name.clone(),
50 version: manifest.cli.version.to_string(),
51 description: manifest.cli.description.clone(),
52 author: manifest.cli.author.clone(),
53 }
54}
55
56fn lower_resources(manifest: &Manifest) -> Vec<Resource> {
58 let mut resources = Vec::new();
59
60 if let Some(db) = &manifest.context.database
61 && let Some(resource) = lower_database_resource("db", db)
62 {
63 resources.push(Resource::Database(resource));
64 }
65
66 if manifest.context.http.is_some() {
67 resources.push(Resource::HttpClient(HttpClientResource {
68 name: "http".into(),
69 }));
70 }
71
72 resources
73}
74
75fn lower_database_resource(name: &str, field: &ContextField) -> Option<DatabaseResource> {
77 let (db_type, env_var, pool_config, sqlite_opts) = match field {
78 ContextField::Postgres(config) => (
79 DatabaseType::Postgres,
80 default_env_var(config.env(), "DATABASE_URL"),
81 lower_pool_config(config.pool()),
82 None,
83 ),
84 ContextField::Mysql(config) => (
85 DatabaseType::Mysql,
86 default_env_var(config.env(), "DATABASE_URL"),
87 lower_pool_config(config.pool()),
88 None,
89 ),
90 ContextField::Sqlite(config) => (
91 DatabaseType::Sqlite,
92 default_env_var(config.env.as_deref(), "DATABASE_URL"),
93 lower_pool_config(&config.pool),
94 Some(lower_sqlite_options(config)),
95 ),
96 ContextField::Http(_) => return None,
97 };
98
99 Some(DatabaseResource {
100 name: name.into(),
101 db_type,
102 env_var,
103 pool: pool_config,
104 sqlite: sqlite_opts,
105 })
106}
107
108fn default_env_var(env: Option<&str>, default: &str) -> String {
110 env.unwrap_or(default).into()
111}
112
113fn lower_pool_config(config: &baobao_manifest::PoolConfig) -> PoolConfig {
115 PoolConfig {
116 max_connections: config.max_connections,
117 min_connections: config.min_connections,
118 acquire_timeout: config.acquire_timeout.map(Duration::from_secs),
119 idle_timeout: config.idle_timeout.map(Duration::from_secs),
120 max_lifetime: config.max_lifetime.map(Duration::from_secs),
121 }
122}
123
124fn lower_sqlite_options(config: &baobao_manifest::SqliteConfig) -> SqliteOptions {
126 SqliteOptions {
127 path: config.path.clone(),
128 create_if_missing: config.create_if_missing,
129 read_only: config.read_only,
130 journal_mode: config.journal_mode.as_ref().map(|m| match m {
131 baobao_manifest::JournalMode::Wal => baobao_ir::JournalMode::Wal,
132 baobao_manifest::JournalMode::Delete => baobao_ir::JournalMode::Delete,
133 baobao_manifest::JournalMode::Truncate => baobao_ir::JournalMode::Truncate,
134 baobao_manifest::JournalMode::Persist => baobao_ir::JournalMode::Persist,
135 baobao_manifest::JournalMode::Memory => baobao_ir::JournalMode::Memory,
136 baobao_manifest::JournalMode::Off => baobao_ir::JournalMode::Off,
137 }),
138 synchronous: config.synchronous.as_ref().map(|s| match s {
139 baobao_manifest::SynchronousMode::Off => baobao_ir::SynchronousMode::Off,
140 baobao_manifest::SynchronousMode::Normal => baobao_ir::SynchronousMode::Normal,
141 baobao_manifest::SynchronousMode::Full => baobao_ir::SynchronousMode::Full,
142 }),
143 busy_timeout: config.busy_timeout.map(Duration::from_millis),
144 foreign_keys: config.foreign_keys,
145 }
146}
147
148fn lower_commands(commands: &HashMap<String, Command>) -> Vec<Operation> {
150 let mut names: Vec<_> = commands.keys().collect();
152 names.sort();
153
154 names
155 .into_iter()
156 .map(|name| {
157 let cmd = &commands[name];
158 Operation::Command(lower_command(name, cmd, vec![name.clone()]))
159 })
160 .collect()
161}
162
163fn lower_command(name: &str, cmd: &Command, path: Vec<String>) -> CommandOp {
165 let mut inputs = Vec::new();
166
167 let mut arg_names: Vec<_> = cmd.args.keys().collect();
169 arg_names.sort();
170 for arg_name in arg_names {
171 let arg = &cmd.args[arg_name];
172 inputs.push(Input {
173 name: arg_name.clone(),
174 ty: lower_arg_type(&arg.arg_type),
175 kind: InputKind::Positional,
176 required: arg.required,
177 default: arg.default.as_ref().and_then(lower_default_value),
178 description: arg.description.clone(),
179 choices: arg.choices.clone(),
180 });
181 }
182
183 let mut flag_names: Vec<_> = cmd.flags.keys().collect();
185 flag_names.sort();
186 for flag_name in flag_names {
187 let flag = &cmd.flags[flag_name];
188 inputs.push(lower_flag(flag_name, flag));
189 }
190
191 let mut child_names: Vec<_> = cmd.commands.keys().collect();
193 child_names.sort();
194 let children: Vec<_> = child_names
195 .into_iter()
196 .map(|child_name| {
197 let child_cmd = &cmd.commands[child_name];
198 let mut child_path = path.clone();
199 child_path.push(child_name.clone());
200 lower_command(child_name, child_cmd, child_path)
201 })
202 .collect();
203
204 CommandOp {
205 name: name.into(),
206 path,
207 description: cmd.description.clone(),
208 inputs,
209 children,
210 }
211}
212
213fn lower_flag(name: &str, flag: &Flag) -> Input {
215 Input {
216 name: name.into(),
217 ty: lower_arg_type(&flag.flag_type),
218 kind: InputKind::Flag {
219 short: flag.short.as_ref().map(|s| *s.get_ref()),
220 },
221 required: false,
222 default: flag.default.as_ref().and_then(lower_default_value),
223 description: flag.description.clone(),
224 choices: flag.choices.clone(),
225 }
226}
227
228fn lower_arg_type(ty: &ArgType) -> InputType {
230 match ty {
231 ArgType::String => InputType::String,
232 ArgType::Int => InputType::Int,
233 ArgType::Float => InputType::Float,
234 ArgType::Bool => InputType::Bool,
235 ArgType::Path => InputType::Path,
236 }
237}
238
239fn lower_default_value(value: &toml::Value) -> Option<DefaultValue> {
241 match value {
242 toml::Value::String(s) => Some(DefaultValue::String(s.clone())),
243 toml::Value::Integer(i) => Some(DefaultValue::Int(*i)),
244 toml::Value::Float(f) => Some(DefaultValue::Float(*f)),
245 toml::Value::Boolean(b) => Some(DefaultValue::Bool(*b)),
246 _ => None, }
248}
249
250#[cfg(test)]
251mod tests {
252 use baobao_manifest::Manifest;
253
254 use super::*;
255
256 fn parse_manifest(content: &str) -> Manifest {
257 toml::from_str(content).expect("Failed to parse test manifest")
258 }
259
260 fn make_test_manifest() -> Manifest {
261 parse_manifest(
262 r#"
263 [cli]
264 name = "test"
265 version = "1.0.0"
266 description = "Test CLI"
267 language = "rust"
268 "#,
269 )
270 }
271
272 #[test]
273 fn test_lower_phase() {
274 let manifest = make_test_manifest();
275 let mut ctx = CompilationContext::new(manifest);
276
277 assert!(ctx.ir.is_none());
278
279 LowerPhase.run(&mut ctx).expect("lower should succeed");
280
281 assert!(ctx.ir.is_some());
282
283 let ir = ctx.ir.as_ref().unwrap();
284 assert_eq!(ir.meta.name, "test");
285 assert_eq!(ir.meta.version, "1.0.0");
286 }
287
288 #[test]
289 fn test_lower_arg_type() {
290 assert_eq!(lower_arg_type(&ArgType::String), InputType::String);
291 assert_eq!(lower_arg_type(&ArgType::Int), InputType::Int);
292 assert_eq!(lower_arg_type(&ArgType::Float), InputType::Float);
293 assert_eq!(lower_arg_type(&ArgType::Bool), InputType::Bool);
294 assert_eq!(lower_arg_type(&ArgType::Path), InputType::Path);
295 }
296
297 #[test]
298 fn test_lower_default_value() {
299 assert_eq!(
300 lower_default_value(&toml::Value::String("hello".into())),
301 Some(DefaultValue::String("hello".into()))
302 );
303 assert_eq!(
304 lower_default_value(&toml::Value::Integer(42)),
305 Some(DefaultValue::Int(42))
306 );
307 assert_eq!(
308 lower_default_value(&toml::Value::Boolean(true)),
309 Some(DefaultValue::Bool(true))
310 );
311 }
312
313 #[test]
314 fn test_lower_pool_config() {
315 let manifest_config = baobao_manifest::PoolConfig {
316 max_connections: Some(10),
317 min_connections: Some(2),
318 acquire_timeout: Some(30),
319 idle_timeout: Some(600),
320 max_lifetime: Some(1800),
321 };
322
323 let ir_config = lower_pool_config(&manifest_config);
324
325 assert_eq!(ir_config.max_connections, Some(10));
326 assert_eq!(ir_config.min_connections, Some(2));
327 assert_eq!(ir_config.acquire_timeout, Some(Duration::from_secs(30)));
328 assert_eq!(ir_config.idle_timeout, Some(Duration::from_secs(600)));
329 assert_eq!(ir_config.max_lifetime, Some(Duration::from_secs(1800)));
330 }
331}