1use serde::Serialize;
14
15use crate::{ContextFieldInfo, ContextFieldType, DatabaseType, PoolConfig, SqliteOptions};
16
17#[derive(Debug, Clone, Serialize)]
19pub struct AppIR {
20 pub meta: AppMeta,
22 pub resources: Vec<Resource>,
24 pub operations: Vec<Operation>,
26}
27
28impl AppIR {
29 pub fn has_async(&self) -> bool {
31 self.resources
32 .iter()
33 .any(|r| matches!(r, Resource::Database(_)))
34 }
35
36 pub fn has_database(&self) -> bool {
38 self.resources
39 .iter()
40 .any(|r| matches!(r, Resource::Database(_)))
41 }
42
43 pub fn has_http(&self) -> bool {
45 self.resources
46 .iter()
47 .any(|r| matches!(r, Resource::HttpClient(_)))
48 }
49
50 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 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 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 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, 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(), is_async: false, pool: PoolConfig::default(),
106 sqlite: None,
107 },
108 })
109 .collect()
110 }
111}
112
113#[derive(Debug, Clone, Serialize)]
115pub struct AppMeta {
116 pub name: String,
118 pub version: String,
120 pub description: Option<String>,
122 pub author: Option<String>,
124}
125
126#[derive(Debug, Clone, Serialize)]
128pub enum Resource {
129 Database(DatabaseResource),
131 HttpClient(HttpClientResource),
133}
134
135#[derive(Debug, Clone, Serialize)]
137pub struct DatabaseResource {
138 pub name: String,
140 pub db_type: DatabaseType,
142 pub env_var: String,
144 pub pool: PoolConfig,
146 pub sqlite: Option<SqliteOptions>,
148}
149
150#[derive(Debug, Clone, Serialize)]
152pub struct HttpClientResource {
153 pub name: String,
155}
156
157#[derive(Debug, Clone, Serialize)]
159pub enum Operation {
160 Command(CommandOp),
162 }
164
165#[derive(Debug, Clone, Serialize)]
167pub struct CommandOp {
168 pub name: String,
170 pub path: Vec<String>,
172 pub description: String,
174 pub inputs: Vec<Input>,
176 pub children: Vec<CommandOp>,
178}
179
180impl CommandOp {
181 pub fn has_subcommands(&self) -> bool {
183 !self.children.is_empty()
184 }
185
186 pub fn handler_path(&self) -> String {
188 self.path.join("/")
189 }
190}
191
192#[derive(Debug, Clone, Serialize)]
194pub struct Input {
195 pub name: String,
197 pub ty: InputType,
199 pub kind: InputKind,
201 pub required: bool,
203 pub default: Option<DefaultValue>,
205 pub description: Option<String>,
207 pub choices: Option<Vec<String>>,
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
213pub enum InputType {
214 String,
215 Int,
216 Float,
217 Bool,
218 Path,
219}
220
221#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
223pub enum InputKind {
224 Positional,
226 Flag {
228 short: Option<char>,
230 },
231}
232
233#[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 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}