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` — Spec 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::{{Spec, Element, JsonUi, Response}};
80
81/// Build the {title} index view.
82pub async fn view() -> Response {{
83    let spec = Spec::builder()
84        .title("{title}")
85        .layout("app")
86        .element(
87            "root",
88            Element::new("Card")
89                .prop("title", "{title}")
90                .prop(
91                    "description",
92                    "Edit src/modules/{name}/views/index.rs to customize this view.",
93                )
94                .child("heading"),
95        )
96        .element(
97            "heading",
98            Element::new("Text")
99                .prop("content", "{title}")
100                .prop("element", "h1"),
101        )
102        .build()
103        .expect("spec is valid");
104
105    JsonUi::render(&spec, &serde_json::json!({{}}))
106}}
107"#
108    )
109}
110
111/// `src/modules/<name>/routes.rs` — canonical `register(router)` hook.
112pub fn module_routes_rs(name: &str) -> String {
113    format!(
114        r#"//! {name} routes
115
116use ferro::Router;
117
118/// Register the {name} module routes onto the given router.
119pub fn register(router: Router) -> Router {{
120    router.get("/{name}", super::controller::index)
121}}
122"#
123    )
124}
125
126/// `migration/src/m_<ts>_create_<name>.rs` — optional SeaORM migration stub.
127pub fn module_migration_rs(name: &str, ts: &str) -> String {
128    let struct_name = format!("M{ts}CreateTable");
129    format!(
130        r#"//! create_{name} migration ({ts})
131
132use sea_orm_migration::prelude::*;
133
134#[derive(DeriveMigrationName)]
135pub struct {struct_name};
136
137#[async_trait::async_trait]
138impl MigrationTrait for {struct_name} {{
139    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
140        manager
141            .create_table(
142                Table::create()
143                    .table(Entity::Table)
144                    .if_not_exists()
145                    .col(
146                        ColumnDef::new(Column::Id)
147                            .integer()
148                            .not_null()
149                            .auto_increment()
150                            .primary_key(),
151                    )
152                    .col(ColumnDef::new(Column::CreatedAt).timestamp().not_null())
153                    .col(ColumnDef::new(Column::UpdatedAt).timestamp().not_null())
154                    .to_owned(),
155            )
156            .await
157    }}
158
159    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
160        manager
161            .drop_table(Table::drop().table(Entity::Table).to_owned())
162            .await
163    }}
164}}
165
166#[derive(DeriveIden)]
167enum Entity {{
168    #[sea_orm(iden = "{name}")]
169    Table,
170}}
171
172#[derive(DeriveIden)]
173enum Column {{
174    Id,
175    CreatedAt,
176    UpdatedAt,
177}}
178"#
179    )
180}
181
182fn titlecase(name: &str) -> String {
183    let mut out = String::with_capacity(name.len());
184    let mut upper_next = true;
185    for ch in name.chars() {
186        if ch == '_' || ch == '-' {
187            out.push(' ');
188            upper_next = true;
189        } else if upper_next {
190            out.extend(ch.to_uppercase());
191            upper_next = false;
192        } else {
193            out.push(ch);
194        }
195    }
196    out
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[test]
204    fn mod_rs_declares_views() {
205        let out = module_mod_rs("orders");
206        assert!(out.contains("pub mod controller;"));
207        assert!(out.contains("pub mod model;"));
208        assert!(out.contains("pub mod routes;"));
209        assert!(out.contains("pub mod views;"));
210    }
211
212    #[test]
213    fn mod_rs_headless_omits_views() {
214        let out = module_mod_rs_headless("orders");
215        assert!(out.contains("pub mod controller;"));
216        assert!(!out.contains("pub mod views;"));
217    }
218
219    #[test]
220    fn routes_rs_exposes_register_fn() {
221        let out = module_routes_rs("orders");
222        assert!(out.contains("pub fn register(router: Router) -> Router"));
223        assert!(out.contains("\"/orders\""));
224    }
225
226    #[test]
227    fn controller_rs_uses_handler_macro() {
228        let out = module_controller_rs("orders");
229        assert!(out.contains("#[handler]"));
230        assert!(out.contains("\"orders\""));
231    }
232
233    #[test]
234    fn migration_rs_implements_migration_trait() {
235        let out = module_migration_rs("orders", "20260407120000");
236        assert!(out.contains("MigrationTrait"));
237        assert!(out.contains("orders"));
238    }
239}