Skip to main content

doido_generators/generators/
helper.rs

1use crate::generator::{GeneratedFile, Generator};
2use crate::generators::{to_pascal, to_snake};
3use doido_core::Result;
4
5/// Fallback `app/helpers/mod.rs` when the app doesn't have one on disk yet.
6const HELPERS_MOD_BASE: &str = include_str!("../../templates/new/app/helpers/mod.rs");
7const HELPERS_MOD_PATH: &str = "app/helpers/mod.rs";
8
9pub struct HelperGenerator;
10
11/// `Posts` → `PostsHelper`; `PostsHelper` stays as-is.
12fn helper_type_name(name: &str) -> String {
13    let pascal = to_pascal(name);
14    if pascal.ends_with("Helper") {
15        pascal
16    } else {
17        format!("{pascal}Helper")
18    }
19}
20
21/// Module/file stem: `posts_helper`, not `posts_helper_helper`.
22fn helper_module_snake(name: &str) -> String {
23    let snake = to_snake(name);
24    if snake.ends_with("_helper") {
25        snake
26    } else {
27        format!("{snake}_helper")
28    }
29}
30
31/// `(module_snake, type_name)` — e.g. `("posts", "Posts")` → `("posts_helper", "PostsHelper")`.
32pub(crate) fn helper_names(name: &str) -> (String, String) {
33    (helper_module_snake(name), helper_type_name(name))
34}
35
36pub(crate) fn render_helper_content(name: &str, helper_label: &str) -> String {
37    let (snake, pascal) = helper_names(name);
38    crate::templates::get("helper/helper.rs.template")
39        .replace("{pascal}", &pascal)
40        .replace("{snake}", &snake)
41        .replace("{helper_label}", helper_label)
42}
43
44pub(crate) fn register_helper_in_mod(existing: &str, name: &str) -> String {
45    let (snake, pascal) = helper_names(name);
46    register_helper(existing, &snake, &pascal)
47}
48
49pub(crate) fn helpers_mod_path() -> &'static str {
50    HELPERS_MOD_PATH
51}
52
53pub(crate) fn read_helpers_mod() -> String {
54    std::fs::read_to_string(HELPERS_MOD_PATH).unwrap_or_else(|_| HELPERS_MOD_BASE.to_string())
55}
56
57impl Generator for HelperGenerator {
58    fn name(&self) -> &str {
59        "helper"
60    }
61
62    fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
63        let name = args.first().copied().ok_or_else(|| {
64            doido_core::anyhow::anyhow!("helper generator requires a name argument")
65        })?;
66        let snake = helper_module_snake(name);
67        let pascal = helper_type_name(name);
68        let content = render_helper_content(name, &to_snake(name));
69        let test = crate::templates::get("helper/helper_test.rs.template")
70            .replace("{pascal}", &pascal)
71            .replace("{snake}", &snake);
72
73        let helpers_mod = register_helper_in_mod(&read_helpers_mod(), name);
74
75        Ok(vec![
76            GeneratedFile {
77                path: format!("app/helpers/{snake}.rs"),
78                content,
79            },
80            GeneratedFile {
81                path: HELPERS_MOD_PATH.to_string(),
82                content: helpers_mod,
83            },
84            GeneratedFile {
85                path: format!("tests/{snake}_test.rs"),
86                content: test,
87            },
88        ])
89    }
90}
91
92/// Appends `pub mod <name>;` + `pub use …::NameHelper` to `app/helpers/mod.rs`.
93/// Idempotent: skips when the module is already declared.
94fn register_helper(helpers_mod: &str, snake: &str, pascal: &str) -> String {
95    let decl = format!("pub mod {snake};");
96    if helpers_mod.lines().any(|l| l.trim() == decl) {
97        return helpers_mod.to_string();
98    }
99    let use_line = format!("pub use {snake}::{pascal};");
100    let marker = "@generated-helpers";
101    let mut lines: Vec<String> = helpers_mod.lines().map(String::from).collect();
102    match lines.iter().position(|l| l.contains(marker)) {
103        Some(i) => {
104            lines.insert(i, use_line);
105            lines.insert(i, decl);
106        }
107        None => {
108            lines.push(decl);
109            lines.push(use_line);
110        }
111    }
112    let mut out = lines.join("\n");
113    out.push('\n');
114    out
115}