kegani 0.1.0

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Client generator implementation
//!
//! Generates TypeScript and Rust API clients from OpenAPI spec.

use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};

/// Client type to generate
#[derive(Debug, Clone)]
pub enum ClientType {
    TypeScript,
    Rust,
}

/// Generator configuration
#[derive(Debug, Clone)]
pub struct GeneratorConfig {
    pub output_dir: String,
    pub client_type: ClientType,
    pub api_url: Option<String>,
    pub include_examples: bool,
}

impl Default for GeneratorConfig {
    fn default() -> Self {
        Self {
            output_dir: "./generated".to_string(),
            client_type: ClientType::TypeScript,
            api_url: None,
            include_examples: true,
        }
    }
}

/// Client generator
pub struct ClientGenerator {
    config: GeneratorConfig,
}

impl ClientGenerator {
    /// Create a new generator
    pub fn new(config: GeneratorConfig) -> Self {
        Self { config }
    }

    /// Generate TypeScript client
    pub async fn generate_typescript(&self, spec: &OpenApiSpec) -> Result<String> {
        let mut output = String::new();

        // Header
        output.push_str("// Generated by Kegani Client Generator\n");
        output.push_str("// DO NOT EDIT MANUALLY\n\n");

        // API client class
        output.push_str("export class KeganiClient {\n");
        output.push_str("  private baseUrl: string;\n");
        output.push_str("  private headers: Record<string, string>;\n\n");

        output.push_str("  constructor(baseUrl: string, headers: Record<string, string> = {}) {\n");
        output.push_str("    this.baseUrl = baseUrl;\n");
        output.push_str("    this.headers = {\n");
        output.push_str("      'Content-Type': 'application/json',\n");
        output.push_str("      ...headers,\n");
        output.push_str("    };\n");
        output.push_str("  }\n\n");

        // Generate methods for each path
        for (path, item) in &spec.paths {
            if let Some(_get) = &item.get {
                let method_name = Self::to_method_name("get", path);
                output.push_str(&format!("  async {}(): Promise<any> {{\n", method_name));
                output.push_str("    const response = await fetch(`${this.baseUrl}");
                output.push_str(path);
                output.push_str("`,\n      {\n");
                output.push_str("        method: 'GET',\n");
                output.push_str("        headers: this.headers,\n");
                output.push_str("      });\n");
                output.push_str("    if (!response.ok) throw new Error(await response.text());\n");
                output.push_str("    return response.json();\n");
                output.push_str("  }\n\n");
            }

            if let Some(_post) = &item.post {
                let method_name = Self::to_method_name("create", path);
                output.push_str(&format!("  async {}(data: any): Promise<any> {{\n", method_name));
                output.push_str("    const response = await fetch(`${this.baseUrl}");
                output.push_str(path);
                output.push_str("`,\n      {\n");
                output.push_str("        method: 'POST',\n");
                output.push_str("        headers: this.headers,\n");
                output.push_str("        body: JSON.stringify(data),\n");
                output.push_str("      });\n");
                output.push_str("    if (!response.ok) throw new Error(await response.text());\n");
                output.push_str("    return response.json();\n");
                output.push_str("  }\n\n");
            }
        }

        output.push_str("}\n");

        Ok(output)
    }

    /// Generate Rust client
    pub async fn generate_rust(&self, spec: &OpenApiSpec) -> Result<String> {
        let mut output = String::new();

        // Header
        output.push_str("//! Generated by Kegani Client Generator\n");
        output.push_str("//! DO NOT EDIT MANUALLY\n\n");

        output.push_str("use reqwest::{Client, Error};\n");
        output.push_str("use serde::{Deserialize, Serialize};\n\n");

        output.push_str("pub struct KeganiClient {\n");
        output.push_str("    client: Client,\n");
        output.push_str("    base_url: String,\n");
        output.push_str("    auth_token: Option<String>,\n");
        output.push_str("}\n\n");

        output.push_str("impl KeganiClient {\n");
        output.push_str("    pub fn new(base_url: &str) -> Self {\n");
        output.push_str("        Self {\n");
        output.push_str("            client: Client::new(),\n");
        output.push_str("            base_url: base_url.to_string(),\n");
        output.push_str("            auth_token: None,\n");
        output.push_str("        }\n");
        output.push_str("    }\n\n");

        output.push_str("    pub fn with_auth(mut self, token: &str) -> Self {\n");
        output.push_str("        self.auth_token = Some(token.to_string());\n");
        output.push_str("        self\n");
        output.push_str("    }\n\n");

        // Generate methods
        for (path, item) in &spec.paths {
            if item.get.is_some() {
                let method_name = Self::to_method_name("get", path);
                output.push_str(&format!("    pub async fn {}(&self) -> Result<serde_json::Value, Error> {{\n", method_name));
                output.push_str("        let url = format!(\"{}{}\", self.base_url, \"");
                output.push_str(path);
                output.push_str("\");\n");
                output.push_str("        self.client\n");
                output.push_str("            .get(&url)\n");
                output.push_str("            .header(\"Accept\", \"application/json\")\n");
                output.push_str("            .send()\n");
                output.push_str("            .await?\n");
                output.push_str("            .json()\n");
                output.push_str("    }\n\n");
            }
        }

        output.push_str("}\n");

        Ok(output)
    }

    /// Write output to file
    pub async fn write_output(&self, content: &str, filename: &str) -> Result<()> {
        let path = Path::new(&self.config.output_dir).join(filename);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        std::fs::write(&path, content)?;
        Ok(())
    }

    fn to_method_name(prefix: &str, path: &str) -> String {
        // Convert /api/users -> get_api_users
        let clean = path.replace("/", "_").replace("{", "").replace("}", "");
        format!("{}_{}", prefix, clean.trim_start_matches('_'))
    }
}

/// OpenAPI specification (simplified)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenApiSpec {
    pub openapi: String,
    pub info: Info,
    pub paths: std::collections::HashMap<String, PathItem>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Info {
    pub title: String,
    pub version: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PathItem {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub get: Option<Operation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub post: Option<Operation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub put: Option<Operation>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub delete: Option<Operation>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Operation {
    pub summary: Option<String>,
    pub operation_id: Option<String>,
    pub parameters: Option<Vec<Parameter>>,
    pub request_body: Option<RequestBody>,
    pub responses: std::collections::HashMap<String, Response>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
    pub name: String,
    pub location: String,
    pub required: bool,
    pub schema: Schema,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestBody {
    pub required: bool,
    pub content: std::collections::HashMap<String, MediaType>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
    pub description: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MediaType {
    pub schema: Schema,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Schema {
    #[serde(rename = "type")]
    pub type_: Option<String>,
    pub format: Option<String>,
    pub properties: Option<std::collections::HashMap<String, Schema>>,
}

impl OpenApiSpec {
    /// Load from JSON file
    pub fn from_file(path: &str) -> Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let spec: OpenApiSpec = serde_json::from_str(&content)?;
        Ok(spec)
    }

    /// Load from URL
    pub async fn from_url(url: &str) -> Result<Self> {
        let response = reqwest::get(url).await?;
        let spec: OpenApiSpec = response.json().await?;
        Ok(spec)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_method_name_conversion() {
        assert_eq!(
            ClientGenerator::to_method_name("get", "/api/users/{id}"),
            "get_api_users_id"
        );
    }
}