baobao_ir/
app.rs

1//! Application Intermediate Representation.
2//!
3//! This module defines the unified IR for CLI applications (and future HTTP servers).
4//! The IR serves as a clean abstraction layer between the manifest parsing and
5//! language-specific code generation.
6//!
7//! # Architecture
8//!
9//! ```text
10//! bao.toml → Manifest (parsing) → AppIR (lowering) → Generator (codegen)
11//! ```
12
13use serde::Serialize;
14
15use crate::{ContextFieldInfo, ContextFieldType, DatabaseType, PoolConfig, SqliteOptions};
16
17/// Application IR - unified representation for code generation.
18#[derive(Debug, Clone, Serialize)]
19pub struct AppIR {
20    /// Application metadata.
21    pub meta: AppMeta,
22    /// Shared resources (database, HTTP client, etc.).
23    pub resources: Vec<Resource>,
24    /// Operations (commands for CLI, routes for HTTP).
25    pub operations: Vec<Operation>,
26}
27
28impl AppIR {
29    /// Returns true if any resource requires async initialization.
30    pub fn has_async(&self) -> bool {
31        self.resources
32            .iter()
33            .any(|r| matches!(r, Resource::Database(_)))
34    }
35
36    /// Returns true if a database resource is configured.
37    pub fn has_database(&self) -> bool {
38        self.resources
39            .iter()
40            .any(|r| matches!(r, Resource::Database(_)))
41    }
42
43    /// Returns true if an HTTP client resource is configured.
44    pub fn has_http(&self) -> bool {
45        self.resources
46            .iter()
47            .any(|r| matches!(r, Resource::HttpClient(_)))
48    }
49
50    /// Iterate over all commands.
51    pub fn commands(&self) -> impl Iterator<Item = &CommandOp> {
52        self.operations.iter().map(|op| {
53            let Operation::Command(cmd) = op;
54            cmd
55        })
56    }
57
58    /// Collect all handler paths from commands (for orphan detection).
59    pub fn handler_paths(&self) -> Vec<String> {
60        fn collect(cmd: &CommandOp, paths: &mut Vec<String>) {
61            paths.push(cmd.handler_path());
62            for child in &cmd.children {
63                collect(child, paths);
64            }
65        }
66
67        let mut paths = Vec::new();
68        for cmd in self.commands() {
69            collect(cmd, &mut paths);
70        }
71        paths
72    }
73
74    /// Count total number of leaf commands (commands without subcommands).
75    pub fn command_count(&self) -> usize {
76        fn count(cmd: &CommandOp) -> usize {
77            if cmd.children.is_empty() {
78                1
79            } else {
80                cmd.children.iter().map(count).sum()
81            }
82        }
83
84        self.commands().map(count).sum()
85    }
86
87    /// Collect context fields from resources.
88    pub fn context_fields(&self) -> Vec<ContextFieldInfo> {
89        self.resources
90            .iter()
91            .map(|resource| match resource {
92                Resource::Database(db) => ContextFieldInfo {
93                    name: db.name.clone(),
94                    field_type: ContextFieldType::Database(db.db_type),
95                    env_var: db.env_var.clone(),
96                    is_async: true, // Database operations are always async
97                    pool: db.pool.clone(),
98                    sqlite: db.sqlite.clone(),
99                },
100                Resource::HttpClient(http) => ContextFieldInfo {
101                    name: http.name.clone(),
102                    field_type: ContextFieldType::Http,
103                    env_var: String::new(), // HTTP client doesn't need env var
104                    is_async: false,        // HTTP client creation is sync
105                    pool: PoolConfig::default(),
106                    sqlite: None,
107                },
108            })
109            .collect()
110    }
111}
112
113/// Application metadata.
114#[derive(Debug, Clone, Serialize)]
115pub struct AppMeta {
116    /// Application name.
117    pub name: String,
118    /// Version string.
119    pub version: String,
120    /// Description for help text.
121    pub description: Option<String>,
122    /// Author information.
123    pub author: Option<String>,
124}
125
126/// A shared resource in the application context.
127#[derive(Debug, Clone, Serialize)]
128pub enum Resource {
129    /// Database connection pool.
130    Database(DatabaseResource),
131    /// HTTP client.
132    HttpClient(HttpClientResource),
133}
134
135/// Database resource configuration.
136#[derive(Debug, Clone, Serialize)]
137pub struct DatabaseResource {
138    /// Field name in the context struct.
139    pub name: String,
140    /// Database type (Postgres, MySQL, SQLite).
141    pub db_type: DatabaseType,
142    /// Environment variable for the connection string.
143    pub env_var: String,
144    /// Pool configuration.
145    pub pool: PoolConfig,
146    /// SQLite-specific options.
147    pub sqlite: Option<SqliteOptions>,
148}
149
150/// HTTP client resource configuration.
151#[derive(Debug, Clone, Serialize)]
152pub struct HttpClientResource {
153    /// Field name in the context struct.
154    pub name: String,
155}
156
157/// An operation in the application.
158#[derive(Debug, Clone, Serialize)]
159pub enum Operation {
160    /// CLI command.
161    Command(CommandOp),
162    // Future: Route(RouteOp),
163}
164
165/// A CLI command operation.
166#[derive(Debug, Clone, Serialize)]
167pub struct CommandOp {
168    /// Command name.
169    pub name: String,
170    /// Full path from root (e.g., ["users", "create"]).
171    pub path: Vec<String>,
172    /// Command description.
173    pub description: String,
174    /// Input parameters (args and flags).
175    pub inputs: Vec<Input>,
176    /// Child commands (subcommands).
177    pub children: Vec<CommandOp>,
178}
179
180impl CommandOp {
181    /// Returns true if this command has subcommands.
182    pub fn has_subcommands(&self) -> bool {
183        !self.children.is_empty()
184    }
185
186    /// Returns the handler path (e.g., "users/create" for nested commands).
187    pub fn handler_path(&self) -> String {
188        self.path.join("/")
189    }
190}
191
192/// An input parameter for a command.
193#[derive(Debug, Clone, Serialize)]
194pub struct Input {
195    /// Parameter name.
196    pub name: String,
197    /// Parameter type.
198    pub ty: InputType,
199    /// Parameter kind (positional or flag).
200    pub kind: InputKind,
201    /// Whether the parameter is required.
202    pub required: bool,
203    /// Default value.
204    pub default: Option<DefaultValue>,
205    /// Description for help text.
206    pub description: Option<String>,
207    /// Allowed choices (creates enum in generated code).
208    pub choices: Option<Vec<String>>,
209}
210
211/// Input parameter type.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
213pub enum InputType {
214    String,
215    Int,
216    Float,
217    Bool,
218    Path,
219}
220
221/// Input parameter kind.
222#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
223pub enum InputKind {
224    /// Positional argument.
225    Positional,
226    /// Named flag with optional short form.
227    Flag {
228        /// Short flag character (e.g., 'v' for -v).
229        short: Option<char>,
230    },
231}
232
233/// A default value for an input.
234#[derive(Debug, Clone, PartialEq, Serialize)]
235pub enum DefaultValue {
236    String(String),
237    Int(i64),
238    Float(f64),
239    Bool(bool),
240}
241
242impl DefaultValue {
243    /// Convert to a string representation suitable for code generation.
244    pub fn to_code_string(&self) -> String {
245        match self {
246            Self::String(s) => s.clone(),
247            Self::Int(i) => i.to_string(),
248            Self::Float(f) => f.to_string(),
249            Self::Bool(b) => b.to_string(),
250        }
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_command_has_subcommands() {
260        let cmd = CommandOp {
261            name: "test".into(),
262            path: vec!["test".into()],
263            description: "A test command".into(),
264            inputs: vec![],
265            children: vec![],
266        };
267        assert!(!cmd.has_subcommands());
268
269        let parent = CommandOp {
270            name: "parent".into(),
271            path: vec!["parent".into()],
272            description: "A parent command".into(),
273            inputs: vec![],
274            children: vec![cmd],
275        };
276        assert!(parent.has_subcommands());
277    }
278
279    #[test]
280    fn test_command_handler_path() {
281        let cmd = CommandOp {
282            name: "create".into(),
283            path: vec!["users".into(), "create".into()],
284            description: "Create a user".into(),
285            inputs: vec![],
286            children: vec![],
287        };
288        assert_eq!(cmd.handler_path(), "users/create");
289    }
290
291    #[test]
292    fn test_default_value_to_code_string() {
293        assert_eq!(
294            DefaultValue::String("hello".into()).to_code_string(),
295            "hello"
296        );
297        assert_eq!(DefaultValue::Int(42).to_code_string(), "42");
298        assert_eq!(DefaultValue::Bool(true).to_code_string(), "true");
299    }
300}