ru-openapi-cg 0.1.4

A powerful OpenAPI 3.0 code generator written in Rust that supports multiple programming languages and frameworks
// src/generator.rs

use std::fs::File;
use std::io::Write;
use std::collections::HashMap;

use handlebars::Handlebars;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct OpenAPI {
    paths: HashMap<String, PathItem>,
}

#[derive(Debug, Deserialize)]
struct PathItem {
    get: Option<Operation>,
    post: Option<Operation>,
    put: Option<Operation>,
    delete: Option<Operation>,
    patch: Option<Operation>,
}

#[derive(Debug, Deserialize)]
struct Operation {
    #[serde(rename = "operationId")]
    operation_id: Option<String>,
    description: Option<String>,
    parameters: Option<Vec<Parameter>>,
    #[serde(rename = "requestBody")]
    request_body: Option<RequestBody>,
    responses: Option<HashMap<String, Response>>,
}

#[derive(Debug, Deserialize)]
struct Parameter {
    name: String,
    #[serde(rename = "in")]
    location: String, // path/query/header
    required: Option<bool>,
    description: Option<String>,
    schema: Option<Schema>,
}

#[derive(Debug, Deserialize, Clone)]
struct Schema {
    #[serde(rename = "type")]
    ty: Option<String>,
    format: Option<String>,
    items: Option<Box<Schema>>,
    #[serde(rename = "$ref")]
    ref_path: Option<String>,
}

#[derive(Debug, Deserialize)]
struct RequestBody {
    content: Option<HashMap<String, Content>>,
    required: Option<bool>,
    description: Option<String>,
}

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Response {
    description: Option<String>,
    content: Option<HashMap<String, Content>>,
}

#[derive(Debug, Deserialize)]
struct Content {
    schema: Option<Schema>,
}

pub mod models {
    use serde::Serialize;

    #[derive(Serialize)]
    pub struct Endpoint {
        pub name: String,
        pub camel_name: String,
        pub path: String,
        pub formatted_url: String,
        pub path_segments: Vec<PathSegment>,
        pub method: String,
        pub params: Vec<Param>,
        pub query_params: Vec<Param>,
        pub body_param: Option<BodyParam>,
        pub response_type: String,
        pub description: Option<String>,
    }

    #[derive(Serialize)]
    pub struct PathSegment {
        pub segment: String,
        pub is_param: bool,
        pub param_name: String,
    }

    #[derive(Serialize, Clone)]
    pub struct Param {
        pub name: String,
        pub original_name: String, // 保持原始名称用于 URL 处理
        pub location: String, // path/query/header
        pub required: bool,
        pub ty: String,
        pub description: Option<String>,
    }

    #[derive(Serialize)]
    pub struct BodyParam {
        pub name: String,
        pub ty: String,
        pub required: bool,
        pub description: Option<String>,
    }

    #[derive(Serialize)]
    pub struct ApiSpec {
        pub target: String,
        pub endpoints: Vec<Endpoint>,
    }
}

use models::{Endpoint, Param, BodyParam, ApiSpec, PathSegment};

fn extract_ref_type_name(ref_path: &str) -> String {
    // e.g. "#/components/schemas/PayResultResponse" => "PayResultResponse"
    ref_path.rsplit('/').next().unwrap_or(ref_path).to_string()
}

fn schema_type_to_dart(ty: &Option<String>, format: &Option<String>, items: &Option<Box<Schema>>, ref_path: &Option<String>) -> String {
    if let Some(ref_path) = ref_path {
        return extract_ref_type_name(ref_path);
    }
    match (ty.as_deref(), format.as_deref()) {
        (Some("integer"), Some("int64")) => "int".to_string(),
        (Some("integer"), _) => "int".to_string(),
        (Some("number"), Some("float")) => "double".to_string(),
        (Some("number"), Some("double")) => "double".to_string(),
        (Some("number"), _) => "double".to_string(),
        (Some("boolean"), _) => "bool".to_string(),
        (Some("string"), Some("date-time")) => "String".to_string(),
        (Some("string"), _) => "String".to_string(),
        (Some("array"), _) => {
            let item_ty = items.as_ref().map(|s| schema_type_to_dart(&s.ty, &s.format, &s.items, &s.ref_path)).unwrap_or("dynamic".to_string());
            format!("List<{}>", item_ty)
        },
        (Some("object"), _) => "Map<String, dynamic>".to_string(),
        _ => "String".to_string(),
    }
}

