doido_auth/generators/
controller.rs1use 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}