use crate::config::AuthConfig;
use crate::encryption::AutoEncryptionConfig;
use crate::{routing::HttpMethod, Error, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
pub type EntityId = String;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workspace {
pub id: EntityId,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub tags: Vec<String>,
pub config: WorkspaceConfig,
pub folders: Vec<Folder>,
pub requests: Vec<MockRequest>,
#[serde(default)]
pub order: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FolderInheritanceConfig {
#[serde(default)]
pub headers: HashMap<String, String>,
pub auth: Option<AuthConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Folder {
pub id: EntityId,
pub name: String,
pub description: Option<String>,
pub parent_id: Option<EntityId>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub folders: Vec<Folder>,
pub requests: Vec<MockRequest>,
pub inheritance: FolderInheritanceConfig,
#[serde(default)]
pub order: i32,
#[serde(default = "default_true")]
pub expanded: bool,
#[serde(default)]
pub collapsed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockRequest {
pub id: EntityId,
pub name: String,
pub method: HttpMethod,
pub url: String,
pub description: Option<String>,
pub folder_id: Option<EntityId>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub query_params: HashMap<String, String>,
pub body: Option<String>,
pub auth: Option<AuthConfig>,
pub responses: Vec<MockResponse>,
#[serde(default)]
pub order: i32,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default = "default_true")]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MockResponse {
pub id: EntityId,
pub status_code: u16,
pub name: String,
pub body: String,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default)]
pub delay: u64,
#[serde(default)]
pub active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub history: Vec<ResponseHistoryEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub intelligent: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub drift: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResponseHistoryEntry {
pub timestamp: DateTime<Utc>,
pub request_id: EntityId,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentColor {
pub red: u8,
pub green: u8,
pub blue: u8,
#[serde(default = "default_alpha")]
pub alpha: u8,
}
impl EnvironmentColor {
pub fn new(red: u8, green: u8, blue: u8) -> Self {
Self {
red,
green,
blue,
alpha: 255,
}
}
pub fn from_hex(hex: &str) -> Result<Self> {
let hex = hex.trim_start_matches('#');
if hex.len() != 6 {
return Err(Error::internal("Invalid hex color format"));
}
let red = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| Error::internal("Invalid hex color format"))?;
let green = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| Error::internal("Invalid hex color format"))?;
let blue = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| Error::internal("Invalid hex color format"))?;
Ok(Self::new(red, green, blue))
}
pub fn to_hex(&self) -> String {
format!("#{:02x}{:02x}{:02x}", self.red, self.green, self.blue)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Environment {
pub id: EntityId,
pub name: String,
pub variables: HashMap<String, String>,
pub color: EnvironmentColor,
#[serde(default)]
pub active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkspaceConfig {
pub base_url: Option<String>,
pub auth: Option<AuthConfig>,
#[serde(default)]
pub headers: HashMap<String, String>,
#[serde(default = "default_timeout")]
pub timeout_seconds: u64,
#[serde(default = "default_true")]
pub ssl_verify: bool,
pub proxy: Option<String>,
#[serde(default)]
pub auto_encryption: AutoEncryptionConfig,
#[serde(default)]
pub reality_level: Option<crate::RealityLevel>,
#[serde(default)]
pub ai_mode: Option<crate::ai_studio::config::AiMode>,
}
fn default_timeout() -> u64 {
30
}
fn default_true() -> bool {
true
}
fn default_alpha() -> u8 {
255
}
impl Default for WorkspaceConfig {
fn default() -> Self {
Self {
base_url: None,
auth: None,
headers: HashMap::new(),
timeout_seconds: default_timeout(),
ssl_verify: default_true(),
proxy: None,
auto_encryption: AutoEncryptionConfig::default(),
reality_level: None,
ai_mode: None, }
}
}
impl Workspace {
pub fn new(name: String) -> Self {
let now = Utc::now();
let id = Uuid::new_v4().to_string();
Self {
id,
name,
description: None,
created_at: now,
updated_at: now,
tags: Vec::new(),
config: WorkspaceConfig::default(),
folders: Vec::new(),
requests: Vec::new(),
order: 0,
}
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn add_folder(&mut self, mut folder: Folder) {
folder.parent_id = None;
folder.touch();
self.folders.push(folder);
self.touch();
}
pub fn add_request(&mut self, mut request: MockRequest) {
request.folder_id = None;
request.touch();
self.requests.push(request);
self.touch();
}
}
impl Folder {
pub fn new(name: String) -> Self {
let now = Utc::now();
let id = Uuid::new_v4().to_string();
Self {
id,
name,
description: None,
parent_id: None,
created_at: now,
updated_at: now,
folders: Vec::new(),
requests: Vec::new(),
inheritance: FolderInheritanceConfig {
headers: HashMap::new(),
auth: None,
},
order: 0,
expanded: true,
collapsed: false,
}
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn add_folder(&mut self, mut folder: Folder) {
folder.parent_id = Some(self.id.clone());
folder.touch();
self.folders.push(folder);
self.touch();
}
pub fn add_request(&mut self, mut request: MockRequest) {
request.folder_id = Some(self.id.clone());
request.touch();
self.requests.push(request);
self.touch();
}
pub fn get_inherited_headers(&self, all_folders: &[Folder]) -> HashMap<String, String> {
let mut headers = HashMap::new();
if let Some(parent_id) = &self.parent_id {
if let Some(parent) = all_folders.iter().find(|f| f.id == *parent_id) {
headers = parent.get_inherited_headers(all_folders);
}
}
for (key, value) in &self.inheritance.headers {
headers.insert(key.clone(), value.clone());
}
headers
}
}
impl MockRequest {
pub fn new(name: String, method: HttpMethod, url: String) -> Self {
let now = Utc::now();
let id = Uuid::new_v4().to_string();
Self {
id,
name,
method,
url,
description: None,
folder_id: None,
created_at: now,
updated_at: now,
headers: HashMap::new(),
query_params: HashMap::new(),
body: None,
auth: None,
responses: Vec::new(),
order: 0,
tags: Vec::new(),
enabled: true,
}
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn add_response(&mut self, mut response: MockResponse) {
response.active = self.responses.is_empty(); response.touch();
self.responses.push(response);
self.touch();
}
pub fn active_response(&self) -> Option<&MockResponse> {
self.responses.iter().find(|r| r.active)
}
pub fn active_response_mut(&mut self) -> Option<&mut MockResponse> {
self.responses.iter_mut().find(|r| r.active)
}
}
impl MockResponse {
pub fn new(status_code: u16, name: String, body: String) -> Self {
let now = Utc::now();
let id = Uuid::new_v4().to_string();
Self {
id,
status_code,
name,
body,
headers: HashMap::new(),
delay: 0,
active: false,
created_at: now,
updated_at: now,
history: Vec::new(),
intelligent: None,
drift: None,
}
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn record_usage(&mut self, request_id: EntityId, duration_ms: u64) {
self.history.push(ResponseHistoryEntry {
timestamp: Utc::now(),
request_id,
duration_ms,
});
self.touch();
}
}
impl Environment {
pub fn new(name: String) -> Self {
let now = Utc::now();
let id = Uuid::new_v4().to_string();
Self {
id,
name,
variables: HashMap::new(),
color: EnvironmentColor::new(64, 128, 255), active: false,
created_at: now,
updated_at: now,
}
}
pub fn touch(&mut self) {
self.updated_at = Utc::now();
}
pub fn set_variable(&mut self, key: String, value: String) {
self.variables.insert(key, value);
self.touch();
}
pub fn remove_variable(&mut self, key: &str) -> Option<String> {
let result = self.variables.remove(key);
if result.is_some() {
self.touch();
}
result
}
pub fn get_variable(&self, key: &str) -> Option<&String> {
self.variables.get(key)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::routing::HttpMethod;
#[test]
fn test_environment_color_new() {
let color = EnvironmentColor::new(255, 128, 64);
assert_eq!(color.red, 255);
assert_eq!(color.green, 128);
assert_eq!(color.blue, 64);
assert_eq!(color.alpha, 255);
}
#[test]
fn test_environment_color_from_hex() {
let color = EnvironmentColor::from_hex("#ff8040").unwrap();
assert_eq!(color.red, 255);
assert_eq!(color.green, 128);
assert_eq!(color.blue, 64);
}
#[test]
fn test_environment_color_from_hex_with_hash() {
let color = EnvironmentColor::from_hex("#00ff00").unwrap();
assert_eq!(color.red, 0);
assert_eq!(color.green, 255);
assert_eq!(color.blue, 0);
}
#[test]
fn test_environment_color_from_hex_invalid_length() {
assert!(EnvironmentColor::from_hex("ff80").is_err());
assert!(EnvironmentColor::from_hex("ff8040ff").is_err());
}
#[test]
fn test_environment_color_from_hex_invalid_chars() {
assert!(EnvironmentColor::from_hex("#gggggg").is_err());
}
#[test]
fn test_environment_color_to_hex() {
let color = EnvironmentColor::new(255, 128, 64);
assert_eq!(color.to_hex(), "#ff8040");
}
#[test]
fn test_workspace_new() {
let workspace = Workspace::new("Test Workspace".to_string());
assert_eq!(workspace.name, "Test Workspace");
assert!(!workspace.id.is_empty());
assert!(workspace.folders.is_empty());
assert!(workspace.requests.is_empty());
}
#[test]
fn test_workspace_touch() {
let mut workspace = Workspace::new("Test".to_string());
let old_updated = workspace.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
workspace.touch();
assert!(workspace.updated_at > old_updated);
}
#[test]
fn test_workspace_add_folder() {
let mut workspace = Workspace::new("Test".to_string());
let folder = Folder::new("Test Folder".to_string());
workspace.add_folder(folder);
assert_eq!(workspace.folders.len(), 1);
assert_eq!(workspace.folders[0].name, "Test Folder");
}
#[test]
fn test_workspace_add_request() {
let mut workspace = Workspace::new("Test".to_string());
let request =
MockRequest::new("Test Request".to_string(), HttpMethod::GET, "/api/test".to_string());
workspace.add_request(request);
assert_eq!(workspace.requests.len(), 1);
assert_eq!(workspace.requests[0].name, "Test Request");
}
#[test]
fn test_folder_new() {
let folder = Folder::new("Test Folder".to_string());
assert_eq!(folder.name, "Test Folder");
assert!(!folder.id.is_empty());
assert!(folder.folders.is_empty());
assert!(folder.requests.is_empty());
}
#[test]
fn test_folder_touch() {
let mut folder = Folder::new("Test".to_string());
let old_updated = folder.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
folder.touch();
assert!(folder.updated_at > old_updated);
}
#[test]
fn test_folder_add_folder() {
let mut parent = Folder::new("Parent".to_string());
let child = Folder::new("Child".to_string());
parent.add_folder(child);
assert_eq!(parent.folders.len(), 1);
assert_eq!(parent.folders[0].name, "Child");
}
#[test]
fn test_folder_add_request() {
let mut folder = Folder::new("Test".to_string());
let request =
MockRequest::new("Test Request".to_string(), HttpMethod::POST, "/api/test".to_string());
folder.add_request(request);
assert_eq!(folder.requests.len(), 1);
assert_eq!(folder.requests[0].name, "Test Request");
}
#[test]
fn test_folder_get_inherited_headers() {
let mut parent = Folder::new("Parent".to_string());
parent.inheritance.headers.insert("X-Parent".to_string(), "value1".to_string());
let mut child = Folder::new("Child".to_string());
child.parent_id = Some(parent.id.clone());
child.inheritance.headers.insert("X-Child".to_string(), "value2".to_string());
let all_folders = vec![parent.clone(), child.clone()];
let headers = child.get_inherited_headers(&all_folders);
assert_eq!(headers.get("X-Parent"), Some(&"value1".to_string()));
assert_eq!(headers.get("X-Child"), Some(&"value2".to_string()));
}
#[test]
fn test_mock_request_new() {
let request =
MockRequest::new("Test Request".to_string(), HttpMethod::GET, "/api/test".to_string());
assert_eq!(request.name, "Test Request");
assert_eq!(request.method, HttpMethod::GET);
assert_eq!(request.url, "/api/test");
assert!(request.responses.is_empty());
}
#[test]
fn test_mock_request_touch() {
let mut request =
MockRequest::new("Test".to_string(), HttpMethod::GET, "/api/test".to_string());
let old_updated = request.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
request.touch();
assert!(request.updated_at > old_updated);
}
#[test]
fn test_mock_request_add_response() {
let mut request =
MockRequest::new("Test".to_string(), HttpMethod::GET, "/api/test".to_string());
let response =
MockResponse::new(200, "Success".to_string(), r#"{"status": "ok"}"#.to_string());
request.add_response(response);
assert_eq!(request.responses.len(), 1);
assert_eq!(request.responses[0].status_code, 200);
}
#[test]
fn test_mock_request_active_response() {
let mut request =
MockRequest::new("Test".to_string(), HttpMethod::GET, "/api/test".to_string());
let mut response1 = MockResponse::new(200, "Success".to_string(), "ok".to_string());
response1.active = false;
let mut response2 =
MockResponse::new(404, "Not Found".to_string(), "not found".to_string());
response2.active = true;
request.add_response(response1);
request.add_response(response2);
let active = request.active_response();
assert!(active.is_some());
let status = active.unwrap().status_code;
assert!(status == 200 || status == 404);
}
#[test]
fn test_mock_request_active_response_mut() {
let mut request =
MockRequest::new("Test".to_string(), HttpMethod::GET, "/api/test".to_string());
let mut response = MockResponse::new(200, "Success".to_string(), "ok".to_string());
response.active = true;
request.add_response(response);
let active = request.active_response_mut();
assert!(active.is_some());
active.unwrap().status_code = 201;
assert_eq!(request.responses[0].status_code, 201);
}
#[test]
fn test_mock_response_new() {
let response =
MockResponse::new(200, "Success".to_string(), r#"{"data": "test"}"#.to_string());
assert_eq!(response.status_code, 200);
assert_eq!(response.name, "Success");
assert_eq!(response.body, r#"{"data": "test"}"#);
assert!(!response.id.is_empty());
}
#[test]
fn test_mock_response_touch() {
let mut response = MockResponse::new(200, "Test".to_string(), "body".to_string());
let old_updated = response.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
response.touch();
assert!(response.updated_at > old_updated);
}
#[test]
fn test_mock_response_record_usage() {
let mut response = MockResponse::new(200, "Test".to_string(), "body".to_string());
let request_id = "req-123".to_string();
response.record_usage(request_id.clone(), 150);
assert_eq!(response.history.len(), 1);
assert_eq!(response.history[0].request_id, request_id);
assert_eq!(response.history[0].duration_ms, 150);
}
#[test]
fn test_environment_new() {
let env = Environment::new("Production".to_string());
assert_eq!(env.name, "Production");
assert!(!env.id.is_empty());
assert!(env.variables.is_empty());
assert!(!env.active);
}
#[test]
fn test_environment_touch() {
let mut env = Environment::new("Test".to_string());
let old_updated = env.updated_at;
std::thread::sleep(std::time::Duration::from_millis(10));
env.touch();
assert!(env.updated_at > old_updated);
}
#[test]
fn test_environment_set_variable() {
let mut env = Environment::new("Test".to_string());
env.set_variable("API_KEY".to_string(), "secret123".to_string());
assert_eq!(env.variables.get("API_KEY"), Some(&"secret123".to_string()));
}
#[test]
fn test_environment_remove_variable() {
let mut env = Environment::new("Test".to_string());
env.set_variable("KEY".to_string(), "value".to_string());
let removed = env.remove_variable("KEY");
assert_eq!(removed, Some("value".to_string()));
assert!(env.variables.is_empty());
}
#[test]
fn test_environment_remove_nonexistent_variable() {
let mut env = Environment::new("Test".to_string());
let removed = env.remove_variable("NONEXISTENT");
assert!(removed.is_none());
}
#[test]
fn test_environment_get_variable() {
let mut env = Environment::new("Test".to_string());
env.set_variable("API_URL".to_string(), "https://api.example.com".to_string());
let value = env.get_variable("API_URL");
assert_eq!(value, Some(&"https://api.example.com".to_string()));
}
#[test]
fn test_environment_get_nonexistent_variable() {
let env = Environment::new("Test".to_string());
let value = env.get_variable("NONEXISTENT");
assert!(value.is_none());
}
#[test]
fn test_workspace_config_default() {
let config = WorkspaceConfig::default();
assert!(config.base_url.is_none());
}
#[test]
fn test_folder_inheritance_config_creation() {
let mut headers = HashMap::new();
headers.insert("X-Custom".to_string(), "value".to_string());
let config = FolderInheritanceConfig {
headers,
auth: None,
};
assert_eq!(config.headers.len(), 1);
assert!(config.auth.is_none());
}
}