rok-cli 0.3.2

Developer CLI for rok-based Axum applications
//! `--template microservice` — Minimal service with health endpoint, Docker, K8s.

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

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

    let files: Vec<(&str, String)> = vec![
        ("Cargo.toml", cargo_toml(name)),
        ("src/main.rs", main_rs(name)),
        ("k8s/deployment.yaml", k8s_deployment(name)),
        ("k8s/service.yaml", k8s_service(name)),
        (".env.example", ENV_EXAMPLE.into()),
        (".gitignore", GITIGNORE.into()),
        ("Dockerfile", DOCKERFILE.into()),
        ("src/handler/mod.rs", HANDLER_MOD_RS.into()),
        ("src/handler/ping.rs", HANDLER_PING_RS.into()),
    ];

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

    Ok(())
}

fn cargo_toml(name: &str) -> String {
    format!(
        r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"

[[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"
rok-health  = {{ version = "0.1" }}
rok-problem = {{ version = "0.1", features = ["axum"] }}
"#
    )
}

fn main_rs(name: &str) -> String {
    format!(
        r#"mod handler;

use axum::Router;
use rok_health::HealthRouter;

#[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 health = HealthRouter::new().build();

    let app = Router::new()
        .route("/ping", axum::routing::get(handler::ping::ping))
        .merge(health);

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

fn k8s_deployment(name: &str) -> String {
    format!(
        r#"apiVersion: apps/v1
kind: Deployment
metadata:
  name: {name}
spec:
  replicas: 2
  selector:
    matchLabels:
      app: {name}
  template:
    metadata:
      labels:
        app: {name}
    spec:
      containers:
        - name: {name}
          image: {name}:latest
          ports:
            - containerPort: 3000
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 3000
          livenessProbe:
            httpGet:
              path: /health/live
              port: 3000
"#
    )
}

fn k8s_service(name: &str) -> String {
    format!(
        r#"apiVersion: v1
kind: Service
metadata:
  name: {name}
spec:
  selector:
    app: {name}
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP
"#
    )
}

const ENV_EXAMPLE: &str = "LISTEN_ADDR=0.0.0.0:3000\n";

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

const DOCKERFILE: &str = r#"# ── build ────────────────────────────────────────────────────────────────────
FROM rust:1.82-slim AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

# ── runtime ───────────────────────────────────────────────────────────────────
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/app /usr/local/bin/app
EXPOSE 3000
CMD ["app"]
"#;

const HANDLER_MOD_RS: &str = "pub mod ping;\n";

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

pub async fn ping() -> impl IntoResponse {
    Json(json!({ "status": "ok" }))
}
"#;