pub fn module_mod_rs(name: &str) -> String {
format!(
"//! {name} feature module\n\
\n\
pub mod controller;\n\
pub mod model;\n\
pub mod routes;\n\
pub mod views;\n"
)
}
pub fn module_mod_rs_headless(name: &str) -> String {
format!(
"//! {name} feature module (headless)\n\
\n\
pub mod controller;\n\
pub mod model;\n\
pub mod routes;\n"
)
}
pub fn module_controller_rs(name: &str) -> String {
format!(
r#"//! {name} controller
use ferro::{{handler, json_response, Request, Response}};
#[handler]
pub async fn index(_req: Request) -> Response {{
json_response!({{
"module": "{name}"
}})
}}
"#
)
}
pub fn module_model_rs(name: &str) -> String {
format!(
r#"//! {name} model
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "{name}")]
pub struct Model {{
#[sea_orm(primary_key)]
pub id: i32,
}}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {{}}
impl ActiveModelBehavior for ActiveModel {{}}
"#
)
}
pub fn module_views_mod_rs() -> String {
"//! Views for this feature module\n\npub mod index;\n".to_string()
}
pub fn module_view_index_rs(name: &str) -> String {
let title = titlecase(name);
format!(
r#"//! {title} index view
use ferro::{{
Component, ComponentNode, CardProps, JsonUiView, TextElement, TextProps,
}};
/// Build the {title} index view.
pub fn view() -> JsonUiView {{
JsonUiView::new()
.title("{title}")
.layout("app")
.component(ComponentNode {{
key: "heading".to_string(),
component: Component::Text(TextProps {{
content: "{title}".to_string(),
element: TextElement::H1,
}}),
action: None,
visibility: None,
}})
.component(ComponentNode {{
key: "card".to_string(),
component: Component::Card(CardProps {{
title: "{title}".to_string(),
description: Some(
"Edit src/modules/{name}/views/index.rs to customize this view.".to_string(),
),
children: vec![],
footer: vec![],
}}),
action: None,
visibility: None,
}})
}}
"#
)
}
pub fn module_routes_rs(name: &str) -> String {
format!(
r#"//! {name} routes
use ferro::Router;
/// Register the {name} module routes onto the given router.
pub fn register(router: Router) -> Router {{
router.get("/{name}", super::controller::index)
}}
"#
)
}
pub fn module_migration_rs(name: &str, ts: &str) -> String {
let struct_name = format!("M{ts}CreateTable");
format!(
r#"//! create_{name} migration ({ts})
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct {struct_name};
#[async_trait::async_trait]
impl MigrationTrait for {struct_name} {{
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
manager
.create_table(
Table::create()
.table(Entity::Table)
.if_not_exists()
.col(
ColumnDef::new(Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Column::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(Column::UpdatedAt).timestamp().not_null())
.to_owned(),
)
.await
}}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {{
manager
.drop_table(Table::drop().table(Entity::Table).to_owned())
.await
}}
}}
#[derive(DeriveIden)]
enum Entity {{
#[sea_orm(iden = "{name}")]
Table,
}}
#[derive(DeriveIden)]
enum Column {{
Id,
CreatedAt,
UpdatedAt,
}}
"#
)
}
fn titlecase(name: &str) -> String {
let mut out = String::with_capacity(name.len());
let mut upper_next = true;
for ch in name.chars() {
if ch == '_' || ch == '-' {
out.push(' ');
upper_next = true;
} else if upper_next {
out.extend(ch.to_uppercase());
upper_next = false;
} else {
out.push(ch);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mod_rs_declares_views() {
let out = module_mod_rs("orders");
assert!(out.contains("pub mod controller;"));
assert!(out.contains("pub mod model;"));
assert!(out.contains("pub mod routes;"));
assert!(out.contains("pub mod views;"));
}
#[test]
fn mod_rs_headless_omits_views() {
let out = module_mod_rs_headless("orders");
assert!(out.contains("pub mod controller;"));
assert!(!out.contains("pub mod views;"));
}
#[test]
fn routes_rs_exposes_register_fn() {
let out = module_routes_rs("orders");
assert!(out.contains("pub fn register(router: Router) -> Router"));
assert!(out.contains("\"/orders\""));
}
#[test]
fn controller_rs_uses_handler_macro() {
let out = module_controller_rs("orders");
assert!(out.contains("#[handler]"));
assert!(out.contains("\"orders\""));
}
#[test]
fn migration_rs_implements_migration_trait() {
let out = module_migration_rs("orders", "20260407120000");
assert!(out.contains("MigrationTrait"));
assert!(out.contains("orders"));
}
}