Skip to main content

doido_generators/generators/
controller.rs

1use crate::generator::{GeneratedFile, Generator};
2use crate::generators::helper::{
3    helper_names, helpers_mod_path, read_helpers_mod, register_helper_in_mod, render_helper_content,
4};
5use crate::generators::{to_pascal, to_snake};
6use doido_core::Result;
7
8/// Fallback `app/controllers/mod.rs` when the app doesn't have one on disk yet.
9const CONTROLLERS_MOD_BASE: &str = include_str!("../../templates/new/app/controllers/mod.rs");
10pub(crate) const CONTROLLERS_MOD_PATH: &str = "app/controllers/mod.rs";
11
12/// Fallback `config/routes.rs` when the app doesn't have one on disk yet.
13const ROUTES_BASE: &str = include_str!("../../templates/new/config/routes.rs");
14const ROUTES_PATH: &str = "config/routes.rs";
15
16pub struct ControllerGenerator;
17
18pub(crate) fn read_routes() -> String {
19    std::fs::read_to_string(ROUTES_PATH).unwrap_or_else(|_| ROUTES_BASE.to_string())
20}
21
22/// Injects `use crate::controllers::<Controller>;` and a `get!(…)` route into
23/// `config/routes.rs`, preserving existing routes. Idempotent on the route line.
24pub(crate) fn inject_controller_route(routes: &str, snake: &str, controller: &str) -> String {
25    let route = format!("get!(\"/{snake}\", {controller}::index);");
26    if routes.contains(&route) {
27        return routes.to_string();
28    }
29
30    let use_line = format!("use crate::controllers::{controller};");
31    let mut lines: Vec<String> = routes.lines().map(String::from).collect();
32
33    if !routes.contains(&use_line) {
34        let pos = lines
35            .iter()
36            .rposition(|l| l.contains("use crate::controllers"))
37            .map(|i| i + 1)
38            .unwrap_or(0);
39        lines.insert(pos, use_line);
40    }
41
42    if let Some(open) = lines.iter().position(|l| l.contains("routes!")) {
43        if let Some(close_rel) = lines[open..].iter().position(|l| l.trim() == "}") {
44            let close = open + close_rel;
45            lines.insert(close, format!("        {route}"));
46        }
47    }
48
49    let mut out = lines.join("\n");
50    out.push('\n');
51    out
52}
53
54pub(crate) fn read_controllers_mod() -> String {
55    std::fs::read_to_string(CONTROLLERS_MOD_PATH)
56        .unwrap_or_else(|_| CONTROLLERS_MOD_BASE.to_string())
57}
58
59/// Appends `mod <stem>_controller;` + `pub use …` to `app/controllers/mod.rs`.
60/// Idempotent: skips when the module is already declared.
61pub(crate) fn register_controller_in_mod(
62    controllers_mod: &str,
63    module_stem: &str,
64    controller: &str,
65) -> String {
66    let module = format!("{module_stem}_controller");
67    let decl = format!("mod {module};");
68    if controllers_mod.lines().any(|l| l.trim() == decl) {
69        return controllers_mod.to_string();
70    }
71    let mut out = controllers_mod.trim_end().to_string();
72    out.push('\n');
73    out.push_str(&format!("mod {module};\n"));
74    out.push_str(&format!("pub use {module}::{controller};\n"));
75    out
76}
77
78impl Generator for ControllerGenerator {
79    fn name(&self) -> &str {
80        "controller"
81    }
82
83    fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
84        let name = args.first().copied().ok_or_else(|| {
85            doido_core::anyhow::anyhow!("controller generator requires a name argument")
86        })?;
87        let snake = to_snake(name);
88        let pascal = to_pascal(name);
89        let (_, helper_type) = helper_names(name);
90        let content = crate::templates::get("controller/controller.rs.template")
91            .replace("{pascal}", &pascal)
92            .replace("{snake}", &snake)
93            .replace("{Helper}", &helper_type);
94        let test = crate::templates::get("controller/controller_test.rs.template")
95            .replace("{pascal}", &pascal)
96            .replace("{snake}", &snake);
97        let (helper_snake, _) = helper_names(name);
98        let helpers_mod = register_helper_in_mod(&read_helpers_mod(), name);
99        let controller_type = format!("{pascal}Controller");
100        let controllers_mod =
101            register_controller_in_mod(&read_controllers_mod(), &snake, &controller_type);
102        let routes = inject_controller_route(&read_routes(), &snake, &controller_type);
103        Ok(vec![
104            GeneratedFile {
105                path: format!("app/helpers/{helper_snake}.rs"),
106                content: render_helper_content(name, &snake),
107            },
108            GeneratedFile {
109                path: helpers_mod_path().to_string(),
110                content: helpers_mod,
111            },
112            GeneratedFile {
113                path: format!("app/controllers/{snake}_controller.rs"),
114                content,
115            },
116            GeneratedFile {
117                path: CONTROLLERS_MOD_PATH.to_string(),
118                content: controllers_mod,
119            },
120            GeneratedFile {
121                path: ROUTES_PATH.to_string(),
122                content: routes,
123            },
124            GeneratedFile {
125                path: format!("tests/{snake}_controller_test.rs"),
126                content: test,
127            },
128        ])
129    }
130}