Skip to main content

doido_auth/generators/
controller.rs

1//! `doido generate auth:controller` — controller with `require_user` guards.
2
3use super::route_injector::{
4    inject_action_routes, read_controllers_mod, read_routes, register_controller,
5    CONTROLLERS_MOD_PATH, ROUTES_PATH,
6};
7use super::template;
8use super::{to_pascal, to_snake, AuthGenerator, GeneratedFile};
9use doido_core::Result;
10
11pub struct AuthControllerGenerator;
12
13fn render_action(action: &str, pascal: &str) -> String {
14    match action {
15        "index" => format!(
16            "    /// GET /{snake}\n    pub async fn index(ctx: Context, _user: CurrentUser<User>) -> doido::Result<Response> {{\n        Ok(ctx.render(\"{snake}/index\", json!({{}})))\n    }}\n\n",
17            snake = to_snake(pascal)
18        ),
19        "show" => format!(
20            "    /// GET /{snake}/{{id}}\n    pub async fn show(ctx: Context, _user: CurrentUser<User>) -> doido::Result<Response> {{\n        let _id = parse_id(&ctx);\n        Ok(ctx.render(\"{snake}/show\", json!({{}})))\n    }}\n\n",
21            snake = to_snake(pascal)
22        ),
23        other => format!(
24            "    /// GET /{snake}/{other}\n    pub async fn {other}(ctx: Context, _user: CurrentUser<User>) -> doido::Result<Response> {{\n        Ok(ctx.json(json!({{\"action\": \"{other}\"}})))\n    }}\n\n",
25            snake = to_snake(pascal)
26        ),
27    }
28}
29
30impl AuthGenerator for AuthControllerGenerator {
31    fn name(&self) -> &str {
32        "auth:controller"
33    }
34
35    fn generate(&self, args: &[&str]) -> Result<Vec<GeneratedFile>> {
36        let positional: Vec<&str> = args
37            .iter()
38            .copied()
39            .filter(|a| !a.starts_with("--"))
40            .collect();
41
42        let name = positional.first().copied().ok_or_else(|| {
43            doido_core::anyhow::anyhow!("auth:controller requires a name argument")
44        })?;
45
46        let actions: Vec<&str> = if positional.len() > 1 {
47            positional[1..].to_vec()
48        } else {
49            vec!["index"]
50        };
51
52        let snake = to_snake(name);
53        let pascal = to_pascal(name);
54        let controller = format!("{pascal}Controller");
55
56        let actions_body: String = actions.iter().map(|a| render_action(a, &pascal)).collect();
57
58        let content = template("auth_controller.rs.template")
59            .replace("{pascal}", &pascal)
60            .replace("{actions}", &actions_body);
61
62        let controllers_mod = register_controller(&read_controllers_mod(), &snake, &controller);
63        let routes = inject_action_routes(&read_routes(), &snake, &controller, &actions);
64
65        Ok(vec![
66            GeneratedFile {
67                path: format!("app/controllers/{snake}_controller.rs"),
68                content,
69            },
70            GeneratedFile {
71                path: CONTROLLERS_MOD_PATH.to_string(),
72                content: controllers_mod,
73            },
74            GeneratedFile {
75                path: ROUTES_PATH.to_string(),
76                content: routes,
77            },
78        ])
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    #[test]
87    fn emits_require_user_and_routes() {
88        let files = AuthControllerGenerator
89            .generate(&["Dashboard", "index", "show"])
90            .unwrap();
91        let controller = files
92            .iter()
93            .find(|f| f.path == "app/controllers/dashboard_controller.rs")
94            .unwrap();
95        assert!(controller.content.contains("require_user"));
96        assert!(controller.content.contains("CurrentUser<User>"));
97
98        let routes = files.iter().find(|f| f.path == ROUTES_PATH).unwrap();
99        assert!(routes.content.contains("DashboardController::index"));
100        assert!(routes.content.contains("DashboardController::show"));
101    }
102}