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::{{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
111pub 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
126pub 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}