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, 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, pub location: String, 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 {
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 }
}
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, ¶ms);
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(())
}