fn schema_type_to_typescript(ty: &Option<String>, format: &Option<String>, items: &Option<Box<Schema>>, _ref_path: &Option<String>) -> String {
    match (ty.as_deref(), format.as_deref()) {
        (Some("integer"), _) => "number".to_string(),
        (Some("number"), _) => "number".to_string(),
        (Some("boolean"), _) => "boolean".to_string(),
        (Some("string"), _) => "string".to_string(),
        (Some("array"), _) => {
            let item_ty = items.as_ref().map(|s| schema_type_to_typescript(&s.ty, &s.format, &s.items, &s.ref_path)).unwrap_or("any".to_string());
            format!("{}[]", item_ty)
        },
        (Some("object"), _) => "any".to_string(),
        _ => "any".to_string(),
    }
}

fn schema_type_to_python(ty: &Option<String>, format: &Option<String>, items: &Option<Box<Schema>>, _ref_path: &Option<String>) -> String {
    match (ty.as_deref(), format.as_deref()) {
        (Some("integer"), _) => "int".to_string(),
        (Some("number"), _) => "float".to_string(),
        (Some("boolean"), _) => "bool".to_string(),
        (Some("string"), _) => "str".to_string(),
        (Some("array"), _) => {
            let item_ty = items.as_ref().map(|s| schema_type_to_python(&s.ty, &s.format, &s.items, &s.ref_path)).unwrap_or("Any".to_string());
            format!("List[{}]", item_ty)
        },
        (Some("object"), _) => "dict".to_string(),
        _ => "Any".to_string(),
    }
}

fn schema_type_to_java(ty: &Option<String>, format: &Option<String>, items: &Option<Box<Schema>>, _ref_path: &Option<String>) -> String {
    match (ty.as_deref(), format.as_deref()) {
        (Some("integer"), Some("int64")) => "Long".to_string(),
        (Some("integer"), _) => "Integer".to_string(),
        (Some("number"), Some("float")) => "Float".to_string(),
        (Some("number"), Some("double")) => "Double".to_string(),
        (Some("number"), _) => "Double".to_string(),
        (Some("boolean"), _) => "Boolean".to_string(),
        (Some("string"), _) => "String".to_string(),
        (Some("array"), _) => {
            let item_ty = items.as_ref().map(|s| schema_type_to_java(&s.ty, &s.format, &s.items, &s.ref_path)).unwrap_or("Object".to_string());
            format!("List<{}>", item_ty)
        },
        (Some("object"), _) => "Map<String, Object>".to_string(),
        _ => "Object".to_string(),
    }
}

fn get_type_mapper(template_path: &str) -> fn(&Option<String>, &Option<String>, &Option<Box<Schema>>, &Option<String>) -> String {
    if template_path.contains("dart") || template_path.contains("flutter") {
        schema_type_to_dart
    } else if template_path.contains("typescript") || template_path.contains("ts") {
        schema_type_to_typescript
    } else if template_path.contains("python") || template_path.contains("py") {
        schema_type_to_python
    } else if template_path.contains("java") {
        schema_type_to_java
    } else {
        schema_type_to_dart // 默认dart
    }
}

fn to_camel_case(s: &str) -> String {
    let mut result = String::new();
    let mut make_upper = false;
    
    for (i, c) in s.chars().enumerate() {
        if c == '_' {
            make_upper = true;
        } else if make_upper {
            result.push(c.to_ascii_uppercase());
            make_upper = false;
        } else if i == 0 {
            result.push(c.to_ascii_lowercase());
        } else {
            result.push(c);
        }
    }
    
    result
}

fn parse_path_segments(path: &str) -> Vec<PathSegment> {
    path.split('/')
        .map(|segment| {
            if segment.starts_with('{') && segment.ends_with('}') {
                let param_name = segment.trim_start_matches('{').trim_end_matches('}').to_string();
                PathSegment {
                    segment: segment.to_string(),
                    is_param: true,
                    param_name,
                }
            } else {
                PathSegment {
                    segment: segment.to_string(),
                    is_param: false,
                    param_name: String::new(),
                }
            }
        })
        .collect()
}

