1#![deny(missing_docs)]
51
52pub mod config;
53pub mod scaffolding;
54pub mod templates;
55pub mod validation;
56
57pub use config::{Archetype, ProjectConfig};
58
59use std::path::{Path, PathBuf};
60
61use clap::{Parser, Subcommand, ValueEnum};
62
63#[derive(Debug, Clone, Copy, Default, ValueEnum)]
65pub enum CliArchetype {
66 #[default]
68 Basic,
69 Gateway,
71 Consumer,
73 Producer,
75 Bff,
77 Scheduled,
79 WebsocketGateway,
81 SagaOrchestrator,
83 LegacyAdapter,
85}
86
87impl From<CliArchetype> for Archetype {
88 fn from(cli: CliArchetype) -> Self {
89 match cli {
90 CliArchetype::Basic => Archetype::Basic,
91 CliArchetype::Gateway => Archetype::Gateway,
92 CliArchetype::Consumer => Archetype::Consumer,
93 CliArchetype::Producer => Archetype::Producer,
94 CliArchetype::Bff => Archetype::Bff,
95 CliArchetype::Scheduled => Archetype::Scheduled,
96 CliArchetype::WebsocketGateway => Archetype::WebSocketGateway,
97 CliArchetype::SagaOrchestrator => Archetype::SagaOrchestrator,
98 CliArchetype::LegacyAdapter => Archetype::AntiCorruptionLayer,
99 }
100 }
101}
102
103#[derive(Parser)]
104#[command(name = "allframe")]
105#[command(about = "AllFrame CLI - The composable Rust API framework", long_about = None)]
106#[command(version)]
107struct Cli {
108 #[command(subcommand)]
109 command: Commands,
110}
111
112#[derive(Subcommand)]
113enum Commands {
114 Ignite {
116 name: PathBuf,
118
119 #[arg(short, long, value_enum, default_value_t = CliArchetype::Basic)]
121 archetype: CliArchetype,
122
123 #[arg(long)]
125 service_name: Option<String>,
126
127 #[arg(long)]
129 api_base_url: Option<String>,
130
131 #[arg(long)]
133 group_id: Option<String>,
134
135 #[arg(long)]
137 brokers: Option<String>,
138
139 #[arg(long)]
141 all_features: bool,
142 },
143 Forge {
145 prompt: String,
147 },
148}
149
150pub fn run() -> anyhow::Result<()> {
158 let cli = Cli::parse();
159
160 match cli.command {
161 Commands::Ignite {
162 name,
163 archetype,
164 service_name,
165 api_base_url,
166 group_id,
167 brokers: _,
168 all_features: _,
169 } => {
170 ignite_project(&name, archetype, service_name, api_base_url, group_id)?;
171 }
172 Commands::Forge { prompt } => {
173 forge_code(&prompt)?;
174 }
175 }
176
177 Ok(())
178}
179
180fn ignite_project(
185 project_path: &Path,
186 archetype: CliArchetype,
187 service_name: Option<String>,
188 api_base_url: Option<String>,
189 group_id: Option<String>,
190) -> anyhow::Result<()> {
191 let project_name = project_path
192 .file_name()
193 .and_then(|n| n.to_str())
194 .ok_or_else(|| anyhow::anyhow!("Invalid project path"))?;
195
196 validation::validate_project_name(project_name)?;
197
198 if project_path.exists() {
199 anyhow::bail!("Directory already exists: {}", project_path.display());
200 }
201
202 std::fs::create_dir_all(project_path)?;
203
204 let config = match archetype {
206 CliArchetype::Basic => ProjectConfig::new(project_name),
207 CliArchetype::Gateway => {
208 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Gateway);
209
210 if let Some(gateway) = config.gateway.as_mut() {
212 if let Some(svc_name) = service_name.clone() {
213 gateway.service_name = svc_name.clone();
214 gateway.display_name = to_title_case(&svc_name);
215 } else {
216 gateway.service_name = project_name.replace('-', "_");
218 gateway.display_name = to_title_case(project_name);
219 }
220
221 if let Some(url) = api_base_url {
222 gateway.api_base_url = url;
223 }
224 }
225
226 config
227 }
228 CliArchetype::Consumer => {
229 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Consumer);
230
231 if let Some(consumer) = config.consumer.as_mut() {
233 if let Some(svc_name) = service_name {
234 consumer.service_name = svc_name.clone();
235 consumer.display_name = to_title_case(&svc_name);
236 } else {
237 consumer.service_name = project_name.replace('-', "_");
239 consumer.display_name = to_title_case(project_name);
240 }
241
242 if let Some(gid) = group_id {
243 consumer.group_id = gid;
244 } else {
245 consumer.group_id = format!("{}-group", consumer.service_name);
246 }
247 }
248
249 config
250 }
251 CliArchetype::Producer => {
252 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Producer);
253
254 if let Some(producer) = config.producer.as_mut() {
256 if let Some(svc_name) = service_name.clone() {
257 producer.service_name = svc_name.clone();
258 producer.display_name = to_title_case(&svc_name);
259 } else {
260 producer.service_name = project_name.replace('-', "_");
262 producer.display_name = to_title_case(project_name);
263 }
264 }
265
266 config
267 }
268 CliArchetype::Bff => {
269 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Bff);
270
271 if let Some(bff) = config.bff.as_mut() {
273 if let Some(svc_name) = service_name.clone() {
274 bff.service_name = svc_name.clone();
275 bff.display_name = to_title_case(&svc_name);
276 } else {
277 bff.service_name = project_name.replace('-', "_");
279 bff.display_name = to_title_case(project_name);
280 }
281
282 if let Some(url) = api_base_url {
283 if let Some(backend) = bff.backends.first_mut() {
284 backend.base_url = url;
285 }
286 }
287 }
288
289 config
290 }
291 CliArchetype::Scheduled => {
292 let mut config = ProjectConfig::new(project_name).with_archetype(Archetype::Scheduled);
293
294 if let Some(scheduled) = config.scheduled.as_mut() {
296 if let Some(svc_name) = service_name.clone() {
297 scheduled.service_name = svc_name.clone();
298 scheduled.display_name = to_title_case(&svc_name);
299 } else {
300 scheduled.service_name = project_name.replace('-', "_");
302 scheduled.display_name = to_title_case(project_name);
303 }
304 }
305
306 config
307 }
308 CliArchetype::WebsocketGateway => {
309 let mut config =
310 ProjectConfig::new(project_name).with_archetype(Archetype::WebSocketGateway);
311
312 if let Some(ws) = config.websocket_gateway.as_mut() {
314 if let Some(svc_name) = service_name.clone() {
315 ws.service_name = svc_name.clone();
316 ws.display_name = to_title_case(&svc_name);
317 } else {
318 ws.service_name = project_name.replace('-', "_");
320 ws.display_name = to_title_case(project_name);
321 }
322 }
323
324 config
325 }
326 CliArchetype::SagaOrchestrator => {
327 let mut config =
328 ProjectConfig::new(project_name).with_archetype(Archetype::SagaOrchestrator);
329
330 if let Some(saga) = config.saga_orchestrator.as_mut() {
332 if let Some(svc_name) = service_name.clone() {
333 saga.service_name = svc_name.clone();
334 saga.display_name = to_title_case(&svc_name);
335 } else {
336 saga.service_name = project_name.replace('-', "_");
338 saga.display_name = to_title_case(project_name);
339 }
340 }
341
342 config
343 }
344 CliArchetype::LegacyAdapter => {
345 let mut config =
346 ProjectConfig::new(project_name).with_archetype(Archetype::AntiCorruptionLayer);
347
348 if let Some(acl) = config.acl.as_mut() {
350 if let Some(svc_name) = service_name {
351 acl.service_name = svc_name.clone();
352 acl.display_name = to_title_case(&svc_name);
353 } else {
354 acl.service_name = project_name.replace('-', "_");
356 acl.display_name = to_title_case(project_name);
357 }
358
359 if let Some(url) = api_base_url {
360 acl.legacy_system.connection_string = url;
361 }
362 }
363
364 config
365 }
366 };
367
368 match config.archetype {
370 Archetype::Basic => {
371 scaffolding::create_directory_structure(project_path)?;
372 scaffolding::generate_files(project_path, project_name)?;
373 }
374 Archetype::Gateway => {
375 scaffolding::create_gateway_structure(project_path)?;
376 scaffolding::generate_gateway_files(project_path, &config)?;
377 }
378 Archetype::Consumer => {
379 scaffolding::create_consumer_structure(project_path)?;
380 scaffolding::generate_consumer_files(project_path, &config)?;
381 }
382 Archetype::Producer => {
383 scaffolding::create_producer_structure(project_path)?;
384 scaffolding::generate_producer_files(project_path, &config)?;
385 }
386 Archetype::Bff => {
387 scaffolding::create_bff_structure(project_path)?;
388 scaffolding::generate_bff_files(project_path, &config)?;
389 }
390 Archetype::Scheduled => {
391 scaffolding::create_scheduled_structure(project_path)?;
392 scaffolding::generate_scheduled_files(project_path, &config)?;
393 }
394 Archetype::WebSocketGateway => {
395 scaffolding::create_websocket_structure(project_path)?;
396 scaffolding::generate_websocket_files(project_path, &config)?;
397 }
398 Archetype::SagaOrchestrator => {
399 scaffolding::create_saga_structure(project_path)?;
400 scaffolding::generate_saga_files(project_path, &config)?;
401 }
402 Archetype::AntiCorruptionLayer => {
403 scaffolding::create_acl_structure(project_path)?;
404 scaffolding::generate_acl_files(project_path, &config)?;
405 }
406 _ => {
407 anyhow::bail!("Archetype {:?} is not yet implemented", config.archetype);
408 }
409 }
410
411 println!(
412 "AllFrame {} project created successfully: {}",
413 config.archetype, project_name
414 );
415 println!("\nNext steps:");
416 println!(" cd {}", project_name);
417
418 match config.archetype {
419 Archetype::Gateway => {
420 println!(" # Edit src/config.rs to set your API credentials");
421 println!(" cargo build");
422 println!(" cargo run");
423 }
424 Archetype::Consumer => {
425 println!(" # Set environment variables for broker connection");
426 println!(" # See README.md for configuration options");
427 println!(" cargo build");
428 println!(" cargo run");
429 }
430 Archetype::Producer => {
431 println!(" # Set DATABASE_URL and KAFKA_BROKERS environment variables");
432 println!(" # Run database migrations (see README.md)");
433 println!(" cargo build");
434 println!(" cargo run");
435 }
436 Archetype::Bff => {
437 println!(" # Set API_BASE_URL for backend service connection");
438 println!(" # See README.md for configuration options");
439 println!(" cargo build");
440 println!(" cargo run");
441 }
442 Archetype::Scheduled => {
443 println!(" # Configure jobs in src/config.rs");
444 println!(" # See README.md for cron expression reference");
445 println!(" cargo build");
446 println!(" cargo run");
447 }
448 Archetype::WebSocketGateway => {
449 println!(" # Configure channels in src/config.rs");
450 println!(" # See README.md for WebSocket API documentation");
451 println!(" cargo build");
452 println!(" cargo run");
453 }
454 Archetype::SagaOrchestrator => {
455 println!(" # Configure sagas and steps in src/config.rs");
456 println!(" # See README.md for saga pattern documentation");
457 println!(" cargo build");
458 println!(" cargo run");
459 }
460 Archetype::AntiCorruptionLayer => {
461 println!(" # Configure legacy system connection in src/config.rs");
462 println!(" # See README.md for transformer implementation guide");
463 println!(" cargo build");
464 println!(" cargo run");
465 }
466 _ => {
467 println!(" cargo test");
468 println!(" cargo run");
469 }
470 }
471
472 Ok(())
473}
474
475fn to_title_case(s: &str) -> String {
477 s.split(|c| c == '-' || c == '_')
478 .map(|word| {
479 let mut chars = word.chars();
480 match chars.next() {
481 None => String::new(),
482 Some(first) => first.to_uppercase().chain(chars).collect(),
483 }
484 })
485 .collect::<Vec<_>>()
486 .join(" ")
487}
488
489fn forge_code(_prompt: &str) -> anyhow::Result<()> {
491 anyhow::bail!("allframe forge is not yet implemented")
492}