use crate::import::curl_import::MockForgeRoute as CurlRoute;
use crate::import::har_import::MockForgeRoute as HarRoute;
use crate::import::insomnia_import::MockForgeRoute as InsomniaRoute;
use crate::import::openapi_import::MockForgeRoute as OpenApiRoute;
use crate::import::postman_import::{
ImportResult as PostmanImportResult, MockForgeRoute as PostmanRoute,
};
use crate::routing::HttpMethod;
use crate::workspace::{MockRequest, MockResponse, Workspace, WorkspaceRegistry};
use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportRoute {
pub method: String,
pub path: String,
pub headers: HashMap<String, String>,
pub body: Option<String>,
pub response: ImportResponse,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportResponse {
pub status: u16,
pub headers: HashMap<String, String>,
pub body: Value,
}
#[derive(Debug, Clone)]
pub struct WorkspaceImportConfig {
pub create_folders: bool,
pub base_folder_name: Option<String>,
pub preserve_hierarchy: bool,
pub max_depth: usize,
}
impl Default for WorkspaceImportConfig {
fn default() -> Self {
Self {
create_folders: true,
base_folder_name: None,
preserve_hierarchy: true,
max_depth: 2,
}
}
}
#[derive(Debug)]
pub struct WorkspaceImportResult {
pub workspace: Workspace,
pub request_count: usize,
pub folder_count: usize,
pub warnings: Vec<String>,
}
fn postman_route_to_import_route(route: PostmanRoute) -> ImportRoute {
ImportRoute {
method: route.method,
path: route.path,
headers: route.headers,
body: route.body,
response: ImportResponse {
status: route.response.status,
headers: route.response.headers,
body: route.response.body,
},
}
}
fn insomnia_route_to_import_route(route: InsomniaRoute) -> ImportRoute {
ImportRoute {
method: route.method,
path: route.path,
headers: route.headers,
body: route.body,
response: ImportResponse {
status: route.response.status,
headers: route.response.headers,
body: route.response.body,
},
}
}
fn curl_route_to_import_route(route: CurlRoute) -> ImportRoute {
ImportRoute {
method: route.method,
path: route.path,
headers: route.headers,
body: route.body,
response: ImportResponse {
status: route.response.status,
headers: route.response.headers,
body: route.response.body,
},
}
}
fn openapi_route_to_import_route(route: OpenApiRoute) -> ImportRoute {
ImportRoute {
method: route.method,
path: route.path,
headers: route.headers,
body: route.body,
response: ImportResponse {
status: route.response.status,
headers: route.response.headers,
body: route.response.body,
},
}
}
fn har_route_to_import_route(route: HarRoute) -> ImportRoute {
ImportRoute {
method: route.method,
path: route.path,
headers: route.headers,
body: route.body,
response: ImportResponse {
status: route.response.status,
headers: route.response.headers,
body: route.response.body,
},
}
}
pub fn import_postman_to_workspace(
routes: Vec<ImportRoute>,
workspace_name: String,
config: WorkspaceImportConfig,
) -> Result<WorkspaceImportResult> {
let mut workspace = Workspace::new(workspace_name);
let warnings = Vec::new();
if config.create_folders {
let folder_groups = group_routes_by_folders(&routes, &config, |r| &r.path);
for (folder_path, folder_routes) in folder_groups {
let folder_id = create_folder_hierarchy(&mut workspace, &folder_path, &config)?;
add_routes_to_folder(&mut workspace, folder_id, folder_routes)?;
}
} else {
for route in &routes {
let request = convert_postman_route_to_request(route);
workspace.add_request(request)?;
}
}
let folder_count = if config.create_folders {
let folder_groups = group_routes_by_folders(&routes, &config, |r| &r.path);
count_folders(&folder_groups)
} else {
0
};
let result = WorkspaceImportResult {
workspace,
request_count: routes.len(),
folder_count,
warnings,
};
Ok(result)
}
pub fn import_postman_to_existing_workspace(
registry: &mut WorkspaceRegistry,
workspace_id: &str,
routes: Vec<ImportRoute>,
config: WorkspaceImportConfig,
) -> Result<WorkspaceImportResult> {
let workspace = registry
.get_workspace_mut(workspace_id)
.ok_or_else(|| Error::not_found("Workspace", workspace_id))?;
let warnings = Vec::new();
if config.create_folders {
let folder_groups = group_routes_by_folders(&routes, &config, |r| &r.path);
for (folder_path, folder_routes) in folder_groups {
let folder_id = create_folder_hierarchy(workspace, &folder_path, &config)?;
add_routes_to_folder(workspace, folder_id, folder_routes)?;
}
} else {
for route in &routes {
let request = convert_postman_route_to_request(route);
workspace.add_request(request)?;
}
}
let folder_count = if config.create_folders {
let folder_groups = group_routes_by_folders(&routes, &config, |r| &r.path);
count_folders(&folder_groups)
} else {
0
};
let result = WorkspaceImportResult {
workspace: workspace.clone(),
request_count: routes.len(),
folder_count,
warnings,
};
Ok(result)
}
fn group_routes_by_folders<'a, T>(
routes: &'a [T],
config: &'a WorkspaceImportConfig,
get_path: fn(&T) -> &str,
) -> HashMap<String, Vec<&'a T>> {
let mut groups = HashMap::new();
for route in routes {
let path = get_path(route);
let folder_path = determine_folder_path(path, config);
groups.entry(folder_path).or_insert_with(Vec::new).push(route);
}
groups
}
fn determine_folder_path(route_path: &str, config: &WorkspaceImportConfig) -> String {
if !config.preserve_hierarchy {
return config.base_folder_name.clone().unwrap_or_else(|| "Imported".to_string());
}
let segments: Vec<&str> = route_path
.trim_start_matches('/')
.split('/')
.filter(|s| !s.is_empty())
.collect();
if segments.is_empty() {
return config.base_folder_name.clone().unwrap_or_else(|| "Root".to_string());
}
let depth = std::cmp::min(config.max_depth, std::cmp::max(1, segments.len().saturating_sub(1)));
let folder_segments = &segments[..depth];
if folder_segments.is_empty() {
config.base_folder_name.clone().unwrap_or_else(|| "Root".to_string())
} else {
folder_segments.join("/")
}
}
fn create_folder_hierarchy(
workspace: &mut Workspace,
folder_path: &str,
_config: &WorkspaceImportConfig,
) -> Result<String> {
if folder_path == "Root" || folder_path.is_empty() {
return Ok("root".to_string());
}
let segments: Vec<&str> = folder_path.split('/').collect();
let mut current_parent: Option<String> = None;
for segment in segments {
let folder_name = segment.to_string();
let existing_folder = if let Some(parent_id) = ¤t_parent {
workspace
.find_folder(parent_id)
.and_then(|parent| parent.folders.iter().find(|f| f.name == folder_name))
} else {
workspace.folders.iter().find(|f| f.name == folder_name)
};
let folder_id = if let Some(existing) = existing_folder {
existing.id.clone()
} else {
if let Some(parent_id) = ¤t_parent {
let parent = workspace
.find_folder_mut(parent_id)
.ok_or_else(|| Error::not_found("Parent folder", parent_id))?;
parent.add_folder(folder_name)?
} else {
workspace.add_folder(folder_name)?
}
};
current_parent = Some(folder_id);
}
current_parent.ok_or_else(|| Error::internal("Failed to create folder hierarchy"))
}
fn add_routes_to_folder(
workspace: &mut Workspace,
folder_id: String,
routes: Vec<&ImportRoute>,
) -> Result<()> {
if folder_id == "root" {
for route in &routes {
let request = convert_postman_route_to_request(route);
workspace.add_request(request)?;
}
} else {
let folder = workspace
.find_folder_mut(&folder_id)
.ok_or_else(|| Error::not_found("Folder", &folder_id))?;
for route in &routes {
let request = convert_postman_route_to_request(route);
folder.add_request(request)?;
}
}
Ok(())
}
fn convert_postman_route_to_request(route: &ImportRoute) -> MockRequest {
let method = match route.method.to_uppercase().as_str() {
"GET" => HttpMethod::GET,
"POST" => HttpMethod::POST,
"PUT" => HttpMethod::PUT,
"DELETE" => HttpMethod::DELETE,
"PATCH" => HttpMethod::PATCH,
"HEAD" => HttpMethod::HEAD,
"OPTIONS" => HttpMethod::OPTIONS,
_ => HttpMethod::GET, };
let mut request =
MockRequest::new(method, route.path.clone(), format!("Imported: {}", route.path));
let mut response = MockResponse::default();
response.status_code = route.response.status;
for (key, value) in &route.response.headers {
response.headers.insert(key.clone(), value.clone());
}
if let Some(body_value) = route.response.body.as_str() {
response.body = Some(body_value.to_string());
} else {
response.body = Some("{}".to_string());
}
request.response = response;
request.tags.push("imported".to_string());
request
}
fn count_folders<T>(groups: &HashMap<String, Vec<T>>) -> usize {
groups.keys().filter(|k| *k != "Root" && !k.is_empty()).count()
}
pub fn create_workspace_from_postman(
import_result: PostmanImportResult,
workspace_name: Option<String>,
) -> Result<WorkspaceImportResult> {
let name = workspace_name.unwrap_or_else(|| "Postman Import".to_string());
let config = WorkspaceImportConfig::default();
let routes: Vec<ImportRoute> =
import_result.routes.into_iter().map(postman_route_to_import_route).collect();
import_postman_to_workspace(routes, name, config)
}
pub fn create_workspace_from_insomnia(
import_result: crate::import::InsomniaImportResult,
workspace_name: Option<String>,
) -> Result<WorkspaceImportResult> {
let name = workspace_name.unwrap_or_else(|| "Insomnia Import".to_string());
let config = WorkspaceImportConfig::default();
let routes: Vec<ImportRoute> =
import_result.routes.into_iter().map(insomnia_route_to_import_route).collect();
import_postman_to_workspace(routes, name, config)
}
pub fn create_workspace_from_curl(
import_result: crate::import::CurlImportResult,
workspace_name: Option<String>,
) -> Result<WorkspaceImportResult> {
let name = workspace_name.unwrap_or_else(|| "Curl Import".to_string());
let config = WorkspaceImportConfig {
create_folders: false, ..Default::default()
};
let routes: Vec<ImportRoute> =
import_result.routes.into_iter().map(curl_route_to_import_route).collect();
import_postman_to_workspace(routes, name, config)
}
pub fn create_workspace_from_openapi(
import_result: crate::import::OpenApiImportResult,
workspace_name: Option<String>,
) -> Result<WorkspaceImportResult> {
let name = workspace_name.unwrap_or_else(|| "OpenAPI Import".to_string());
let config = WorkspaceImportConfig::default();
let routes: Vec<ImportRoute> =
import_result.routes.into_iter().map(openapi_route_to_import_route).collect();
import_postman_to_workspace(routes, name, config)
}
pub fn create_workspace_from_har(
import_result: crate::import::HarImportResult,
workspace_name: Option<String>,
) -> Result<WorkspaceImportResult> {
let name = workspace_name.unwrap_or_else(|| "HAR Import".to_string());
let config = WorkspaceImportConfig::default();
let routes: Vec<ImportRoute> =
import_result.routes.into_iter().map(har_route_to_import_route).collect();
import_postman_to_workspace(routes, name, config)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_folder_path_determination() {
let config = WorkspaceImportConfig::default();
assert_eq!(determine_folder_path("/api/users", &config), "api");
assert_eq!(determine_folder_path("/api/v1/users/profile", &config), "api/v1");
assert_eq!(determine_folder_path("/", &config), "Root");
let mut limited_config = config.clone();
limited_config.max_depth = 1;
assert_eq!(determine_folder_path("/api/v1/users/profile", &limited_config), "api");
}
#[test]
fn test_workspace_creation() {
let routes = vec![
ImportRoute {
method: "GET".to_string(),
path: "/api/users".to_string(),
headers: HashMap::new(),
body: None,
response: ImportResponse {
status: 200,
headers: HashMap::new(),
body: serde_json::json!({"users": []}),
},
},
ImportRoute {
method: "POST".to_string(),
path: "/api/users".to_string(),
headers: HashMap::new(),
body: None,
response: ImportResponse {
status: 201,
headers: HashMap::new(),
body: serde_json::json!({"id": 1}),
},
},
];
let config = WorkspaceImportConfig::default();
let result =
import_postman_to_workspace(routes, "Test Workspace".to_string(), config).unwrap();
assert_eq!(result.workspace.name, "Test Workspace");
assert_eq!(result.request_count, 2);
assert!(result.folder_count > 0);
}
}