fn format_url_with_params(path: &str, params: &[Param]) -> String {
    let mut formatted_url = path.to_string();
    
    for param in params {
        if param.location == "path" {
            let original_param = format!("{{{}}}", param.original_name);
            let dart_interpolation = format!("${{{}}}", param.name); // 使用驼峰化后的参数名
            formatted_url = formatted_url.replace(&original_param, &dart_interpolation);
        }
    }
    
    formatted_url
}

pub fn generate(input_path: &str, template_path: &str, output_path: &str, target: &str) -> anyhow::Result<()> {
    let file = File::open(input_path)?;
    let openapi: OpenAPI = serde_json::from_reader(file)?;

    let type_mapper = get_type_mapper(template_path);
    let mut spec = ApiSpec { 
        target: target.to_string(),
        endpoints: vec![] 
    };

    for (path, item) in openapi.paths.iter() {
        for (method, op) in [
            ("get", &item.get),
            ("post", &item.post),
            ("put", &item.put),
            ("delete", &item.delete),
            ("patch", &item.patch),
        ] {
            if let Some(op) = op {
                let mut params = vec![];
                let mut query_params = vec![];
                if let Some(parameters) = &op.parameters {
                    for p in parameters {
                        let ty = type_mapper(&p.schema.as_ref().and_then(|s| s.ty.clone()), &p.schema.as_ref().and_then(|s| s.format.clone()), &p.schema.as_ref().and_then(|s| s.items.clone()), &p.schema.as_ref().and_then(|s| s.ref_path.clone()));
                        let camel_param_name = to_camel_case(&p.name);
                        let param = Param {
                            name: camel_param_name,
                            original_name: p.name.clone(),
                            location: p.location.clone(),
                            required: p.required.unwrap_or(false),
                            ty: if p.required.unwrap_or(false) { ty.clone() } else { format!("{}?", ty) },
                            description: p.description.clone(),
                        };
                        if p.location == "query" {
                            query_params.push(param.clone());
                        }
                        params.push(param);
                    }
                }
                let body_param = op.request_body.as_ref().and_then(|body| {
                    body.content.as_ref().and_then(|content| {
                        content.get("application/json").and_then(|c| {
                            let opid = op.operation_id.as_deref().unwrap_or("Body");
                            let ty = type_mapper(&c.schema.as_ref().and_then(|s| s.ty.clone()), &c.schema.as_ref().and_then(|s| s.format.clone()), &c.schema.as_ref().and_then(|s| s.items.clone()), &c.schema.as_ref().and_then(|s| s.ref_path.clone()));
                            Some(BodyParam {
                                name: format!("{}Form", opid),
                                ty,
                                required: body.required.unwrap_or(true),
                                description: body.description.clone(),
                            })
                        })
                    })
                });
                let response_type = if let Some(responses) = &op.responses {
                    if let Some(resp) = responses.get("200") {
                        if let Some(content) = &resp.content {
                            if let Some(app_json) = content.get("application/json") {
                                if let Some(schema) = &app_json.schema {
                                    let ty = type_mapper(&schema.ty, &schema.format, &schema.items, &schema.ref_path);
                                    ty
                                } else {
                                    "String".to_string()
                                }
                            } else {
                                "String".to_string()
                            }
                        } else {
                            "String".to_string()
                        }
                    } else {
                        "String".to_string()
                    }
                } else {
                    "String".to_string()
                };
                let endpoint_name = op.operation_id.clone().unwrap_or_else(|| format!("{}_{}", method, path.replace("/", "_").replace("{", "").replace("}", "")));
                let path_segments = parse_path_segments(path);
                let formatted_url = format_url_with_params(path, &params);
                spec.endpoints.push(Endpoint {
                    name: endpoint_name.clone(),
                    camel_name: to_camel_case(&endpoint_name),
                    path: path.to_string(),
                    formatted_url,
                    path_segments,
                    method: method.to_string(),
                    params,
                    query_params,
                    body_param,
                    response_type,
                    description: op.description.clone(),
                });
            }
        }
    }

    let mut reg = Handlebars::new();
    reg.register_template_file("api", template_path)?;
    let rendered = reg.render("api", &spec)?;
    let mut file = File::create(output_path)?;
    file.write_all(rendered.as_bytes())?;
    println!("✅ Generated code written to {}", output_path);
    Ok(())
}