use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;
use heck::{ToSnakeCase, ToUpperCamelCase};
pub struct CrudScaffold;
impl Scaffold for CrudScaffold {
fn name(&self) -> &'static str {
"crud"
}
fn description(&self) -> &'static str {
"Full CRUD: model + migration + controller + validation + routes + tests"
}
fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
let mut r = ScaffoldResult::default();
let model = args.model_or("Resource");
let pascal = model.to_upper_camel_case();
let snake = model.to_snake_case();
let d = args.dry_run;
write_file(
&mut r,
&format!("src/app/models/{snake}.rs"),
&model_template(&pascal, &snake),
d,
)?;
write_file(
&mut r,
&format!("src/app/controllers/{snake}_controller.rs"),
&controller_template(&pascal, &snake),
d,
)?;
write_file(
&mut r,
&format!("src/app/requests/{snake}_request.rs"),
&request_template(&pascal, &snake),
d,
)?;
write_file(
&mut r,
&format!("migrations/create_{snake}s_table.sql"),
&migration_template(&snake),
d,
)?;
write_file(
&mut r,
&format!("tests/{snake}_controller_test.rs"),
&test_template(&pascal, &snake),
d,
)?;
r.warnings
.push(format!("Register /{snake}s routes in src/app/routes.rs"));
Ok(r)
}
}
fn model_template(pascal: &str, snake: &str) -> String {
format!(
r#"use rok_orm::prelude::*;
use serde::{{Deserialize, Serialize}};
#[derive(Debug, Clone, Serialize, Deserialize, Model)]
#[rok_orm(table = "{snake}s")]
pub struct {pascal} {{
pub id: i64,
pub name: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}}
"#
)
}
fn controller_template(pascal: &str, snake: &str) -> String {
format!(
r#"use axum::{{extract::{{Path, State}}, response::IntoResponse, Json}};
use rok_auth::axum::{{Ctx, Response}};
use rok_validate::Valid;
use crate::app::{{models::{snake}::{pascal}, requests::{snake}_request::{{Create{pascal}Request, Update{pascal}Request}}}};
pub async fn index(State(pool): State<sqlx::PgPool>) -> impl IntoResponse {{
let items = {pascal}::all(&pool).await.unwrap_or_default();
Response::json(serde_json::json!({{ "data": items }}))
}}
pub async fn show(State(pool): State<sqlx::PgPool>, Path(id): Path<i64>) -> impl IntoResponse {{
match {pascal}::find(id, &pool).await {{
Ok(item) => Response::json(serde_json::json!({{ "data": item }})),
Err(_) => Response::not_found("{pascal} not found"),
}}
}}
pub async fn store(State(pool): State<sqlx::PgPool>, Valid(Json(req)): Valid<Json<Create{pascal}Request>>) -> impl IntoResponse {{
// TODO: insert into DB
Response::created(serde_json::json!({{ "message": "created" }}))
}}
pub async fn update(State(pool): State<sqlx::PgPool>, Path(id): Path<i64>, Valid(Json(req)): Valid<Json<Update{pascal}Request>>) -> impl IntoResponse {{
// TODO: update in DB
Response::json(serde_json::json!({{ "message": "updated" }}))
}}
pub async fn destroy(State(pool): State<sqlx::PgPool>, Path(id): Path<i64>) -> impl IntoResponse {{
// TODO: delete from DB
Response::no_content()
}}
"#
)
}
fn request_template(pascal: &str, _snake: &str) -> String {
format!(
r#"use rok_validate::Validate;
use serde::Deserialize;
#[derive(Debug, Deserialize, Validate)]
pub struct Create{pascal}Request {{
#[validate(required, max_length = 255)]
pub name: Option<String>,
}}
#[derive(Debug, Deserialize, Validate)]
pub struct Update{pascal}Request {{
#[validate(max_length = 255)]
pub name: Option<String>,
}}
"#
)
}
fn migration_template(snake: &str) -> String {
format!(
r#"CREATE TABLE {snake}s (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#
)
}
fn test_template(_pascal: &str, snake: &str) -> String {
format!(
r#"#[cfg(test)]
mod tests {{
#[tokio::test]
async fn test_{snake}_index_returns_list() {{
// TODO: GET /{snake}s and verify 200 + data array
}}
#[tokio::test]
async fn test_{snake}_store_creates_record() {{
// TODO: POST /{snake}s and verify 201 + record in DB
}}
#[tokio::test]
async fn test_{snake}_show_returns_record() {{
// TODO: GET /{snake}s/1 and verify 200 + correct data
}}
#[tokio::test]
async fn test_{snake}_destroy_deletes_record() {{
// TODO: DELETE /{snake}s/1 and verify 204 + record gone
}}
}}
"#
)
}