Skip to main content

ferro_cli/templates/
module.rs

1//! Stub templates for `ferro make:module` — the feature-module convention
2//! (controller/model/views/routes). These are injected into user projects, so
3//! every snippet must compile as-is inside a fresh Ferro application.
4
5/// `src/modules/<name>/mod.rs` with the default (views included) layout.
6pub fn module_mod_rs(name: &str) -> String {
7    format!(
8        "//! {name} feature module\n\
9         \n\
10         pub mod controller;\n\
11         pub mod model;\n\
12         pub mod routes;\n\
13         pub mod views;\n"
14    )
15}
16
17/// `src/modules/<name>/mod.rs` for the `--no-views` (headless) variant.
18pub fn module_mod_rs_headless(name: &str) -> String {
19    format!(
20        "//! {name} feature module (headless)\n\
21         \n\
22         pub mod controller;\n\
23         pub mod model;\n\
24         pub mod routes;\n"
25    )
26}
27
28/// `src/modules/<name>/controller.rs` — minimal compiling handler stub.
29pub fn module_controller_rs(name: &str) -> String {
30    format!(
31        r#"//! {name} controller
32
33use ferro::{{handler, json_response, Request, Response}};
34
35#[handler]
36pub async fn index(_req: Request) -> Response {{
37    json_response!({{
38        "module": "{name}"
39    }})
40}}
41"#
42    )
43}
44
45/// `src/modules/<name>/model.rs` — empty SeaORM entity stub.
46pub fn module_model_rs(name: &str) -> String {
47    format!(
48        r#"//! {name} model
49
50use sea_orm::entity::prelude::*;
51
52#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
53#[sea_orm(table_name = "{name}")]
54pub struct Model {{
55    #[sea_orm(primary_key)]
56    pub id: i32,
57}}
58
59#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
60pub enum Relation {{}}
61
62impl ActiveModelBehavior for ActiveModel {{}}
63"#
64    )
65}
66
67/// `src/modules/<name>/views/mod.rs`.
68pub fn module_views_mod_rs() -> String {
69    "//! Views for this feature module\n\npub mod index;\n".to_string()
70}
71
72/// `src/modules/<name>/views/index.rs` — JsonUiView stub (mirrors
73/// `json_view_template` in make.rs, scoped to the module).
74pub fn module_view_index_rs(name: &str) -> String {
75    let title = titlecase(name);
76    format!(
77        r#"//! {title} index view
78
79use ferro::{{
80    Component, ComponentNode, CardProps, JsonUiView, TextElement, TextProps,
81}};
82
83/// Build the {title} index view.
84pub fn view() -> JsonUiView {{
85    JsonUiView::new()
86        .title("{title}")
87        .layout("app")
88        .component(ComponentNode {{
89            key: "heading".to_string(),
90            component: Component::Text(TextProps {{
91                content: "{title}".to_string(),
92                element: TextElement::H1,
93            }}),
94            action: None,
95            visibility: None,
96        }})
97        .component(ComponentNode {{
98            key: "card".to_string(),
99            component: Component::Card(CardProps {{
100                title: "{title}".to_string(),
101                description: Some(
102                    "Edit src/modules/{name}/views/index.rs to customize this view.".to_string(),
103                ),
104                children: vec![],
105                footer: vec![],
106            }}),
107            action: None,
108            visibility: None,
109        }})
110}}
111"#
112    )
113}
114
115/// `src/modules/<name>/routes.rs` — canonical `register(router)` hook.
116pub fn module_routes_rs(name: &str) -> String {
117    format!(
118        r#"//! {name} routes
119
120use ferro::Router;
121
122/// Register the {name} module routes onto the given router.
123pub fn register(router: Router) -> Router {{
124    router.get("/{name}", super::controller::index)
125}}
126"#
127    )
128}
129
130/// `migration/src/m_<ts>_create_<name>.rs` — optional SeaORM migration stub.
131pub fn module_migration_rs(name: &str, ts: &str) -> String {
132    let struct_name = format!("M{ts}CreateTable");
133    format!(
134        r#"//! create_{name} migration ({ts})
135
136use sea_orm_migration::prelude::*;
137
138#[derive(DeriveMigrationName)]
139pub struct {struct_name};
140
141#[async_trait::async_trait]
142impl MigrationTrait for {struct_name} {{
143    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
144        manager
145            .create_table(
146                Table::create()
147                    .table(Entity::Table)
148                    .if_not_exists()
149                    .col(
150                        ColumnDef::new(Column::Id)
151                            .integer()
152                            .not_null()
153                            .auto_increment()
154                            .primary_key(),
155                    )
156                    .col(ColumnDef::new(Column::CreatedAt).timestamp().not_null())
157                    .col(ColumnDef::new(Column::UpdatedAt).timestamp().not_null())
158                    .to_owned(),
159            )
160            .await
161    }}
162
163    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
164        manager
165            .drop_table(Table::drop().table(Entity::Table).to_owned())
166            .await
167    }}
168}}
169
170#[derive(DeriveIden)]
171enum Entity {{
172    #[sea_orm(iden = "{name}")]
173    Table,
174}}
175
176#[derive(DeriveIden)]
177enum Column {{
178    Id,
179    CreatedAt,
180    UpdatedAt,
181}}
182"#
183    )
184}
185
186fn titlecase(name: &str) -> String {
187    let mut out = String::with_capacity(name.len());
188    let mut upper_next = true;
189    for ch in name.chars() {
190        if ch == '_' || ch == '-' {
191            out.push(' ');
192            upper_next = true;
193        } else if upper_next {
194            out.extend(ch.to_uppercase());
195            upper_next = false;
196        } else {
197            out.push(ch);
198        }
199    }
200    out
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn mod_rs_declares_views() {
209        let out = module_mod_rs("orders");
210        assert!(out.contains("pub mod controller;"));
211        assert!(out.contains("pub mod model;"));
212        assert!(out.contains("pub mod routes;"));
213        assert!(out.contains("pub mod views;"));
214    }
215
216    #[test]
217    fn mod_rs_headless_omits_views() {
218        let out = module_mod_rs_headless("orders");
219        assert!(out.contains("pub mod controller;"));
220        assert!(!out.contains("pub mod views;"));
221    }
222
223    #[test]
224    fn routes_rs_exposes_register_fn() {
225        let out = module_routes_rs("orders");
226        assert!(out.contains("pub fn register(router: Router) -> Router"));
227        assert!(out.contains("\"/orders\""));
228    }
229
230    #[test]
231    fn controller_rs_uses_handler_macro() {
232        let out = module_controller_rs("orders");
233        assert!(out.contains("#[handler]"));
234        assert!(out.contains("\"orders\""));
235    }
236
237    #[test]
238    fn migration_rs_implements_migration_trait() {
239        let out = module_migration_rs("orders", "20260407120000");
240        assert!(out.contains("MigrationTrait"));
241        assert!(out.contains("orders"));
242    }
243}