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 v2 view file (--no-ai fallback).
103///
104/// Returns a standalone JSON spec string — not Rust source. The output is intended
105/// to be written directly to `src/views/{name}.json` and served by a handler that
106/// calls `JsonUi::render_file("views/{name}.json", data)`.
107pub fn json_view_template(name: &str, title: &str, layout: &str) -> String {
108    format!(
109        r#"{{
110  "$schema": "ferro-json-ui/v2",
111  "title": "{title}",
112  "layout": "{layout}",
113  "root": "root",
114  "elements": {{
115    "root": {{
116      "type": "Card",
117      "props": {{
118        "title": "{title}",
119        "description": "Edit src/views/{name}.json to customize this view."
120      }},
121      "children": ["heading"]
122    }},
123    "heading": {{
124      "type": "Text",
125      "props": {{ "content": "{title}", "element": "h1" }}
126    }}
127  }}
128}}
129"#,
130    )
131}
132
133/// Handler template paired with a JSON-UI v2 spec file.
134pub fn json_view_handler_template(name: &str) -> String {
135    format!(
136        r#"#[handler]
137pub async fn {name}(req: Request) -> Response {{
138    let data = serde_json::json!({{}});
139    JsonUi::render_file("views/{name}.json", data)
140}}
141"#,
142    )
143}
144
145/// Template for generating new error with make:error command
146pub fn error_template(struct_name: &str) -> String {
147    // Convert PascalCase to human readable message
148    let mut message = String::new();
149    for (i, c) in struct_name.chars().enumerate() {
150        if c.is_uppercase() && i > 0 {
151            message.push(' ');
152            message.push(c.to_lowercase().next().unwrap());
153        } else {
154            message.push(c);
155        }
156    }
157
158    format!(
159        r#"//! {struct_name} error
160
161use ferro::domain_error;
162
163#[domain_error(status = 500, message = "{message}")]
164pub struct {struct_name};
165"#
166    )
167}
168
169/// Template for generating new scheduled task with make:task command
170pub fn task_template(file_name: &str, struct_name: &str) -> String {
171    format!(
172        r#"//! {struct_name} scheduled task
173//!
174//! Created with `ferro make:task {file_name}`
175
176use async_trait::async_trait;
177use ferro::{{Task, TaskResult}};
178
179/// {struct_name} - A scheduled task
180///
181/// Implement your task logic in the `handle()` method.
182/// Register this task in `src/schedule.rs` with the fluent API.
183///
184/// # Example Registration
185///
186/// ```rust,ignore
187/// // In src/schedule.rs
188/// use crate::tasks::{file_name};
189///
190/// schedule.add(
191///     schedule.task({struct_name}::new())
192///         .daily()
193///         .at("03:00")
194///         .name("{file_name}")
195///         .description("TODO: Add task description")
196/// );
197/// ```
198pub struct {struct_name};
199
200impl {struct_name} {{
201    /// Create a new instance of this task
202    pub fn new() -> Self {{
203        Self
204    }}
205}}
206
207impl Default for {struct_name} {{
208    fn default() -> Self {{
209        Self::new()
210    }}
211}}
212
213#[async_trait]
214impl Task for {struct_name} {{
215    async fn handle(&self) -> TaskResult {{
216        // TODO: Implement your task logic here
217        println!("Running {struct_name}...");
218        Ok(())
219    }}
220}}
221"#
222    )
223}
224
225// Event templates
226
227/// Template for generating new events with make:event command
228pub fn event_template(file_name: &str, struct_name: &str) -> String {
229    format!(
230        r#"//! {struct_name} event
231//!
232//! Created with `ferro make:event {file_name}`
233
234use ferro_events::Event;
235use serde::{{Deserialize, Serialize}};
236
237/// {struct_name} - A domain event
238///
239/// Events represent something that has happened in your application.
240/// Listeners can react to these events asynchronously.
241///
242/// # Dispatching
243///
244/// ```rust,ignore
245/// use crate::events::{file_name}::{struct_name};
246///
247/// // Ergonomic dispatch (awaits all listeners)
248/// {struct_name} {{ /* fields */ }}.dispatch().await?;
249///
250/// // Fire and forget (spawns background task)
251/// {struct_name} {{ /* fields */ }}.dispatch_sync();
252/// ```
253#[derive(Debug, Clone, Serialize, Deserialize)]
254pub struct {struct_name} {{
255    // TODO: Add event data fields
256    // pub user_id: i64,
257    // pub created_at: chrono::DateTime<chrono::Utc>,
258}}
259
260impl Event for {struct_name} {{
261    fn name(&self) -> &'static str {{
262        "{struct_name}"
263    }}
264}}
265"#
266    )
267}
268
269/// Template for events/mod.rs
270pub fn events_mod() -> &'static str {
271    r#"//! Application events
272//!
273//! This module contains domain events that can be dispatched
274//! and handled by listeners.
275
276"#
277}
278
279// Listener templates
280
281/// Template for generating new listeners with make:listener command
282pub fn listener_template(file_name: &str, struct_name: &str, event_type: &str) -> String {
283    format!(
284        r#"//! {struct_name} listener
285//!
286//! Created with `ferro make:listener {file_name}`
287
288use ferro_events::{{async_trait, Error, Listener}};
289// TODO: Import the event type
290// use crate::events::your_event::YourEvent;
291
292/// {struct_name} - An event listener
293///
294/// Listeners react to events and perform side effects.
295/// They can be synchronous or queued for background processing.
296///
297/// # Example Registration
298///
299/// ```rust,ignore
300/// // In your app initialization
301/// use ferro_events::EventDispatcher;
302/// use crate::listeners::{file_name}::{struct_name};
303///
304/// let mut dispatcher = EventDispatcher::new();
305/// dispatcher.listen::<{event_type}, _>({struct_name});
306/// ```
307pub struct {struct_name};
308
309#[async_trait]
310impl Listener<{event_type}> for {struct_name} {{
311    async fn handle(&self, event: &{event_type}) -> Result<(), Error> {{
312        // TODO: Implement listener logic
313        tracing::info!("{struct_name} handling event: {{:?}}", event);
314        Ok(())
315    }}
316}}
317"#
318    )
319}
320
321/// Template for listeners/mod.rs
322pub fn listeners_mod() -> &'static str {
323    r#"//! Application event listeners
324//!
325//! This module contains listeners that react to domain events.
326
327"#
328}
329
330// Job templates
331
332/// Template for generating new jobs with make:job command
333pub fn job_template(file_name: &str, struct_name: &str) -> String {
334    format!(
335        r#"//! {struct_name} background job
336//!
337//! Created with `ferro make:job {file_name}`
338
339use ferro_queue::{{async_trait, Error, Job, Queueable}};
340use serde::{{Deserialize, Serialize}};
341
342/// {struct_name} - A background job
343///
344/// Jobs are queued for background processing by workers.
345/// They support retries, delays, and queue prioritization.
346///
347/// # Example
348///
349/// ```rust,ignore
350/// use crate::jobs::{file_name}::{struct_name};
351///
352/// // Dispatch immediately
353/// {struct_name} {{ /* fields */ }}.dispatch().await?;
354///
355/// // Dispatch with delay
356/// {struct_name} {{ /* fields */ }}
357///     .delay(std::time::Duration::from_secs(60))
358///     .dispatch()
359///     .await?;
360///
361/// // Dispatch to specific queue
362/// {struct_name} {{ /* fields */ }}
363///     .on_queue("high-priority")
364///     .dispatch()
365///     .await?;
366/// ```
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct {struct_name} {{
369    // TODO: Add job data fields
370    // pub user_id: i64,
371    // pub payload: String,
372}}
373
374#[async_trait]
375impl Job for {struct_name} {{
376    async fn handle(&self) -> Result<(), Error> {{
377        // TODO: Implement job logic
378        tracing::info!("Processing {struct_name}: {{:?}}", self);
379        Ok(())
380    }}
381
382    fn max_retries(&self) -> u32 {{
383        3
384    }}
385
386    fn retry_delay(&self, attempt: u32) -> std::time::Duration {{
387        // Exponential backoff
388        std::time::Duration::from_secs(2u64.pow(attempt))
389    }}
390}}
391"#
392    )
393}
394
395/// Template for jobs/mod.rs
396pub fn jobs_mod() -> &'static str {
397    r#"//! Application background jobs
398//!
399//! This module contains jobs that are processed asynchronously
400//! by queue workers.
401
402"#
403}
404
405// Notification templates
406
407/// Template for generating new notifications with make:notification command
408pub fn notification_template(file_name: &str, struct_name: &str) -> String {
409    format!(
410        r#"//! {struct_name} notification
411//!
412//! Created with `ferro make:notification {file_name}`
413
414use ferro_notifications::{{Channel, DatabaseMessage, MailMessage, Notification}};
415
416/// {struct_name} - A multi-channel notification
417///
418/// Notifications can be sent through multiple channels:
419/// - Mail: Email via SMTP
420/// - Database: In-app notifications
421/// - Slack: Webhook messages
422///
423/// # Example
424///
425/// ```rust,ignore
426/// use crate::notifications::{file_name}::{struct_name};
427///
428/// // Send notification to a user
429/// user.notify({struct_name} {{ /* fields */ }}).await?;
430/// ```
431pub struct {struct_name} {{
432    // TODO: Add notification data fields
433    // pub order_id: i64,
434    // pub tracking_number: String,
435}}
436
437impl Notification for {struct_name} {{
438    fn via(&self) -> Vec<Channel> {{
439        // TODO: Choose notification channels
440        vec![Channel::Mail, Channel::Database]
441    }}
442
443    fn to_mail(&self) -> Option<MailMessage> {{
444        Some(MailMessage::new()
445            .subject("{struct_name}")
446            .body("TODO: Add notification message"))
447    }}
448
449    fn to_database(&self) -> Option<DatabaseMessage> {{
450        Some(DatabaseMessage::new("{file_name}")
451            // TODO: Add notification data
452            // .data("order_id", self.order_id)
453        )
454    }}
455}}
456"#
457    )
458}
459
460/// Template for notifications/mod.rs
461pub fn notifications_mod() -> &'static str {
462    r#"//! Application notifications
463//!
464//! This module contains notifications that can be sent
465//! through multiple channels (mail, database, slack, etc.).
466
467"#
468}
469
470// Seeder templates
471
472/// Template for generating new seeder with make:seeder command
473pub fn seeder_template(file_name: &str, struct_name: &str) -> String {
474    format!(
475        r#"//! {struct_name} database seeder
476//!
477//! Created with `ferro make:seeder {file_name}`
478
479use ferro::{{async_trait, FrameworkError, Seeder}};
480use sea_orm::DatabaseConnection;
481
482/// {struct_name} - A database seeder
483///
484/// Seeders populate the database with test or initial data.
485/// Implement the `run` method to insert records.
486///
487/// # Example Registration
488///
489/// ```rust,ignore
490/// // In src/seeders/mod.rs
491/// use ferro::SeederRegistry;
492/// use super::{file_name}::{struct_name};
493///
494/// pub fn register() -> SeederRegistry {{
495///     SeederRegistry::new()
496///         .add::<{struct_name}>()
497/// }}
498/// ```
499#[derive(Default)]
500pub struct {struct_name};
501
502#[async_trait]
503impl Seeder for {struct_name} {{
504    async fn run(&self, db: &DatabaseConnection) -> Result<(), FrameworkError> {{
505        // TODO: Implement seeder logic using `db`
506        // Example:
507        // use sea_orm::{{ActiveModelTrait, ActiveValue::Set}};
508        // users::ActiveModel {{ name: Set("Admin".into()), ..Default::default() }}
509        //     .insert(db).await?;
510
511        Ok(())
512    }}
513}}
514"#
515    )
516}
517
518/// Template for seeders/mod.rs
519pub fn seeders_mod() -> &'static str {
520    r#"//! Database seeders
521//!
522//! This module contains seeders that populate the database with test
523//! or initial data.
524//!
525//! # Usage
526//!
527//! Register seeders in the `register()` function and run with:
528//! ```bash
529//! ./target/debug/app db:seed           # Run all seeders
530//! ./target/debug/app db:seed --class UsersSeeder  # Run specific seeder
531//! ```
532
533use ferro::SeederRegistry;
534
535/// Register all seeders
536///
537/// Add your seeders here in the order you want them to run.
538/// Seeders are executed in registration order.
539pub fn register() -> SeederRegistry {
540    SeederRegistry::new()
541        // .add::<UsersSeeder>()
542        // .add::<ProductsSeeder>()
543}
544"#
545}
546
547/// Template for generating new factory with make:factory command
548pub fn factory_template(file_name: &str, struct_name: &str, model_name: &str) -> String {
549    format!(
550        r#"//! {struct_name} factory
551//!
552//! Created with `ferro make:factory {file_name}`
553
554use ferro::testing::{{Factory, FactoryTraits, Fake}};
555// use ferro::testing::DatabaseFactory;
556// use crate::models::{model_name};
557
558/// Factory for creating {model_name} instances in tests
559#[derive(Clone)]
560pub struct {struct_name} {{
561    // Add fields matching your model
562    pub id: i64,
563    pub name: String,
564    pub email: String,
565    pub created_at: String,
566}}
567
568impl Factory for {struct_name} {{
569    fn definition() -> Self {{
570        Self {{
571            id: 0, // Will be set by database
572            name: Fake::name(),
573            email: Fake::email(),
574            created_at: Fake::datetime(),
575        }}
576    }}
577
578    fn traits() -> FactoryTraits<Self> {{
579        FactoryTraits::new()
580            // .define("admin", |m: &mut Self| m.role = "admin".to_string())
581            // .define("verified", |m: &mut Self| m.verified = true)
582    }}
583}}
584
585// Uncomment to enable database persistence with create():
586//
587// #[ferro::async_trait]
588// impl DatabaseFactory for {struct_name} {{
589//     type Entity = {model_name}::Entity;
590//     type ActiveModel = {model_name}::ActiveModel;
591// }}
592
593// Usage in tests:
594//
595// // Make without persisting:
596// let model = {struct_name}::factory().make();
597//
598// // Apply named trait:
599// let admin = {struct_name}::factory().trait_("admin").make();
600//
601// // With inline state:
602// let model = {struct_name}::factory()
603//     .state(|m| m.name = "Custom".into())
604//     .make();
605//
606// // Create with database persistence:
607// let model = {struct_name}::factory().create().await?;
608//
609// // Create multiple:
610// let models = {struct_name}::factory().count(5).create_many().await?;
611"#
612    )
613}
614
615/// Template for factories/mod.rs
616pub fn factories_mod() -> &'static str {
617    r#"//! Test factories
618//!
619//! This module contains factories for generating fake model data in tests.
620//!
621//! # Usage
622//!
623//! ```rust,ignore
624//! use crate::factories::UserFactory;
625//! use ferro::testing::Factory;
626//!
627//! // Make without persisting
628//! let user = UserFactory::factory().make();
629//!
630//! // Create with database persistence
631//! let user = UserFactory::factory().create().await?;
632//!
633//! // Create multiple
634//! let users = UserFactory::factory().count(5).create_many().await?;
635//! ```
636
637"#
638}
639
640/// Template for generating new policy with make:policy command
641pub fn policy_template(file_name: &str, struct_name: &str, model_name: &str) -> String {
642    format!(
643        r#"//! {struct_name} authorization policy
644//!
645//! Created with `ferro make:policy {file_name}`
646
647use ferro::authorization::{{AuthResponse, Policy}};
648// TODO: Import your model and user types
649// use crate::models::{model_name}::{{self, Model as {model_name}}};
650// use crate::models::users::Model as User;
651
652/// {struct_name} - Authorization policy for {model_name}
653///
654/// This policy defines who can perform actions on {model_name} records.
655///
656/// # Example Usage
657///
658/// ```rust,ignore
659/// use crate::policies::{file_name}::{struct_name};
660///
661/// let policy = {struct_name};
662///
663/// // Check if user can update the model
664/// if policy.update(&user, &model).allowed() {{
665///     // Proceed with update
666/// }}
667///
668/// // Use the check method for string-based ability lookup
669/// let response = policy.check(&user, "update", Some(&model));
670/// ```
671pub struct {struct_name};
672
673impl Policy<{model_name}> for {struct_name} {{
674    type User = User;
675
676    /// Run before any other authorization checks.
677    ///
678    /// Return `Some(true)` to allow, `Some(false)` to deny,
679    /// or `None` to continue to the specific ability check.
680    fn before(&self, user: &Self::User, _ability: &str) -> Option<bool> {{
681        // Example: Admin bypass
682        // if user.is_admin {{
683        //     return Some(true);
684        // }}
685        None
686    }}
687
688    /// Determine whether the user can view any models.
689    fn view_any(&self, _user: &Self::User) -> AuthResponse {{
690        // TODO: Implement authorization logic
691        AuthResponse::allow()
692    }}
693
694    /// Determine whether the user can view the model.
695    fn view(&self, _user: &Self::User, _model: &{model_name}) -> AuthResponse {{
696        // TODO: Implement authorization logic
697        AuthResponse::allow()
698    }}
699
700    /// Determine whether the user can create models.
701    fn create(&self, _user: &Self::User) -> AuthResponse {{
702        // TODO: Implement authorization logic
703        AuthResponse::allow()
704    }}
705
706    /// Determine whether the user can update the model.
707    fn update(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
708        // TODO: Implement authorization logic
709        // Example: Only owner can update
710        // if user.auth_identifier() == model.user_id as i64 {{
711        //     AuthResponse::allow()
712        // }} else {{
713        //     AuthResponse::deny("You do not own this resource.")
714        // }}
715        AuthResponse::deny_silent()
716    }}
717
718    /// Determine whether the user can delete the model.
719    fn delete(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
720        // Same as update by default
721        self.update(user, model)
722    }}
723
724    /// Determine whether the user can restore the model.
725    fn restore(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
726        self.update(user, model)
727    }}
728
729    /// Determine whether the user can permanently delete the model.
730    fn force_delete(&self, user: &Self::User, model: &{model_name}) -> AuthResponse {{
731        // Usually more restrictive than delete
732        self.delete(user, model)
733    }}
734}}
735
736// TODO: Uncomment and define placeholder types until you import the real ones
737// struct {model_name};
738// struct User;
739// impl ferro::auth::Authenticatable for User {{
740//     fn auth_identifier(&self) -> i64 {{ 0 }}
741//     fn as_any(&self) -> &dyn std::any::Any {{ self }}
742// }}
743"#
744    )
745}
746
747// Lang templates
748
749/// Template for lang/{locale}/validation.json (English validation messages)
750pub fn lang_validation_json() -> &'static str {
751    include_str!("files/lang/validation.json.tpl")
752}
753
754/// Template for lang/{locale}/app.json (starter application translations)
755pub fn lang_app_json() -> &'static str {
756    include_str!("files/lang/app.json.tpl")
757}
758
759/// Template for policies/mod.rs
760pub fn policies_mod() -> &'static str {
761    r#"//! Authorization policies
762//!
763//! This module contains policies that define who can perform actions
764//! on specific models or resources.
765//!
766//! # Usage
767//!
768//! ```rust,ignore
769//! use crate::policies::PostPolicy;
770//! use ferro::authorization::Policy;
771//!
772//! let policy = PostPolicy;
773//!
774//! // Check authorization
775//! if policy.update(&user, &post).allowed() {
776//!     // Proceed with update
777//! }
778//!
779//! // Or use the generic check method
780//! let response = policy.check(&user, "update", Some(&post));
781//! ```
782
783"#
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    #[test]
791    fn json_view_template_is_valid_v2_json() {
792        let out = json_view_template("dashboard", "Dashboard", "dashboard");
793        let v: serde_json::Value = serde_json::from_str(&out).expect("template must be valid JSON");
794        assert_eq!(v["$schema"], "ferro-json-ui/v2");
795        assert_eq!(v["title"], "Dashboard");
796        assert_eq!(v["layout"], "dashboard");
797        assert_eq!(v["root"], "root");
798        assert!(v["elements"]["root"]["type"].as_str() == Some("Card"));
799        assert!(v["elements"]["heading"]["type"].as_str() == Some("Text"));
800    }
801
802    #[test]
803    fn json_view_template_references_name_in_description() {
804        let out = json_view_template("my_page", "My Page", "dashboard");
805        assert!(out.contains("src/views/my_page.json"));
806        assert!(!out.contains("my_page.rs"));
807    }
808
809    #[test]
810    fn json_view_template_has_no_v1_markers() {
811        let out = json_view_template("x", "Y", "dashboard");
812        for marker in ["Spec::builder", "Element::new", "JsonUiView", "use ferro::"] {
813            assert!(
814                !out.contains(marker),
815                "template must not contain v1 marker '{marker}'"
816            );
817        }
818    }
819
820    #[test]
821    fn json_view_template_parses_as_spec() {
822        let out = json_view_template("dashboard", "Dashboard", "dashboard");
823        let spec = ferro_json_ui::Spec::from_json(&out);
824        assert!(
825            spec.is_ok(),
826            "template must parse as a valid Spec: {spec:?}"
827        );
828    }
829}