ferro_cli/templates/
module.rs1pub 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
17pub 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
28pub 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
45pub 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
67pub fn module_views_mod_rs() -> String {
69 "//! Views for this feature module\n\npub mod index;\n".to_string()
70}
71
72pub 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
115pub 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
130pub 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}