1pub fn middleware_template(name: &str, struct_name: &str) -> String {
5 format!(
6 r#"//! {name} middleware
7
8use ferro::{{async_trait, Middleware, Next, Request, Response}};
9
10/// {name} middleware
11pub struct {struct_name};
12
13#[async_trait]
14impl Middleware for {struct_name} {{
15 async fn handle(&self, request: Request, next: Next) -> Response {{
16 // TODO: Implement middleware logic
17 next(request).await
18 }}
19}}
20"#
21 )
22}
23
24pub fn resource_template(name: &str, model: Option<&str>) -> String {
26 let model_attribute = match model {
27 Some(path) => format!("#[resource(model = \"{path}\")]\n"),
28 None => String::new(),
29 };
30
31 format!(
32 r#"use ferro::{{ApiResource, Resource, ResourceMap, Request}};
33
34#[derive(ApiResource)]
35{model_attribute}pub struct {name} {{
36 pub id: i64,
37 // Add fields from your model here
38 // #[resource(rename = "display_name")]
39 // pub name: String,
40 // #[resource(skip)]
41 // pub password_hash: String,
42}}
43"#
44 )
45}
46
47pub fn controller_template(name: &str) -> String {
49 format!(
50 r#"//! {name} controller
51
52use ferro::{{handler, json_response, Request, Response}};
53
54#[handler]
55pub async fn invoke(_req: Request) -> Response {{
56 json_response!({{
57 "controller": "{name}"
58 }})
59}}
60"#
61 )
62}
63
64pub fn action_template(name: &str, struct_name: &str) -> String {
66 format!(
67 r#"//! {name} action
68
69use ferro::injectable;
70
71#[injectable]
72pub struct {struct_name} {{
73 // Dependencies injected via container
74}}
75
76impl {struct_name} {{
77 pub fn execute(&self) {{
78 // TODO: Implement action logic
79 }}
80}}
81"#
82 )
83}
84
85pub fn inertia_page_template(component_name: &str) -> String {
87 format!(
88 r#"export default function {component_name}() {{
89 return (
90 <div className="font-sans p-8 max-w-xl mx-auto">
91 <h1 className="text-3xl font-bold">{component_name}</h1>
92 <p className="mt-2">
93 Edit <code className="bg-muted px-1 rounded">frontend/src/pages/{component_name}.tsx</code> to get started.
94 </p>
95 </div>
96 )
97}}
98"#
99 )
100}
101
102pub fn json_view_template(name: &str, title: &str, layout: &str) -> String {
104 format!(
105 r#"//! {title} JSON-UI view
106
107use ferro::{{
108 ComponentNode, Component, CardProps, JsonUiView, TextElement, TextProps,
109}};
110
111/// Build the {title} view.
112pub fn view() -> JsonUiView {{
113 JsonUiView::new()
114 .title("{title}")
115 .layout("{layout}")
116 .component(ComponentNode {{
117 key: "heading".to_string(),
118 component: Component::Text(TextProps {{
119 content: "{title}".to_string(),
120 element: TextElement::H1,
121 }}),
122 action: None,
123 visibility: None,
124 }})
125 .component(ComponentNode {{
126 key: "card".to_string(),
127 component: Component::Card(CardProps {{
128 title: "{title}".to_string(),
129 description: Some("Edit src/views/{name}.rs to customize this view.".to_string()),
130 children: vec![],
131 footer: vec![],
132 }}),
133 action: None,
134 visibility: None,
135 }})
136}}
137"#,
138 )
139}
140
141pub fn error_template(struct_name: &str) -> String {
143 let mut message = String::new();
145 for (i, c) in struct_name.chars().enumerate() {
146 if c.is_uppercase() && i > 0 {
147 message.push(' ');
148 message.push(c.to_lowercase().next().unwrap());
149 } else {
150 message.push(c);
151 }
152 }
153
154 format!(
155 r#"//! {struct_name} error
156
157use ferro::domain_error;
158
159#[domain_error(status = 500, message = "{message}")]
160pub struct {struct_name};
161"#
162 )
163}
164
165pub fn task_template(file_name: &str, struct_name: &str) -> String {
167 format!(
168 r#"//! {struct_name} scheduled task
169//!
170//! Created with `ferro make:task {file_name}`
171
172use async_trait::async_trait;
173use ferro::{{Task, TaskResult}};
174
175/// {struct_name} - A scheduled task
176///
177/// Implement your task logic in the `handle()` method.
178/// Register this task in `src/schedule.rs` with the fluent API.
179///
180/// # Example Registration
181///
182/// ```rust,ignore
183/// // In src/schedule.rs
184/// use crate::tasks::{file_name};
185///
186/// schedule.add(
187/// schedule.task({struct_name}::new())
188/// .daily()
189/// .at("03:00")
190/// .name("{file_name}")
191/// .description("TODO: Add task description")
192/// );
193/// ```
194pub struct {struct_name};
195
196impl {struct_name} {{
197 /// Create a new instance of this task
198 pub fn new() -> Self {{
199 Self
200 }}
201}}
202
203impl Default for {struct_name} {{
204 fn default() -> Self {{
205 Self::new()
206 }}
207}}
208
209#[async_trait]
210impl Task for {struct_name} {{
211 async fn handle(&self) -> TaskResult {{
212 // TODO: Implement your task logic here
213 println!("Running {struct_name}...");
214 Ok(())
215 }}
216}}
217"#
218 )
219}
220
221pub fn event_template(file_name: &str, struct_name: &str) -> String {
225 format!(
226 r#"//! {struct_name} event
227//!
228//! Created with `ferro make:event {file_name}`
229
230use ferro_events::Event;
231use serde::{{Deserialize, Serialize}};
232
233/// {struct_name} - A domain event
234///
235/// Events represent something that has happened in your application.
236/// Listeners can react to these events asynchronously.
237///
238/// # Dispatching
239///
240/// ```rust,ignore
241/// use crate::events::{file_name}::{struct_name};
242///
243/// // Ergonomic dispatch (awaits all listeners)
244/// {struct_name} {{ /* fields */ }}.dispatch().await?;
245///
246/// // Fire and forget (spawns background task)
247/// {struct_name} {{ /* fields */ }}.dispatch_sync();
248/// ```
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct {struct_name} {{
251 // TODO: Add event data fields
252 // pub user_id: i64,
253 // pub created_at: chrono::DateTime<chrono::Utc>,
254}}
255
256impl Event for {struct_name} {{
257 fn name(&self) -> &'static str {{
258 "{struct_name}"
259 }}
260}}
261"#
262 )
263}
264
265pub fn events_mod() -> &'static str {
267 r#"//! Application events
268//!
269//! This module contains domain events that can be dispatched
270//! and handled by listeners.
271
272"#
273}
274
275pub fn listener_template(file_name: &str, struct_name: &str, event_type: &str) -> String {
279 format!(
280 r#"//! {struct_name} listener
281//!
282//! Created with `ferro make:listener {file_name}`
283
284use ferro_events::{{async_trait, Error, Listener}};
285// TODO: Import the event type
286// use crate::events::your_event::YourEvent;
287
288/// {struct_name} - An event listener
289///
290/// Listeners react to events and perform side effects.
291/// They can be synchronous or queued for background processing.
292///
293/// # Example Registration
294///
295/// ```rust,ignore
296/// // In your app initialization
297/// use ferro_events::EventDispatcher;
298/// use crate::listeners::{file_name}::{struct_name};
299///
300/// let mut dispatcher = EventDispatcher::new();
301/// dispatcher.listen::<{event_type}, _>({struct_name});
302/// ```
303pub struct {struct_name};
304
305#[async_trait]
306impl Listener<{event_type}> for {struct_name} {{
307 async fn handle(&self, event: &{event_type}) -> Result<(), Error> {{
308 // TODO: Implement listener logic
309 tracing::info!("{struct_name} handling event: {{:?}}", event);
310 Ok(())
311 }}
312}}
313"#
314 )
315}
316
317pub fn listeners_mod() -> &'static str {
319 r#"//! Application event listeners
320//!
321//! This module contains listeners that react to domain events.
322
323"#
324}
325
326pub fn job_template(file_name: &str, struct_name: &str) -> String {
330 format!(
331 r#"//! {struct_name} background job
332//!
333//! Created with `ferro make:job {file_name}`
334
335use ferro_queue::{{async_trait, Error, Job, Queueable}};
336use serde::{{Deserialize, Serialize}};
337
338/// {struct_name} - A background job
339///
340/// Jobs are queued for background processing by workers.
341/// They support retries, delays, and queue prioritization.
342///
343/// # Example
344///
345/// ```rust,ignore
346/// use crate::jobs::{file_name}::{struct_name};
347///
348/// // Dispatch immediately
349/// {struct_name} {{ /* fields */ }}.dispatch().await?;
350///
351/// // Dispatch with delay
352/// {struct_name} {{ /* fields */ }}
353/// .delay(std::time::Duration::from_secs(60))
354/// .dispatch()
355/// .await?;
356///
357/// // Dispatch to specific queue
358/// {struct_name} {{ /* fields */ }}
359/// .on_queue("high-priority")
360/// .dispatch()
361/// .await?;
362/// ```
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct {struct_name} {{
365 // TODO: Add job data fields
366 // pub user_id: i64,
367 // pub payload: String,
368}}
369
370#[async_trait]
371impl Job for {struct_name} {{
372 async fn handle(&self) -> Result<(), Error> {{
373 // TODO: Implement job logic
374 tracing::info!("Processing {struct_name}: {{:?}}", self);
375 Ok(())
376 }}
377
378 fn max_retries(&self) -> u32 {{
379 3
380 }}
381
382 fn retry_delay(&self, attempt: u32) -> std::time::Duration {{
383 // Exponential backoff
384 std::time::Duration::from_secs(2u64.pow(attempt))
385 }}
386}}
387"#
388 )
389}
390
391pub fn jobs_mod() -> &'static str {
393 r#"//! Application background jobs
394//!
395//! This module contains jobs that are processed asynchronously
396//! by queue workers.
397
398"#
399}
400
401pub fn notification_template(file_name: &str, struct_name: &str) -> String {
405 format!(
406 r#"//! {struct_name} notification
407//!
408//! Created with `ferro make:notification {file_name}`
409
410use ferro_notifications::{{Channel, DatabaseMessage, MailMessage, Notification}};
411
412/// {struct_name} - A multi-channel notification
413///
414/// Notifications can be sent through multiple channels:
415/// - Mail: Email via SMTP
416/// - Database: In-app notifications
417/// - Slack: Webhook messages
418///
419/// # Example
420///
421/// ```rust,ignore
422/// use crate::notifications::{file_name}::{struct_name};
423///
424/// // Send notification to a user
425/// user.notify({struct_name} {{ /* fields */ }}).await?;
426/// ```
427pub struct {struct_name} {{
428 // TODO: Add notification data fields
429 // pub order_id: i64,
430 // pub tracking_number: String,
431}}
432
433impl Notification for {struct_name} {{
434 fn via(&self) -> Vec<Channel> {{
435 // TODO: Choose notification channels
436 vec![Channel::Mail, Channel::Database]
437 }}
438
439 fn to_mail(&self) -> Option<MailMessage> {{
440 Some(MailMessage::new()
441 .subject("{struct_name}")
442 .body("TODO: Add notification message"))
443 }}
444
445 fn to_database(&self) -> Option<DatabaseMessage> {{
446 Some(DatabaseMessage::new("{file_name}")
447 // TODO: Add notification data
448 // .data("order_id", self.order_id)
449 )
450 }}
451}}
452"#
453 )
454}
455
456pub fn notifications_mod() -> &'static str {
458 r#"//! Application notifications
459//!
460//! This module contains notifications that can be sent
461//! through multiple channels (mail, database, slack, etc.).
462
463"#
464}
465
466pub fn seeder_template(file_name: &str, struct_name: &str) -> String {
470 format!(
471 r#"//! {struct_name} database seeder
472//!
473//! Created with `ferro make:seeder {file_name}`
474
475use ferro::{{async_trait, FrameworkError, Seeder}};
476use sea_orm::DatabaseConnection;
477
478/// {struct_name} - A database seeder
479///
480/// Seeders populate the database with test or initial data.
481/// Implement the `run` method to insert records.
482///
483/// # Example Registration
484///
485/// ```rust,ignore
486/// // In src/seeders/mod.rs
487/// use ferro::SeederRegistry;
488/// use super::{file_name}::{struct_name};
489///
490/// pub fn register() -> SeederRegistry {{
491/// SeederRegistry::new()
492/// .add::<{struct_name}>()
493/// }}
494/// ```
495#[derive(Default)]
496pub struct {struct_name};
497
498#[async_trait]
499impl Seeder for {struct_name} {{
500 async fn run(&self, db: &DatabaseConnection) -> Result<(), FrameworkError> {{
501 // TODO: Implement seeder logic using `db`
502 // Example:
503 // use sea_orm::{{ActiveModelTrait, ActiveValue::Set}};
504 // users::ActiveModel {{ name: Set("Admin".into()), ..Default::default() }}
505 // .insert(db).await?;
506
507 Ok(())
508 }}
509}}
510"#
511 )
512}
513
514pub fn seeders_mod() -> &'static str {
516 r#"//! Database seeders
517//!
518//! This module contains seeders that populate the database with test
519//! or initial data.
520//!
521//! # Usage
522//!
523//! Register seeders in the `register()` function and run with:
524//! ```bash
525//! ./target/debug/app db:seed # Run all seeders
526//! ./target/debug/app db:seed --class UsersSeeder # Run specific seeder
527//! ```
528
529use ferro::SeederRegistry;
530
531/// Register all seeders
532///
533/// Add your seeders here in the order you want them to run.
534/// Seeders are executed in registration order.
535pub fn register() -> SeederRegistry {
536 SeederRegistry::new()
537 // .add::<UsersSeeder>()
538 // .add::<ProductsSeeder>()
539}
540"#
541}
542
543pub fn factory_template(file_name: &str, struct_name: &str, model_name: &str) -> String {
545 format!(
546 r#"//! {struct_name} factory
547//!
548//! Created with `ferro make:factory {file_name}`
549
550use ferro::testing::{{Factory, FactoryTraits, Fake}};
551// use ferro::testing::DatabaseFactory;
552// use crate::models::{model_name};
553
554/// Factory for creating {model_name} instances in tests
555#[derive(Clone)]
556pub struct {struct_name} {{
557 // Add fields matching your model
558 pub id: i64,
559 pub name: String,
560 pub email: String,
561 pub created_at: String,
562}}
563
564impl Factory for {struct_name} {{
565 fn definition() -> Self {{
566 Self {{
567 id: 0, // Will be set by database
568 name: Fake::name(),
569 email: Fake::email(),
570 created_at: Fake::datetime(),
571 }}
572 }}
573
574 fn traits() -> FactoryTraits<Self> {{
575 FactoryTraits::new()
576 // .define("admin", |m: &mut Self| m.role = "admin".to_string())
577 // .define("verified", |m: &mut Self| m.verified = true)
578 }}
579}}
580
581// Uncomment to enable database persistence with create():
582//
583// #[ferro::async_trait]
584// impl DatabaseFactory for {struct_name} {{
585// type Entity = {model_name}::Entity;
586// type ActiveModel = {model_name}::ActiveModel;
587// }}
588
589// Usage in tests:
590//
591// // Make without persisting:
592// let model = {struct_name}::factory().make();
593//
594// // Apply named trait:
595// let admin = {struct_name}::factory().trait_("admin").make();
596//
597// // With inline state:
598// let model = {struct_name}::factory()
599// .state(|m| m.name = "Custom".into())
600// .make();
601//
602// // Create with database persistence:
603// let model = {struct_name}::factory().create().await?;
604//
605// // Create multiple:
606// let models = {struct_name}::factory().count(5).create_many().await?;
607"#
608 )
609}
610
611pub fn factories_mod() -> &'static str {
613 r#"//! Test factories
614//!
615//! This module contains factories for generating fake model data in tests.
616//!
617//! # Usage
618//!
619//! ```rust,ignore
620//! use crate::factories::UserFactory;
621//! use ferro::testing::Factory;
622//!
623//! // Make without persisting
624//! let user = UserFactory::factory().make();
625//!
626//! // Create with database persistence
627//! let user = UserFactory::factory().create().await?;
628//!
629//! // Create multiple
630//! let users = UserFactory::factory().count(5).create_many().await?;
631//! ```
632
633"#
634}
635
636pub fn policy_template(file_name: &str, struct_name: &str, model_name: &str) -> String {
638 format!(
639 r#"//! {struct_name} authorization policy
640//!
641//! Created with `ferro make:policy {file_name}`
642
643use ferro::authorization::{{AuthResponse, Policy}};
644// TODO: Import your model and user types
645// use crate::models::{model_name}::{{self, Model as {model_name}}};
646// use crate::models::users::Model as User;
647
648/// {struct_name} - Authorization policy for {model_name}
649///
650/// This policy defines who can perform actions on {model_name} records.
651///
652/// # Example Usage
653///
654/// ```rust,ignore
655/// use crate::policies::{file_name}::{struct_name};
656///
657/// let policy = {struct_name};
658///
659/// // Check if user can update the model
660/// if policy.update(&user, &model).allowed() {{
661/// // Proceed with update
662/// }}
663///
664/// // Use the check method for string-based ability lookup
665/// let response = policy.check(&user, "update", Some(&model));
666/// ```
667pub struct {struct_name};
668
669impl Policy<{model_name}> for {struct_name} {{
670 type User = User;
671
672 /// Run before any other authorization checks.
673 ///
674 /// Return `Some(true)` to allow, `Some(false)` to deny,
675 /// or `None` to continue to the specific ability check.
676 fn before(&self, user: &Self::User, _ability: &str) -> Option<bool> {{
677 // Example: Admin bypass
678 // if user.is_admin {{
679 // return Some(true);
680 // }}
681 None
682 }}
683
684 /// Determine whether the user can view any models.
685 fn view_any(&self, _user: &Self::User) -> AuthResponse {{
686 // TODO: Implement authorization logic
687 AuthResponse::allow()
688 }}
689
690 /// Determine whether the user can view the model.
691 fn view(&self, _user: &Self::User, _model: &{model_name}) -> AuthResponse {{
692 // TODO: Implement authorization logic
693 AuthResponse::allow()
694 }}
695
696 /// Determine whether the user can create models.
697 fn create(&self, _user: &Self::User) -> AuthResponse {{
698 // TODO: Implement authorization logic
699 AuthResponse::allow()
700 }}
701
702 /// Determine whether the user can update the model.
703 fn update(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
704 // TODO: Implement authorization logic
705 // Example: Only owner can update
706 // if user.auth_identifier() == model.user_id as i64 {{
707 // AuthResponse::allow()
708 // }} else {{
709 // AuthResponse::deny("You do not own this resource.")
710 // }}
711 AuthResponse::deny_silent()
712 }}
713
714 /// Determine whether the user can delete the model.
715 fn delete(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
716 // Same as update by default
717 self.update(user, model)
718 }}
719
720 /// Determine whether the user can restore the model.
721 fn restore(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
722 self.update(user, model)
723 }}
724
725 /// Determine whether the user can permanently delete the model.
726 fn force_delete(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
727 // Usually more restrictive than delete
728 self.delete(user, model)
729 }}
730}}
731
732// TODO: Uncomment and define placeholder types until you import the real ones
733// struct {model_name};
734// struct User;
735// impl ferro::auth::Authenticatable for User {{
736// fn auth_identifier(&self) -> i64 {{ 0 }}
737// fn as_any(&self) -> &dyn std::any::Any {{ self }}
738// }}
739"#
740 )
741}
742
743pub fn lang_validation_json() -> &'static str {
747 include_str!("files/lang/validation.json.tpl")
748}
749
750pub fn lang_app_json() -> &'static str {
752 include_str!("files/lang/app.json.tpl")
753}
754
755pub fn policies_mod() -> &'static str {
757 r#"//! Authorization policies
758//!
759//! This module contains policies that define who can perform actions
760//! on specific models or resources.
761//!
762//! # Usage
763//!
764//! ```rust,ignore
765//! use crate::policies::PostPolicy;
766//! use ferro::authorization::Policy;
767//!
768//! let policy = PostPolicy;
769//!
770//! // Check authorization
771//! if policy.update(&user, &post).allowed() {
772//! // Proceed with update
773//! }
774//!
775//! // Or use the generic check method
776//! let response = policy.check(&user, "update", Some(&post));
777//! ```
778
779"#
780}