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}};
476
477/// {struct_name} - A database seeder
478///
479/// Seeders populate the database with test or initial data.
480/// Implement the `run` method to insert records.
481///
482/// # Example Registration
483///
484/// ```rust,ignore
485/// // In src/seeders/mod.rs
486/// use ferro::SeederRegistry;
487/// use super::{file_name}::{struct_name};
488///
489/// pub fn register() -> SeederRegistry {{
490///     SeederRegistry::new()
491///         .add::<{struct_name}>()
492/// }}
493/// ```
494#[derive(Default)]
495pub struct {struct_name};
496
497#[async_trait]
498impl Seeder for {struct_name} {{
499    async fn run(&self) -> Result<(), FrameworkError> {{
500        // TODO: Implement seeder logic
501        // Example:
502        // User::create()
503        //     .set_name("Admin")
504        //     .set_email("admin@example.com")
505        //     .insert()
506        //     .await?;
507
508        Ok(())
509    }}
510}}
511"#
512    )
513}
514
515/// Template for seeders/mod.rs
516pub fn seeders_mod() -> &'static str {
517    r#"//! Database seeders
518//!
519//! This module contains seeders that populate the database with test
520//! or initial data.
521//!
522//! # Usage
523//!
524//! Register seeders in the `register()` function and run with:
525//! ```bash
526//! ./target/debug/app db:seed           # Run all seeders
527//! ./target/debug/app db:seed --class UsersSeeder  # Run specific seeder
528//! ```
529
530use ferro::SeederRegistry;
531
532/// Register all seeders
533///
534/// Add your seeders here in the order you want them to run.
535/// Seeders are executed in registration order.
536pub fn register() -> SeederRegistry {
537    SeederRegistry::new()
538        // .add::<UsersSeeder>()
539        // .add::<ProductsSeeder>()
540}
541"#
542}
543
544/// Template for generating new factory with make:factory command
545pub fn factory_template(file_name: &str, struct_name: &str, model_name: &str) -> String {
546    format!(
547        r#"//! {struct_name} factory
548//!
549//! Created with `ferro make:factory {file_name}`
550
551use ferro::testing::{{Factory, FactoryTraits, Fake}};
552// use ferro::testing::DatabaseFactory;
553// use crate::models::{model_name};
554
555/// Factory for creating {model_name} instances in tests
556#[derive(Clone)]
557pub struct {struct_name} {{
558    // Add fields matching your model
559    pub id: i64,
560    pub name: String,
561    pub email: String,
562    pub created_at: String,
563}}
564
565impl Factory for {struct_name} {{
566    fn definition() -> Self {{
567        Self {{
568            id: 0, // Will be set by database
569            name: Fake::name(),
570            email: Fake::email(),
571            created_at: Fake::datetime(),
572        }}
573    }}
574
575    fn traits() -> FactoryTraits<Self> {{
576        FactoryTraits::new()
577            // .define("admin", |m: &mut Self| m.role = "admin".to_string())
578            // .define("verified", |m: &mut Self| m.verified = true)
579    }}
580}}
581
582// Uncomment to enable database persistence with create():
583//
584// #[ferro::async_trait]
585// impl DatabaseFactory for {struct_name} {{
586//     type Entity = {model_name}::Entity;
587//     type ActiveModel = {model_name}::ActiveModel;
588// }}
589
590// Usage in tests:
591//
592// // Make without persisting:
593// let model = {struct_name}::factory().make();
594//
595// // Apply named trait:
596// let admin = {struct_name}::factory().trait_("admin").make();
597//
598// // With inline state:
599// let model = {struct_name}::factory()
600//     .state(|m| m.name = "Custom".into())
601//     .make();
602//
603// // Create with database persistence:
604// let model = {struct_name}::factory().create().await?;
605//
606// // Create multiple:
607// let models = {struct_name}::factory().count(5).create_many().await?;
608"#
609    )
610}
611
612/// Template for factories/mod.rs
613pub fn factories_mod() -> &'static str {
614    r#"//! Test factories
615//!
616//! This module contains factories for generating fake model data in tests.
617//!
618//! # Usage
619//!
620//! ```rust,ignore
621//! use crate::factories::UserFactory;
622//! use ferro::testing::Factory;
623//!
624//! // Make without persisting
625//! let user = UserFactory::factory().make();
626//!
627//! // Create with database persistence
628//! let user = UserFactory::factory().create().await?;
629//!
630//! // Create multiple
631//! let users = UserFactory::factory().count(5).create_many().await?;
632//! ```
633
634"#
635}
636
637/// Template for generating new policy with make:policy command
638pub fn policy_template(file_name: &str, struct_name: &str, model_name: &str) -> String {
639    format!(
640        r#"//! {struct_name} authorization policy
641//!
642//! Created with `ferro make:policy {file_name}`
643
644use ferro::authorization::{{AuthResponse, Policy}};
645// TODO: Import your model and user types
646// use crate::models::{model_name}::{{self, Model as {model_name}}};
647// use crate::models::users::Model as User;
648
649/// {struct_name} - Authorization policy for {model_name}
650///
651/// This policy defines who can perform actions on {model_name} records.
652///
653/// # Example Usage
654///
655/// ```rust,ignore
656/// use crate::policies::{file_name}::{struct_name};
657///
658/// let policy = {struct_name};
659///
660/// // Check if user can update the model
661/// if policy.update(&user, &model).allowed() {{
662///     // Proceed with update
663/// }}
664///
665/// // Use the check method for string-based ability lookup
666/// let response = policy.check(&user, "update", Some(&model));
667/// ```
668pub struct {struct_name};
669
670impl Policy<{model_name}> for {struct_name} {{
671    type User = User;
672
673    /// Run before any other authorization checks.
674    ///
675    /// Return `Some(true)` to allow, `Some(false)` to deny,
676    /// or `None` to continue to the specific ability check.
677    fn before(&self, user: &Self::User, _ability: &str) -> Option<bool> {{
678        // Example: Admin bypass
679        // if user.is_admin {{
680        //     return Some(true);
681        // }}
682        None
683    }}
684
685    /// Determine whether the user can view any models.
686    fn view_any(&self, _user: &Self::User) -> AuthResponse {{
687        // TODO: Implement authorization logic
688        AuthResponse::allow()
689    }}
690
691    /// Determine whether the user can view the model.
692    fn view(&self, _user: &Self::User, _model: &{model_name}) -> AuthResponse {{
693        // TODO: Implement authorization logic
694        AuthResponse::allow()
695    }}
696
697    /// Determine whether the user can create models.
698    fn create(&self, _user: &Self::User) -> AuthResponse {{
699        // TODO: Implement authorization logic
700        AuthResponse::allow()
701    }}
702
703    /// Determine whether the user can update the model.
704    fn update(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
705        // TODO: Implement authorization logic
706        // Example: Only owner can update
707        // if user.auth_identifier() == model.user_id as i64 {{
708        //     AuthResponse::allow()
709        // }} else {{
710        //     AuthResponse::deny("You do not own this resource.")
711        // }}
712        AuthResponse::deny_silent()
713    }}
714
715    /// Determine whether the user can delete the model.
716    fn delete(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
717        // Same as update by default
718        self.update(user, model)
719    }}
720
721    /// Determine whether the user can restore the model.
722    fn restore(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
723        self.update(user, model)
724    }}
725
726    /// Determine whether the user can permanently delete the model.
727    fn force_delete(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
728        // Usually more restrictive than delete
729        self.delete(user, model)
730    }}
731}}
732
733// TODO: Uncomment and define placeholder types until you import the real ones
734// struct {model_name};
735// struct User;
736// impl ferro::auth::Authenticatable for User {{
737//     fn auth_identifier(&self) -> i64 {{ 0 }}
738//     fn as_any(&self) -> &dyn std::any::Any {{ self }}
739// }}
740"#
741    )
742}
743
744// Lang templates
745
746/// Template for lang/{locale}/validation.json (English validation messages)
747pub fn lang_validation_json() -> &'static str {
748    include_str!("files/lang/validation.json.tpl")
749}
750
751/// Template for lang/{locale}/app.json (starter application translations)
752pub fn lang_app_json() -> &'static str {
753    include_str!("files/lang/app.json.tpl")
754}
755
756/// Template for policies/mod.rs
757pub fn policies_mod() -> &'static str {
758    r#"//! Authorization policies
759//!
760//! This module contains policies that define who can perform actions
761//! on specific models or resources.
762//!
763//! # Usage
764//!
765//! ```rust,ignore
766//! use crate::policies::PostPolicy;
767//! use ferro::authorization::Policy;
768//!
769//! let policy = PostPolicy;
770//!
771//! // Check authorization
772//! if policy.update(&user, &post).allowed() {
773//!     // Proceed with update
774//! }
775//!
776//! // Or use the generic check method
777//! let response = policy.check(&user, "update", Some(&post));
778//! ```
779
780"#
781}