kegani 0.1.0

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Client templates for code generation

/// TypeScript client template
pub const TYPESCRIPT_CLIENT_TEMPLATE: &str = r#"
// Generated by Kegani Client Generator
// DO NOT EDIT MANUALLY

export interface ApiClientOptions {
  baseUrl: string;
  authToken?: string;
  timeout?: number;
}

export class KeganiApiClient {
  private baseUrl: string;
  private headers: Record<string, string>;
  private timeout: number;

  constructor(options: ApiClientOptions) {
    this.baseUrl = options.baseUrl;
    this.timeout = options.timeout || 30000;
    this.headers = {
      'Content-Type': 'application/json',
      'Accept': 'application/json',
      ...(options.authToken && { 'Authorization': `Bearer ${options.authToken}` }),
    };
  }

  private async request<T>(
    method: string,
    path: string,
    body?: any,
    params?: Record<string, string>
  ): Promise<T> {
    const url = new URL(path, this.baseUrl);
    if (params) {
      Object.entries(params).forEach(([key, value]) => {
        url.searchParams.append(key, value);
      });
    }

    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url.toString(), {
        method,
        headers: this.headers,
        body: body ? JSON.stringify(body) : undefined,
        signal: controller.signal,
      });

      if (!response.ok) {
        const error = await response.text();
        throw new ApiError(response.status, error);
      }

      return response.json();
    } finally {
      clearTimeout(timeout);
    }
  }

  // User endpoints
  async listUsers(params?: { page?: number; size?: number }): Promise<UserListResponse> {
    return this.request('GET', '/api/users', undefined, params);
  }

  async getUser(id: string): Promise<User> {
    return this.request('GET', `/api/users/${id}`);
  }

  async createUser(data: CreateUserInput): Promise<User> {
    return this.request('POST', '/api/users', data);
  }

  async updateUser(id: string, data: UpdateUserInput): Promise<User> {
    return this.request('PUT', `/api/users/${id}`, data);
  }

  async deleteUser(id: string): Promise<void> {
    return this.request('DELETE', `/api/users/${id}`);
  }
}

export class ApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = 'ApiError';
  }
}

// Types
export interface User {
  id: string;
  email: string;
  name: string;
  createdAt: string;
}

export interface CreateUserInput {
  email: string;
  name: string;
  password: string;
}

export interface UpdateUserInput {
  name?: string;
}

export interface UserListResponse {
  items: User[];
  page: number;
  size: number;
  total: number;
}

export default KeganiApiClient;
"#;

/// Rust client template
pub const RUST_CLIENT_TEMPLATE: &str = r#"
//! Generated by Kegani Client Generator
//! DO NOT EDIT MANUALLY

use reqwest::{Client, Error, Response};
use serde::{Deserialize, Serialize};
use std::time::Duration;

/// API client configuration
#[derive(Debug, Clone)]
pub struct ApiClientConfig {
    pub base_url: String,
    pub auth_token: Option<String>,
    pub timeout: Duration,
}

impl Default for ApiClientConfig {
    fn default() -> Self {
        Self {
            base_url: "http://localhost:8080".to_string(),
            auth_token: None,
            timeout: Duration::from_secs(30),
        }
    }
}

/// Kegani API client
#[derive(Clone)]
pub struct KeganiApiClient {
    client: Client,
    config: ApiClientConfig,
}

impl KeganiApiClient {
    /// Create a new client
    pub fn new(config: ApiClientConfig) -> Self {
        let client = Client::builder()
            .timeout(config.timeout)
            .build()
            .unwrap_or_else(|_| Client::new());

        Self { client, config }
    }

    /// Create with auth token
    pub fn with_auth(token: &str) -> Self {
        Self::new(ApiClientConfig {
            auth_token: Some(token.to_string()),
            ..Default::default()
        })
    }

    /// Make a request
    async fn request<T: for<'de> Deserialize<'de>>(
        &self,
        method: &str,
        path: &str,
        body: Option<&impl Serialize>,
    ) -> Result<T, ApiError> {
        let url = format!("{}{}", self.config.base_url, path);

        let mut request = match method {
            "GET" => self.client.get(&url),
            "POST" => self.client.post(&url),
            "PUT" => self.client.put(&url),
            "DELETE" => self.client.delete(&url),
            "PATCH" => self.client.patch(&url),
            _ => return Err(ApiError::InvalidMethod(method.to_string())),
        };

        // Add auth header
        if let Some(token) = &self.config.auth_token {
            request = request.header("Authorization", format!("Bearer {}", token));
        }

        // Add body
        if let Some(body) = body {
            request = request.json(body);
        }

        let response = request.send().await?;
        Self::handle_response(response).await
    }

    async fn handle_response<T: for<'de> Deserialize<'de>>(
        response: Response,
    ) -> Result<T, ApiError> {
        let status = response.status();
        if status.is_success() {
            response.json().await.map_err(ApiError::Request)
        } else {
            let text = response.text().await.unwrap_or_default();
            Err(ApiError::Server(status.as_u16(), text))
        }
    }

    // User endpoints
    pub async fn list_users(&self, page: Option<u32>, size: Option<u32>) -> Result<UserListResponse, ApiError> {
        let mut query = vec![];
        if let Some(p) = page { query.push(format!("page={}", p)); }
        if let Some(s) = size { query.push(format!("size={}", s)); }

        let path = if query.is_empty() {
            "/api/users".to_string()
        } else {
            format!("/api/users?{}", query.join("&"))
        };

        self.request("GET", &path, None::<&()>).await
    }

    pub async fn get_user(&self, id: &str) -> Result<User, ApiError> {
        self.request("GET", &format!("/api/users/{}", id), None::<&()>).await
    }

    pub async fn create_user(&self, input: &CreateUserInput) -> Result<User, ApiError> {
        self.request("POST", "/api/users", Some(input)).await
    }

    pub async fn update_user(&self, id: &str, input: &UpdateUserInput) -> Result<User, ApiError> {
        self.request("PUT", &format!("/api/users/{}", id), Some(input)).await
    }

    pub async fn delete_user(&self, id: &str) -> Result<(), ApiError> {
        let _: serde_json::Value = self.request("DELETE", &format!("/api/users/{}", id), None::<&()>).await?;
        Ok(())
    }
}

/// API error
#[derive(Debug)]
pub enum ApiError {
    Request(reqwest::Error),
    Server(u16, String),
    InvalidMethod(String),
}

impl std::fmt::Display for ApiError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ApiError::Request(e) => write!(f, "Request error: {}", e),
            ApiError::Server(status, msg) => write!(f, "Server error {}: {}", status, msg),
            ApiError::InvalidMethod(m) => write!(f, "Invalid method: {}", m),
        }
    }
}

impl std::error::Error for ApiError {}

impl From<reqwest::Error> for ApiError {
    fn from(e: reqwest::Error) -> Self {
        ApiError::Request(e)
    }
}

// Types
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: String,
    pub email: String,
    pub name: String,
    #[serde(rename = "createdAt")]
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateUserInput {
    pub email: String,
    pub name: String,
    pub password: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateUserInput {
    pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserListResponse {
    pub items: Vec<User>,
    pub page: u32,
    pub size: u32,
    pub total: i64,
}
"#;