use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub enum ClientType {
TypeScript,
Rust,
}
#[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,
}
}
}
pub struct ClientGenerator {
config: GeneratorConfig,
}
impl ClientGenerator {
pub fn new(config: GeneratorConfig) -> Self {
Self { config }
}
pub async fn generate_typescript(&self, spec: &OpenApiSpec) -> Result<String> {
let mut output = String::new();
output.push_str("// Generated by Kegani Client Generator\n");
output.push_str("// DO NOT EDIT MANUALLY\n\n");
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");
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)
}
pub async fn generate_rust(&self, spec: &OpenApiSpec) -> Result<String> {
let mut output = String::new();
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");
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)
}
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 {
let clean = path.replace("/", "_").replace("{", "").replace("}", "");
format!("{}_{}", prefix, clean.trim_start_matches('_'))
}
}
#[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 {
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)
}
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"
);
}
}