rok-cli 0.3.2

Developer CLI for rok-based Axum applications
pub fn generate(output: Option<&str>, format: &str) -> anyhow::Result<()> {
    let path = output.unwrap_or("openapi.json");

    let output_path = if path.ends_with(".yaml") || path.ends_with(".yml") {
        path.to_string()
    } else {
        format!("{}.{}", path.trim_end_matches(".json"), format)
    };

    println!(
        "{} Generating OpenAPI spec → {output_path}",
        console::style("openapi:generate").green().bold()
    );
    println!(
        "  {} This command requires your application to call the OpenAPI generator.\n",
        console::style("info:").yellow()
    );
    println!("  Add the following in your main.rs:\n");
    println!("    use rok_openapi::OpenApiBuilder;\n");
    println!("    let spec = OpenApiBuilder::new(\"My API\", \"1.0.0\")\n");
    println!("      .server(\"http://localhost:3000\", \"Development\")\n");
    println!("      .build();\n");
    println!("    let json = spec.to_json().unwrap();\n");
    println!("    std::fs::write(\"{output_path}\", json).unwrap();");

    Ok(())
}

pub fn serve(port: Option<u16>) -> anyhow::Result<()> {
    let listen_port = port.unwrap_or(3001);

    println!(
        "{} Starting OpenAPI preview server on http://localhost:{listen_port}",
        console::style("openapi:serve").green().bold()
    );
    println!(
        "  {} This command is a placeholder.\n",
        console::style("note:").yellow()
    );
    println!("  Run your app with rok dev and visit /docs for Swagger UI.");

    Ok(())
}

pub fn validate(file: Option<&str>) -> anyhow::Result<()> {
    let target = file.unwrap_or("openapi.json");

    let content = match std::fs::read_to_string(target) {
        Ok(c) => c,
        Err(e) => {
            anyhow::bail!("Cannot read {target}: {e}");
        }
    };

    match serde_json::from_str::<serde_json::Value>(&content) {
        Ok(val) => {
            let openapi_version = val
                .get("openapi")
                .and_then(|v| v.as_str())
                .unwrap_or("unknown");
            let title = val
                .get("info")
                .and_then(|i| i.get("title"))
                .and_then(|t| t.as_str())
                .unwrap_or("Untitled");
            let paths = val
                .get("paths")
                .map(|p| p.as_object().map(|o| o.len()).unwrap_or(0))
                .unwrap_or(0);

            println!(
                "{} {target} is valid JSON (OpenAPI {openapi_version}, \"{title}\", {paths} path(s))",
                console::style("").green().bold()
            );
        }
        Err(e) => {
            anyhow::bail!("Invalid JSON: {e}");
        }
    }

    if let Ok(yaml) = serde_yaml::from_str::<serde_json::Value>(&content) {
        if yaml.get("openapi").is_some() {
            println!(
                "  {} Also valid as YAML.",
                console::style("i").cyan().bold()
            );
        }
    }

    Ok(())
}