use tera::{Tera, Context};
use indexmap::IndexMap;
use crate::tree::Tree;
use crate::models::Statement;
pub struct ControllerGenerator;
impl ControllerGenerator {
pub fn generate(tree: &Tree) -> IndexMap<String, String> {
let mut files = IndexMap::new();
let mut tera = Tera::default();
tera.add_raw_template("handler", HANDLER_TEMPLATE).unwrap();
let mut mod_lines = vec!["pub mod prelude;".to_string()];
let mut prelude_lines = vec![];
for controller in tree.all_controllers() {
let mut ctx = Context::new();
let model_name = controller.name.trim_end_matches("Controller").to_string();
let handler_mod = crate::generators::model_generator::to_snake_case(&model_name);
let handler_struct = format!("{}Handlers", &model_name);
ctx.insert("handler_struct", &handler_struct);
ctx.insert("model_name", &model_name);
ctx.insert("model_var", &model_name.to_lowercase());
let mut method_defs = vec![];
for (method_name, stmts) in &controller.methods {
let code = Self::render_handler(tree, method_name, stmts, &model_name);
method_defs.push(code);
}
ctx.insert("methods", &method_defs);
let rendered = tera.render("handler", &ctx).unwrap();
let path = format!("src/handlers/{}.rs", handler_mod);
files.insert(path, rendered);
mod_lines.push(format!("pub mod {};", handler_mod));
prelude_lines.push(format!("pub use super::{}::{};", handler_mod, handler_struct));
}
files.insert("src/handlers/mod.rs".to_string(), mod_lines.join("\n"));
files.insert("src/handlers/prelude.rs".to_string(), format!("//! Re-exports\n\n{}\n", prelude_lines.join("\n")));
files
}
fn render_handler(tree: &Tree, name: &str, stmts: &[Statement], model_name: &str) -> String {
let model_var = model_name.to_lowercase();
let model_path = format!("crate::models::{}", model_name);
let request_path = format!("crate::requests::{}", name);
let has_request = stmts.iter().any(|s| matches!(s, Statement::Validate(_)));
let mut sig = format!(
"pub async fn {}(State(pool): State<sqlx::PgPool>",
name
);
if has_request {
sig.push_str(&format!(
", Json(body): Json<crate::requests::{}>",
capitalize(name)
));
}
sig.push_str(") -> impl axum::response::IntoResponse");
let mut body = String::new();
for stmt in stmts {
let rendered = match stmt {
Statement::Query(_) => {
format!(
"let items = sqlx::query_as::<_, {}>(\"SELECT * FROM {}\")\n .fetch_all(&pool)\n .await\n .unwrap();",
model_path, pluralize(&model_name.to_lowercase())
)
}
Statement::Find(_) => {
format!(
"let item = sqlx::query_as::<_, {}>(\"SELECT * FROM {} WHERE id = $1\")\n .bind(id)\n .fetch_one(&pool)\n .await\n .unwrap();",
model_path, pluralize(&model_name.to_lowercase())
)
}
Statement::Save(_) => {
format!(
"let item = sqlx::query_as::<_, {}>(\n \"INSERT INTO {} ({}) VALUES ($1) RETURNING *\"\n )\n .fetch_one(&pool)\n .await\n .unwrap();",
model_path, pluralize(&model_name.to_lowercase()), get_insert_columns(tree, model_name)
)
}
Statement::Delete(_) => {
format!(
"sqlx::query(\"DELETE FROM {} WHERE id = $1\")\n .bind(id)\n .execute(&pool)\n .await\n .unwrap();",
pluralize(&model_name.to_lowercase())
)
}
Statement::Validate(ref s) => {
if !s.fields.is_empty() {
format!("// validated via Json(crate::requests::{})", capitalize(name))
} else {
String::new()
}
}
Statement::Render(ref s) => format!("// render: {}", s.view),
Statement::Redirect(ref s) => format!("// redirect: {}", s.route),
Statement::Respond(ref s) => {
let status = s.status.unwrap_or(200);
format!("StatusCode::from_u16({}).unwrap()", status)
}
_ => String::new(),
};
if !rendered.is_empty() {
body.push_str(&format!(" {}\n", rendered));
}
}
body.push_str(&format!(" axum::Json(serde_json::json!({{\"ok\": true}}))"));
format!("{}\n{{\n{}}}", sig, body)
}
}
fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().to_string() + c.as_str(),
}
}
fn pluralize(s: &str) -> String {
format!("{}s", s)
}
fn get_insert_columns(_tree: &Tree, model_name: &str) -> String {
format!("{}, field1, field2", model_name.to_lowercase())
}
const HANDLER_TEMPLATE: &str = r#"use axum::{extract::State, Json};
use axum::http::StatusCode;
use serde_json::Value;
use sqlx::PgPool;
use crate::models::prelude::*;
/// Axum handlers for {{ model_name }} routes.
pub struct {{ handler_struct }};
impl {{ handler_struct }} {
{% for method in methods %}
{{ method }}
{% endfor %}
}
"#;