rok-cli 0.3.9

Developer CLI for rok-based Axum applications
//! `--template minimal` — Bare axum + sqlx skeleton, customised by WizardConfig.

use std::{fs, path::Path};

use super::{AuthDriver, CacheDriver, QueueDriver, WizardConfig};

pub fn generate(name: &str, root: &Path, config: &WizardConfig) -> anyhow::Result<()> {
    for dir in &["src", "database/migrations"] {
        fs::create_dir_all(root.join(dir))?;
    }

    let mut files: Vec<(&str, String)> = vec![
        ("Cargo.toml", cargo_toml(name, config)),
        ("src/main.rs", main_rs(name)),
        (".env.example", env_example(config)),
        (".gitignore", GITIGNORE.into()),
        ("database/migrations/.gitkeep", String::new()),
    ];

    if config.include_examples {
        files.push(("src/example.rs", EXAMPLE_RS.into()));
    }

    for (rel, content) in &files {
        fs::write(root.join(rel), content)?;
        println!("  create  {rel}");
    }

    Ok(())
}

fn cargo_toml(name: &str, config: &WizardConfig) -> String {
    let db_feat = config.db.sqlx_feature();

    let mut extra = String::new();

    if config.auth != AuthDriver::None {
        extra.push_str("rok-auth   = { version = \"0.1\", features = [\"axum\"] }\n");
    }

    if config.cache == CacheDriver::Redis {
        extra.push_str("rok-cache  = { version = \"0.1\", features = [\"redis\", \"axum\"] }\n");
    } else if config.cache == CacheDriver::Memory {
        extra.push_str("rok-cache  = { version = \"0.1\" }\n");
    }

    if config.queue == QueueDriver::Postgres || config.queue == QueueDriver::Redis {
        extra.push_str("rok-queue  = { version = \"0.1\" }\n");
    }

    format!(
        r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "{name}"
path = "src/main.rs"

[dependencies]
axum       = "0.8"
tokio      = {{ version = "1", features = ["full"] }}
serde      = {{ version = "1", features = ["derive"] }}
serde_json = "1"
dotenvy    = "0.15"
sqlx       = {{ version = "0.8", default-features = false, features = [
    "macros", "runtime-tokio-native-tls", "{db_feat}",
] }}
rok-orm    = {{ version = "0.1", features = ["{db_feat}"] }}
{extra}"#
    )
}

fn main_rs(name: &str) -> String {
    format!(
        r#"use axum::{{routing::get, Router}};

#[tokio::main]
async fn main() {{
    dotenvy::dotenv().ok();

    let addr = std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:3000".into());

    let app = Router::new().route("/", get(|| async {{ "{name} is running" }}));

    let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind");
    println!("{name} listening on {{addr}}");
    axum::serve(listener, app).await.expect("server");
}}
"#
    )
}

fn env_example(config: &WizardConfig) -> String {
    let mut out = String::from("LISTEN_ADDR=0.0.0.0:3000\n");

    let db_url = config.db.example_url();
    out.push_str(&format!("DATABASE_URL={db_url}\n"));

    if config.auth == AuthDriver::Jwt
        || config.auth == AuthDriver::Session
        || config.auth == AuthDriver::MagicLink
    {
        out.push_str("JWT_SECRET=change-me-to-a-random-secret\n");
    }

    if config.auth == AuthDriver::Social {
        out.push_str("OAUTH_CLIENT_ID=\nOAUTH_CLIENT_SECRET=\n");
    }

    if config.cache == CacheDriver::Redis || config.queue == QueueDriver::Redis {
        out.push_str("REDIS_URL=redis://localhost:6379\n");
    }

    out
}

const GITIGNORE: &str = "/target\n.env\n";

const EXAMPLE_RS: &str = r#"use axum::{extract::Path, response::IntoResponse, Json};
use serde_json::json;

/// GET /example/:id
pub async fn show(Path(id): Path<i64>) -> impl IntoResponse {
    Json(json!({ "id": id, "message": "replace with real logic" }))
}
"#;