rok-cli 0.3.2

Developer CLI for rok-based Axum applications
use super::{write_file, Scaffold, ScaffoldArgs, ScaffoldResult};
use anyhow::Result;

pub struct FlagsScaffold;

impl Scaffold for FlagsScaffold {
    fn name(&self) -> &'static str {
        "flags"
    }
    fn description(&self) -> &'static str {
        "Feature flags: toggle controller, evaluation, A/B testing, rollout, audit log"
    }

    fn generate(&self, args: &ScaffoldArgs) -> Result<ScaffoldResult> {
        let mut r = ScaffoldResult::default();
        let d = args.dry_run;
        write_file(
            &mut r,
            "src/app/controllers/flag_controller.rs",
            CONTROLLER,
            d,
        )?;
        write_file(
            &mut r,
            "migrations/create_feature_flags_tables.sql",
            MIGRATION,
            d,
        )?;
        r.warnings
            .push("Register /admin/flags routes behind admin guard".into());
        Ok(r)
    }
}

const CONTROLLER: &str = r#"use axum::{extract::{Path, State}, response::IntoResponse, Json};
use rok_auth::axum::{Ctx, Response};

pub async fn index(State(pool): State<sqlx::PgPool>) -> impl IntoResponse {
    // TODO: list all feature flags
    Response::json(serde_json::json!({ "data": [] }))
}

pub async fn evaluate(Path(name): Path<String>, ctx: Ctx) -> impl IntoResponse {
    // TODO: evaluate flag for current user (cohort, percentage rollout, A/B variant)
    Response::json(serde_json::json!({ "flag": name, "enabled": false, "variant": null }))
}

pub async fn toggle(Path(name): Path<String>, Json(body): Json<serde_json::Value>, State(pool): State<sqlx::PgPool>) -> impl IntoResponse {
    // TODO: update flag enabled state, log to flag_audit_log
    Response::json(serde_json::json!({ "message": "Flag updated" }))
}
"#;

const MIGRATION: &str = r#"CREATE TABLE feature_flags (
    id          BIGSERIAL PRIMARY KEY,
    name        TEXT NOT NULL UNIQUE,
    enabled     BOOLEAN NOT NULL DEFAULT false,
    rollout     INTEGER NOT NULL DEFAULT 100,
    variant_a   TEXT,
    variant_b   TEXT,
    cohort      JSONB,
    description TEXT,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
    updated_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE flag_audit_log (
    id         BIGSERIAL PRIMARY KEY,
    flag_id    BIGINT NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE,
    actor_id   BIGINT,
    action     TEXT NOT NULL,
    old_value  JSONB,
    new_value  JSONB,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
"#;