Skip to main content

ferro_cli/templates/
make.rs

1// Make command templates (used by `make:*` commands)
2
3/// Template for generating new middleware with make:middleware command
4pub 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
24/// Template for generating new resource with make:resource command
25pub 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
47/// Template for generating new controller with make:controller command
48pub 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
64/// Template for generating new action with make:action command
65pub 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
85/// Template for generating new Inertia page with make:inertia command
86pub 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
102/// Template for generating a JSON-UI view file (--no-ai fallback).
103pub 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
141/// Template for generating new error with make:error command
142pub fn error_template(struct_name: &str) -> String {
143    // Convert PascalCase to human readable message
144    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
165/// Template for generating new scheduled task with make:task command
166pub 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
221// Event templates
222
223/// Template for generating new events with make:event command
224pub 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
265/// Template for events/mod.rs
266pub 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
275// Listener templates
276
277/// Template for generating new listeners with make:listener command
278pub 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
317/// Template for listeners/mod.rs
318pub 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
326// Job templates
327
328/// Template for generating new jobs with make:job command
329pub 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
391/// Template for jobs/mod.rs
392pub 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
401// Notification templates
402
403/// Template for generating new notifications with make:notification command
404pub 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
456/// Template for notifications/mod.rs
457pub 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
466// Seeder templates
467
468/// Template for generating new seeder with make:seeder command
469pub 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
514/// Template for seeders/mod.rs
515pub 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
543/// Template for generating new factory with make:factory command
544pub 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
611/// Template for factories/mod.rs
612pub 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
636/// Template for generating new policy with make:policy command
637pub 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
743// Lang templates
744
745/// Template for lang/{locale}/validation.json (English validation messages)
746pub fn lang_validation_json() -> &'static str {
747    include_str!("files/lang/validation.json.tpl")
748}
749
750/// Template for lang/{locale}/app.json (starter application translations)
751pub fn lang_app_json() -> &'static str {
752    include_str!("files/lang/app.json.tpl")
753}
754
755/// Template for policies/mod.rs
756pub 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}