use crate::cli::args::Method;
use crate::core::collection::{
Auth, Collection, CollectionItem, Folder, KVParam, Request, RequestBody,
};
use crate::core::parser::SourceParser;
use crate::core::parser::models::{FieldType, Model, ModelField, ModelRegistry};
use regex::Regex;
use std::path::Path;
use walkdir::WalkDir;
pub struct SpringParser;
impl SpringParser {
fn parse_java_type(type_str: &str) -> FieldType {
let type_str = type_str.trim();
if type_str.is_empty() {
return FieldType::Unknown;
}
match type_str.to_lowercase().as_str() {
"string" => FieldType::String,
"int" | "integer" | "long" | "double" | "float" | "bigdecimal" => FieldType::Number,
"boolean" | "bool" => FieldType::Boolean,
"localdate" | "localdatetime" | "instant" | "zodatetime" | "date" => {
FieldType::DateTime
}
t if t.contains("map") => {
FieldType::Map(Box::new(FieldType::String), Box::new(FieldType::Unknown))
}
t if t.contains("list") || t.contains("set") || t.contains("iterable") => {
if let Some(start) = t.find('<') {
if let Some(end) = t.rfind('>') {
let inner = &t[start + 1..end];
return FieldType::Array(Box::new(Self::parse_java_type(inner)));
}
}
FieldType::Array(Box::new(FieldType::Unknown))
}
_ => FieldType::Object(type_str.to_string()),
}
}
}
impl SourceParser for SpringParser {
fn parse(&self, project_path: &Path) -> anyhow::Result<Collection> {
let mut collection = Collection::new(format!(
"{} (Spring Boot)",
project_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
));
collection.env_vars.push(KVParam {
key: "baseUrl".to_string(),
value: "http://localhost:8080".to_string(),
enabled: true,
description: Some("Base URL for the service".to_string()),
});
let mut registry = ModelRegistry::new();
let class_regex =
Regex::new(r"(?m)^(?:public\s+)?(?:class|record)\s+([a-zA-Z0-9_]+)").unwrap();
let field_regex = Regex::new(
r"^\s+(?:private|public|protected)?\s+([a-zA-Z0-9_<>\?]+)\s+([a-zA-Z0-9_]+)\s*(?:;|=)",
)
.unwrap();
for entry in WalkDir::new(project_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.map_or(false, |ext| ext == "java" || ext == "kt")
})
{
let path_str = entry.path().to_string_lossy();
if path_str.contains("target") || path_str.contains(".git") {
continue;
}
if let Ok(content) = std::fs::read_to_string(entry.path()) {
let lines: Vec<&str> = content.lines().collect();
let mut i = 0;
while i < lines.len() {
if let Some(cap) = class_regex.captures(lines[i]) {
let name = cap[1].to_string();
let mut fields = Vec::new();
i += 1;
let mut brace_count = if lines[i - 1].contains('{') { 1 } else { 0 };
while i < lines.len() {
if lines[i].contains('{') {
brace_count += 1;
}
if lines[i].contains('}') {
brace_count -= 1;
}
if let Some(fcap) = field_regex.captures(lines[i]) {
fields.push(ModelField {
name: fcap[2].to_string(),
field_type: Self::parse_java_type(&fcap[1]),
});
}
if brace_count == 0 && lines[i].contains('}') {
break;
}
i += 1;
}
registry.add_model(Model { name, fields });
} else {
i += 1;
}
}
}
}
let mapping_regex = Regex::new(r#"@(Get|Post|Put|Delete|Patch)Mapping(\s*\([^)]*\))?"#).unwrap();
let request_mapping_regex = Regex::new(r#"@RequestMapping(\s*\([^)]*\))?"#).unwrap();
let path_value_regex = Regex::new(r#"['"]([^'"]*)['"]"#).unwrap();
let method_value_regex =
Regex::new(r#"method\s*=\s*RequestMethod\.(GET|POST|PUT|DELETE|PATCH)"#).unwrap();
let request_body_regex =
Regex::new(r"@RequestBody\s+([a-zA-Z0-9_<>]+)\s+([a-zA-Z0-9_]+)").unwrap();
for entry in WalkDir::new(project_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.map_or(false, |ext| ext == "java" || ext == "kt")
})
{
let path_str = entry.path().to_string_lossy();
if path_str.contains("target") || path_str.contains(".git") {
continue;
}
if let Ok(content) = std::fs::read_to_string(entry.path()) {
if content.contains("@FeignClient") {
continue;
}
if !content.contains("@RestController")
&& !content.contains("@Controller")
&& !content.contains("@RequestMapping")
{
continue;
}
let class_mapping_regex = Regex::new(r#"(?m)@RequestMapping\s*\(\s*(?:(?:value|path)\s*=\s*)?['"]([^'"]+)['"]\s*\)(?:[\s\S]*?)(?:class|record)"#).unwrap();
let class_prefix = class_mapping_regex
.captures(&content)
.map(|c| c[1].to_string())
.unwrap_or_default();
let mut requests = Vec::new();
let find_body = |pos: usize| -> RequestBody {
let slice_end = std::cmp::min(content.len(), pos + 500);
if let Some(bcap) = request_body_regex.captures(&content[pos..slice_end]) {
let type_name = &bcap[1];
if let Some(json_body) = registry.generate_json(type_name) {
return RequestBody::raw(json_body, "application/json".to_string());
}
}
RequestBody::default()
};
for cap in mapping_regex.captures_iter(&content) {
let method_prefix = &cap[1];
let mut url_path = "";
if let Some(parens) = cap.get(2) {
if let Some(pcap) = path_value_regex.captures(parens.as_str()) {
url_path = pcap.get(1).map(|m| m.as_str()).unwrap_or("");
}
}
let method = match method_prefix.to_lowercase().as_str() {
"post" => Method::Post,
"put" => Method::Put,
"patch" => Method::Patch,
"delete" => Method::Delete,
_ => Method::Get,
};
let body = find_body(cap.get(0).unwrap().end());
let full_path = if url_path.is_empty() {
class_prefix.clone()
} else {
format!(
"{}/{}",
class_prefix.trim_end_matches('/'),
url_path.trim_start_matches('/')
)
};
let full_path = if full_path.is_empty() {
String::new()
} else if full_path.starts_with('/') {
full_path
} else {
format!("/{}", full_path)
};
requests.push(CollectionItem::Request(Request {
id: uuid::Uuid::new_v4().to_string(),
name: format!("{} {}", method_prefix.to_uppercase(), full_path),
method,
url: format!("{{{{baseUrl}}}}{}", full_path),
params: Vec::new(),
headers: Vec::new(),
auth: Auth::default(),
body,
pre_request_script: None,
post_response_script: None,
}));
}
for cap in request_mapping_regex.captures_iter(&content) {
let mut url_path = "";
let mut method_str = "GET";
if let Some(parens) = cap.get(1) {
let parens_str = parens.as_str();
if let Some(pcap) = path_value_regex.captures(parens_str) {
url_path = pcap.get(1).map(|m| m.as_str()).unwrap_or("");
}
if let Some(mcap) = method_value_regex.captures(parens_str) {
method_str = mcap.get(1).map(|m| m.as_str()).unwrap_or("GET");
}
}
let match_end = cap.get(0).unwrap().end();
let context_after =
&content[match_end..std::cmp::min(content.len(), match_end + 50)];
if context_after.contains("class") || context_after.contains("record") {
continue;
}
let method = match method_str.to_uppercase().as_str() {
"POST" => Method::Post,
"PUT" => Method::Put,
"PATCH" => Method::Patch,
"DELETE" => Method::Delete,
_ => Method::Get,
};
let body = find_body(match_end);
let full_path = if url_path.is_empty() {
class_prefix.clone()
} else {
format!(
"{}/{}",
class_prefix.trim_end_matches('/'),
url_path.trim_start_matches('/')
)
};
let full_path = if full_path.is_empty() {
String::new()
} else if full_path.starts_with('/') {
full_path
} else {
format!("/{}", full_path)
};
requests.push(CollectionItem::Request(Request {
id: uuid::Uuid::new_v4().to_string(),
name: format!("{} {}", method_str.to_uppercase(), full_path),
method,
url: format!("{{{{baseUrl}}}}{}", full_path),
params: Vec::new(),
headers: Vec::new(),
auth: Auth::default(),
body,
pre_request_script: None,
post_response_script: None,
}));
}
if !requests.is_empty() {
let file_name = entry
.path()
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
let mut folder = Folder::new(file_name);
folder.items = requests;
collection.items.push(CollectionItem::Folder(folder));
}
}
}
Ok(collection)
}
}