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 {
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
133pub 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
145pub fn error_template(struct_name: &str) -> String {
147 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
169pub 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
225pub 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
269pub 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
279pub 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
321pub 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
330pub 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
395pub 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
405pub 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
460pub 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
470pub 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
518pub 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
547pub 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
615pub 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
640pub 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
747pub fn lang_validation_json() -> &'static str {
751 include_str!("files/lang/validation.json.tpl")
752}
753
754pub fn lang_app_json() -> &'static str {
756 include_str!("files/lang/app.json.tpl")
757}
758
759pub 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}