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" }))
}
"#;