use crate::core::constants::SCHEMA_FILE;
use crate::core::models::{FieldDefinition, SchemaFile};
use crate::core::persistence;
use anyhow::Result;
pub fn add_field(field: FieldDefinition) -> Result<()> {
let valid_types = ["string", "integer", "boolean"];
if !valid_types.contains(&field.r#type.as_str()) {
return Err(anyhow::anyhow!(
"Invalid field type '{}'. Valid types are: {}",
field.r#type,
valid_types.join(", ")
));
}
let key = field.key.clone();
persistence::atomic_update_json(SCHEMA_FILE, |schema: &mut SchemaFile| {
if schema.fields.iter().any(|f| f.key == key) {
return Err(persistence::PersistenceError::ValidationError(format!(
"Field '{}' already exists in schema",
key
)));
}
schema.fields.push(field);
Ok(())
})?;
println!("Field '{}' added to schema.", key);
Ok(())
}
pub fn remove_field(key: &str) -> Result<()> {
persistence::atomic_update_json(SCHEMA_FILE, |schema: &mut SchemaFile| {
let initial_len = schema.fields.len();
schema.fields.retain(|f| f.key != key);
if schema.fields.len() == initial_len {
return Err(persistence::PersistenceError::IoError {
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Field '{}' not found in schema", key),
),
});
}
Ok(())
})?;
println!("Field '{}' removed from schema.", key);
Ok(())
}
pub fn update_field(key: &str, updated_field: FieldDefinition) -> Result<()> {
persistence::atomic_update_json(SCHEMA_FILE, |schema: &mut SchemaFile| {
if let Some(field) = schema.fields.iter_mut().find(|f| f.key == key) {
*field = updated_field;
Ok(())
} else {
Err(persistence::PersistenceError::IoError {
source: std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Field '{}' not found in schema", key),
),
})
}
})?;
println!("Field '{}' updated in schema.", key);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::path::Path;
use tempfile::TempDir;
struct TestDirGuard {
original_dir: std::path::PathBuf,
}
impl TestDirGuard {
fn new(temp_path: &Path) -> Self {
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_path).unwrap();
Self { original_dir }
}
}
impl Drop for TestDirGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.original_dir);
}
}
#[test]
#[serial]
fn test_schema_operations() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
add_field(FieldDefinition {
key: "test_key".to_string(),
r#type: "string".to_string(),
description: Some("desc".to_string()),
validation: None,
is_secret: false,
})
.unwrap();
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
assert_eq!(schema.fields.len(), 1);
assert_eq!(schema.fields[0].key, "test_key");
update_field(
"test_key",
FieldDefinition {
key: "test_key".to_string(),
r#type: "integer".to_string(),
description: Some("updated desc".to_string()),
validation: None,
is_secret: false,
},
)
.unwrap();
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
assert_eq!(schema.fields[0].r#type, "integer");
assert_eq!(
schema.fields[0].description,
Some("updated desc".to_string())
);
remove_field("test_key").unwrap();
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
assert_eq!(schema.fields.len(), 0);
}
#[test]
#[serial]
fn test_add_field_with_unicode_key() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
let result = add_field(FieldDefinition {
key: "🚀_key".to_string(), r#type: "string".to_string(),
description: Some("Unicode key test".to_string()),
validation: None,
is_secret: false,
});
assert!(result.is_ok());
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
assert!(schema.fields.iter().any(|f| f.key == "🚀_key"));
}
#[test]
#[serial]
fn test_add_field_with_duplicate_key() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
add_field(FieldDefinition {
key: "duplicate_key".to_string(),
r#type: "string".to_string(),
description: Some("first".to_string()),
validation: None,
is_secret: false,
})
.unwrap();
let result = add_field(FieldDefinition {
key: "duplicate_key".to_string(),
r#type: "integer".to_string(),
description: Some("second".to_string()),
validation: None,
is_secret: true,
});
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[test]
#[serial]
fn test_remove_nonexistent_field() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
let result = remove_field("nonexistent_key");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
#[serial]
fn test_update_nonexistent_field() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
if persistence::init_project().is_err() {
return;
}
let result = update_field(
"nonexistent_key",
FieldDefinition {
key: "nonexistent_key".to_string(),
r#type: "string".to_string(),
description: Some("updated".to_string()),
validation: None,
is_secret: false,
},
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not found"));
}
#[test]
#[serial]
fn test_schema_with_many_fields() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
if persistence::init_project().is_err() {
return;
}
for i in 0..100 {
if add_field(FieldDefinition {
key: format!("field_{}", i),
r#type: "string".to_string(),
description: Some(format!("Description for field {}", i)),
validation: None,
is_secret: i % 2 == 0, })
.is_err()
{
return;
}
}
let schema: SchemaFile =
match persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()) {
Ok(s) => s,
Err(_) => return,
};
assert_eq!(schema.fields.len(), 100);
for i in 0..100 {
assert!(schema
.fields
.iter()
.any(|f| f.key == format!("field_{}", i)));
}
}
#[test]
#[serial]
fn test_schema_field_with_complex_validation() {
use crate::core::models::ValidationRules;
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
let field_with_validation = FieldDefinition {
key: "validated_field".to_string(),
r#type: "string".to_string(),
description: Some("A field with validation".to_string()),
validation: Some(ValidationRules {
min_length: Some(5),
max_length: Some(20),
min_value: None,
max_value: None,
pattern: Some(r"^[A-Za-z_][A-Za-z0-9_]*$".to_string()), }),
is_secret: false,
};
add_field(field_with_validation.clone()).unwrap();
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
let retrieved_field = schema
.fields
.iter()
.find(|f| f.key == "validated_field")
.unwrap();
assert_eq!(retrieved_field.r#type, "string");
assert_eq!(
retrieved_field.description,
Some("A field with validation".to_string())
);
assert!(retrieved_field.validation.is_some());
let validation = retrieved_field.validation.as_ref().unwrap();
assert_eq!(validation.min_length, Some(5));
assert_eq!(validation.max_length, Some(20));
assert!(validation.pattern.is_some());
}
#[test]
#[serial]
fn test_schema_field_types() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
let types_to_test = vec!["string", "integer", "boolean"];
for (i, r#type) in types_to_test.iter().enumerate() {
add_field(FieldDefinition {
key: format!("typed_field_{}", i),
r#type: r#type.to_string(),
description: Some(format!("Field of type {}", r#type)),
validation: None,
is_secret: false,
})
.unwrap();
}
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
assert_eq!(schema.fields.len(), 3);
for (i, expected_type) in types_to_test.iter().enumerate() {
let field = schema
.fields
.iter()
.find(|f| f.key == format!("typed_field_{}", i))
.unwrap();
assert_eq!(&field.r#type, expected_type);
}
}
#[test]
#[serial]
fn test_schema_field_with_empty_description() {
let temp_dir = TempDir::new().unwrap();
let _guard = TestDirGuard::new(temp_dir.path());
persistence::init_project().unwrap();
add_field(FieldDefinition {
key: "no_desc_field".to_string(),
r#type: "string".to_string(),
description: None, validation: None,
is_secret: false,
})
.unwrap();
let schema: SchemaFile =
persistence::atomic_read_json(SCHEMA_FILE, |s: &SchemaFile| s.clone()).unwrap();
let field = schema
.fields
.iter()
.find(|f| f.key == "no_desc_field")
.unwrap();
assert_eq!(field.description, None);
}
}