1use clap::{Parser, Subcommand};
9
10#[derive(Parser, Debug)]
16#[command(author, about, long_about = None)]
17pub struct CliArgs {
18 #[arg(short, long, env = "FABRYK_CONFIG")]
20 pub config: Option<String>,
21
22 #[arg(short, long)]
24 pub verbose: bool,
25
26 #[arg(short, long)]
28 pub quiet: bool,
29
30 #[command(subcommand)]
32 pub command: Option<BaseCommand>,
33}
34
35#[derive(Subcommand, Debug)]
37pub enum BaseCommand {
38 Serve {
40 #[arg(short, long, default_value = "3000")]
42 port: u16,
43 },
44
45 Index {
47 #[arg(short, long)]
49 force: bool,
50
51 #[arg(long)]
53 check: bool,
54 },
55
56 Version,
58
59 Health,
61
62 Graph(GraphCommand),
64
65 Config(ConfigCommand),
67}
68
69#[derive(Parser, Debug)]
71pub struct ConfigCommand {
72 #[command(subcommand)]
74 pub command: ConfigAction,
75}
76
77#[derive(Subcommand, Debug)]
79pub enum ConfigAction {
80 Path,
82
83 Get {
85 key: String,
87 },
88
89 Set {
91 key: String,
93
94 value: String,
96 },
97
98 Init {
100 #[arg(short, long)]
102 file: Option<String>,
103
104 #[arg(long)]
106 force: bool,
107 },
108
109 Export {
111 #[arg(long)]
113 docker_env: bool,
114 },
115}
116
117#[derive(Parser, Debug)]
119pub struct GraphCommand {
120 #[command(subcommand)]
122 pub command: GraphSubcommand,
123}
124
125#[derive(Subcommand, Debug)]
127pub enum GraphSubcommand {
128 Build {
130 #[arg(short, long)]
132 output: Option<String>,
133
134 #[arg(long)]
136 dry_run: bool,
137 },
138
139 Validate,
141
142 Stats,
144
145 Query {
147 #[arg(short, long)]
149 id: String,
150
151 #[arg(short = 't', long, default_value = "related")]
153 query_type: String,
154
155 #[arg(long)]
157 to: Option<String>,
158 },
159}
160
161pub trait CliExtension: Send + Sync {
170 type Command: Send + Sync;
172
173 fn handle_command(
175 &self,
176 command: Self::Command,
177 ) -> impl std::future::Future<Output = fabryk_core::Result<()>> + Send;
178}
179
180#[cfg(test)]
185mod tests {
186 use super::*;
187 use clap::Parser;
188
189 #[test]
190 fn test_cli_args_default() {
191 let args = CliArgs::parse_from(["test"]);
192 assert!(args.config.is_none());
193 assert!(!args.verbose);
194 assert!(!args.quiet);
195 assert!(args.command.is_none());
196 }
197
198 #[test]
199 fn test_cli_args_verbose() {
200 let args = CliArgs::parse_from(["test", "--verbose"]);
201 assert!(args.verbose);
202 assert!(!args.quiet);
203 }
204
205 #[test]
206 fn test_cli_args_quiet() {
207 let args = CliArgs::parse_from(["test", "--quiet"]);
208 assert!(!args.verbose);
209 assert!(args.quiet);
210 }
211
212 #[test]
213 fn test_cli_args_config() {
214 let args = CliArgs::parse_from(["test", "--config", "/path/to/config.toml"]);
215 assert_eq!(args.config, Some("/path/to/config.toml".to_string()));
216 }
217
218 #[test]
219 fn test_serve_command() {
220 let args = CliArgs::parse_from(["test", "serve"]);
221 match args.command {
222 Some(BaseCommand::Serve { port }) => assert_eq!(port, 3000),
223 _ => panic!("Expected Serve command"),
224 }
225 }
226
227 #[test]
228 fn test_serve_command_custom_port() {
229 let args = CliArgs::parse_from(["test", "serve", "--port", "8080"]);
230 match args.command {
231 Some(BaseCommand::Serve { port }) => assert_eq!(port, 8080),
232 _ => panic!("Expected Serve command"),
233 }
234 }
235
236 #[test]
237 fn test_index_command() {
238 let args = CliArgs::parse_from(["test", "index"]);
239 match args.command {
240 Some(BaseCommand::Index { force, check }) => {
241 assert!(!force);
242 assert!(!check);
243 }
244 _ => panic!("Expected Index command"),
245 }
246 }
247
248 #[test]
249 fn test_index_command_force() {
250 let args = CliArgs::parse_from(["test", "index", "--force"]);
251 match args.command {
252 Some(BaseCommand::Index { force, check }) => {
253 assert!(force);
254 assert!(!check);
255 }
256 _ => panic!("Expected Index command with force"),
257 }
258 }
259
260 #[test]
261 fn test_version_command() {
262 let args = CliArgs::parse_from(["test", "version"]);
263 assert!(matches!(args.command, Some(BaseCommand::Version)));
264 }
265
266 #[test]
267 fn test_health_command() {
268 let args = CliArgs::parse_from(["test", "health"]);
269 assert!(matches!(args.command, Some(BaseCommand::Health)));
270 }
271
272 #[test]
273 fn test_graph_build_command() {
274 let args = CliArgs::parse_from(["test", "graph", "build"]);
275 match args.command {
276 Some(BaseCommand::Graph(GraphCommand {
277 command: GraphSubcommand::Build { output, dry_run },
278 })) => {
279 assert!(output.is_none());
280 assert!(!dry_run);
281 }
282 _ => panic!("Expected Graph Build command"),
283 }
284 }
285
286 #[test]
287 fn test_graph_build_dry_run() {
288 let args = CliArgs::parse_from(["test", "graph", "build", "--dry-run"]);
289 match args.command {
290 Some(BaseCommand::Graph(GraphCommand {
291 command: GraphSubcommand::Build { dry_run, .. },
292 })) => {
293 assert!(dry_run);
294 }
295 _ => panic!("Expected Graph Build command with dry_run"),
296 }
297 }
298
299 #[test]
300 fn test_graph_validate_command() {
301 let args = CliArgs::parse_from(["test", "graph", "validate"]);
302 match args.command {
303 Some(BaseCommand::Graph(GraphCommand {
304 command: GraphSubcommand::Validate,
305 })) => {}
306 _ => panic!("Expected Graph Validate command"),
307 }
308 }
309
310 #[test]
311 fn test_graph_stats_command() {
312 let args = CliArgs::parse_from(["test", "graph", "stats"]);
313 match args.command {
314 Some(BaseCommand::Graph(GraphCommand {
315 command: GraphSubcommand::Stats,
316 })) => {}
317 _ => panic!("Expected Graph Stats command"),
318 }
319 }
320
321 #[test]
322 fn test_graph_query_command() {
323 let args = CliArgs::parse_from(["test", "graph", "query", "--id", "node-1"]);
324 match args.command {
325 Some(BaseCommand::Graph(GraphCommand {
326 command: GraphSubcommand::Query { id, query_type, to },
327 })) => {
328 assert_eq!(id, "node-1");
329 assert_eq!(query_type, "related");
330 assert!(to.is_none());
331 }
332 _ => panic!("Expected Graph Query command"),
333 }
334 }
335
336 #[test]
337 fn test_graph_query_path() {
338 let args = CliArgs::parse_from([
339 "test",
340 "graph",
341 "query",
342 "--id",
343 "a",
344 "--query-type",
345 "path",
346 "--to",
347 "b",
348 ]);
349 match args.command {
350 Some(BaseCommand::Graph(GraphCommand {
351 command: GraphSubcommand::Query { id, query_type, to },
352 })) => {
353 assert_eq!(id, "a");
354 assert_eq!(query_type, "path");
355 assert_eq!(to, Some("b".to_string()));
356 }
357 _ => panic!("Expected Graph Query path command"),
358 }
359 }
360
361 #[test]
366 fn test_config_path_command() {
367 let args = CliArgs::parse_from(["test", "config", "path"]);
368 match args.command {
369 Some(BaseCommand::Config(ConfigCommand {
370 command: ConfigAction::Path,
371 })) => {}
372 _ => panic!("Expected Config Path command"),
373 }
374 }
375
376 #[test]
377 fn test_config_get_command() {
378 let args = CliArgs::parse_from(["test", "config", "get", "server.port"]);
379 match args.command {
380 Some(BaseCommand::Config(ConfigCommand {
381 command: ConfigAction::Get { key },
382 })) => {
383 assert_eq!(key, "server.port");
384 }
385 _ => panic!("Expected Config Get command"),
386 }
387 }
388
389 #[test]
390 fn test_config_set_command() {
391 let args = CliArgs::parse_from(["test", "config", "set", "server.port", "8080"]);
392 match args.command {
393 Some(BaseCommand::Config(ConfigCommand {
394 command: ConfigAction::Set { key, value },
395 })) => {
396 assert_eq!(key, "server.port");
397 assert_eq!(value, "8080");
398 }
399 _ => panic!("Expected Config Set command"),
400 }
401 }
402
403 #[test]
404 fn test_config_init_command() {
405 let args = CliArgs::parse_from(["test", "config", "init"]);
406 match args.command {
407 Some(BaseCommand::Config(ConfigCommand {
408 command: ConfigAction::Init { file, force },
409 })) => {
410 assert!(file.is_none());
411 assert!(!force);
412 }
413 _ => panic!("Expected Config Init command"),
414 }
415 }
416
417 #[test]
418 fn test_config_init_force() {
419 let args = CliArgs::parse_from(["test", "config", "init", "--force"]);
420 match args.command {
421 Some(BaseCommand::Config(ConfigCommand {
422 command: ConfigAction::Init { force, .. },
423 })) => {
424 assert!(force);
425 }
426 _ => panic!("Expected Config Init command with force"),
427 }
428 }
429
430 #[test]
431 fn test_config_export_command() {
432 let args = CliArgs::parse_from(["test", "config", "export"]);
433 match args.command {
434 Some(BaseCommand::Config(ConfigCommand {
435 command: ConfigAction::Export { docker_env },
436 })) => {
437 assert!(!docker_env);
438 }
439 _ => panic!("Expected Config Export command"),
440 }
441 }
442
443 #[test]
444 fn test_config_export_docker_env() {
445 let args = CliArgs::parse_from(["test", "config", "export", "--docker-env"]);
446 match args.command {
447 Some(BaseCommand::Config(ConfigCommand {
448 command: ConfigAction::Export { docker_env },
449 })) => {
450 assert!(docker_env);
451 }
452 _ => panic!("Expected Config Export command with docker_env"),
453 }
454 }
455}