use mockforge_data::SchemaDefinition;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VbrSchemaDefinition {
#[serde(flatten)]
pub base: SchemaDefinition,
pub primary_key: Vec<String>,
pub foreign_keys: Vec<ForeignKeyDefinition>,
pub indexes: Vec<IndexDefinition>,
pub unique_constraints: Vec<UniqueConstraint>,
pub auto_generation: HashMap<String, AutoGenerationRule>,
pub many_to_many: Vec<ManyToManyDefinition>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ForeignKeyDefinition {
pub field: String,
pub target_entity: String,
pub target_field: String,
#[serde(default)]
pub on_delete: CascadeAction,
#[serde(default)]
pub on_update: CascadeAction,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ManyToManyDefinition {
pub entity_a: String,
pub entity_b: String,
pub junction_table: Option<String>,
pub entity_a_field: String,
pub entity_b_field: String,
#[serde(default)]
pub on_delete_a: CascadeAction,
#[serde(default)]
pub on_delete_b: CascadeAction,
}
impl ManyToManyDefinition {
pub fn new(entity_a: String, entity_b: String) -> Self {
let (_field_a, _field_b) = if entity_a.to_lowercase() < entity_b.to_lowercase() {
(
format!("{}_id", entity_a.to_lowercase()),
format!("{}_id", entity_b.to_lowercase()),
)
} else {
(
format!("{}_id", entity_b.to_lowercase()),
format!("{}_id", entity_a.to_lowercase()),
)
};
let junction_table = if entity_a.to_lowercase() < entity_b.to_lowercase() {
Some(format!("{}_{}", entity_a.to_lowercase(), entity_b.to_lowercase()))
} else {
Some(format!("{}_{}", entity_b.to_lowercase(), entity_a.to_lowercase()))
};
Self {
entity_a: entity_a.clone(),
entity_b: entity_b.clone(),
junction_table,
entity_a_field: format!("{}_id", entity_a.to_lowercase()),
entity_b_field: format!("{}_id", entity_b.to_lowercase()),
on_delete_a: CascadeAction::Cascade,
on_delete_b: CascadeAction::Cascade,
}
}
pub fn with_junction_table(mut self, table_name: String) -> Self {
self.junction_table = Some(table_name);
self
}
pub fn with_fields(mut self, entity_a_field: String, entity_b_field: String) -> Self {
self.entity_a_field = entity_a_field;
self.entity_b_field = entity_b_field;
self
}
pub fn with_cascade_actions(
mut self,
on_delete_a: CascadeAction,
on_delete_b: CascadeAction,
) -> Self {
self.on_delete_a = on_delete_a;
self.on_delete_b = on_delete_b;
self
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "UPPERCASE")]
pub enum CascadeAction {
#[default]
NoAction,
Cascade,
SetNull,
SetDefault,
Restrict,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexDefinition {
pub name: String,
pub fields: Vec<String>,
#[serde(default)]
pub unique: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniqueConstraint {
pub name: String,
pub fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "value", rename_all = "snake_case")]
pub enum AutoGenerationRule {
AutoIncrement,
Uuid,
Timestamp,
Date,
Custom(String),
Pattern(String),
Realistic {
prefix: String,
length: usize,
},
}
impl VbrSchemaDefinition {
pub fn new(base: SchemaDefinition) -> Self {
Self {
base,
primary_key: vec!["id".to_string()], foreign_keys: Vec::new(),
indexes: Vec::new(),
unique_constraints: Vec::new(),
auto_generation: HashMap::new(),
many_to_many: Vec::new(),
}
}
pub fn with_primary_key(mut self, fields: Vec<String>) -> Self {
self.primary_key = fields;
self
}
pub fn with_foreign_key(mut self, fk: ForeignKeyDefinition) -> Self {
self.foreign_keys.push(fk);
self
}
pub fn with_index(mut self, index: IndexDefinition) -> Self {
self.indexes.push(index);
self
}
pub fn with_unique_constraint(mut self, constraint: UniqueConstraint) -> Self {
self.unique_constraints.push(constraint);
self
}
pub fn with_auto_generation(mut self, field: String, rule: AutoGenerationRule) -> Self {
self.auto_generation.insert(field, rule);
self
}
pub fn with_many_to_many(mut self, m2m: ManyToManyDefinition) -> Self {
self.many_to_many.push(m2m);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockforge_data::SchemaDefinition;
fn create_test_schema() -> VbrSchemaDefinition {
let base = SchemaDefinition::new("TestEntity".to_string());
VbrSchemaDefinition::new(base)
}
#[test]
fn test_cascade_action_default() {
let action = CascadeAction::default();
assert!(matches!(action, CascadeAction::NoAction));
}
#[test]
fn test_cascade_action_serialize() {
assert_eq!(serde_json::to_string(&CascadeAction::NoAction).unwrap(), "\"NOACTION\"");
assert_eq!(serde_json::to_string(&CascadeAction::Cascade).unwrap(), "\"CASCADE\"");
assert_eq!(serde_json::to_string(&CascadeAction::SetNull).unwrap(), "\"SETNULL\"");
assert_eq!(serde_json::to_string(&CascadeAction::SetDefault).unwrap(), "\"SETDEFAULT\"");
assert_eq!(serde_json::to_string(&CascadeAction::Restrict).unwrap(), "\"RESTRICT\"");
}
#[test]
fn test_cascade_action_deserialize() {
let action: CascadeAction = serde_json::from_str("\"CASCADE\"").unwrap();
assert!(matches!(action, CascadeAction::Cascade));
}
#[test]
fn test_cascade_action_clone() {
let action = CascadeAction::Cascade;
let cloned = action;
assert_eq!(action, cloned);
}
#[test]
fn test_cascade_action_debug() {
let action = CascadeAction::Restrict;
let debug = format!("{:?}", action);
assert!(debug.contains("Restrict"));
}
#[test]
fn test_foreign_key_definition_clone() {
let fk = ForeignKeyDefinition {
field: "user_id".to_string(),
target_entity: "User".to_string(),
target_field: "id".to_string(),
on_delete: CascadeAction::Cascade,
on_update: CascadeAction::NoAction,
};
let cloned = fk.clone();
assert_eq!(fk.field, cloned.field);
assert_eq!(fk.target_entity, cloned.target_entity);
}
#[test]
fn test_foreign_key_definition_debug() {
let fk = ForeignKeyDefinition {
field: "post_id".to_string(),
target_entity: "Post".to_string(),
target_field: "id".to_string(),
on_delete: CascadeAction::default(),
on_update: CascadeAction::default(),
};
let debug = format!("{:?}", fk);
assert!(debug.contains("ForeignKeyDefinition"));
assert!(debug.contains("post_id"));
}
#[test]
fn test_foreign_key_definition_serialize() {
let fk = ForeignKeyDefinition {
field: "author_id".to_string(),
target_entity: "Author".to_string(),
target_field: "id".to_string(),
on_delete: CascadeAction::Cascade,
on_update: CascadeAction::NoAction,
};
let json = serde_json::to_string(&fk).unwrap();
assert!(json.contains("author_id"));
assert!(json.contains("Author"));
}
#[test]
fn test_many_to_many_definition_new() {
let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string());
assert_eq!(m2m.entity_a, "User");
assert_eq!(m2m.entity_b, "Role");
assert_eq!(m2m.junction_table, Some("role_user".to_string())); assert_eq!(m2m.entity_a_field, "user_id");
assert_eq!(m2m.entity_b_field, "role_id");
}
#[test]
fn test_many_to_many_definition_alphabetical_order() {
let m2m1 = ManyToManyDefinition::new("Apple".to_string(), "Banana".to_string());
assert_eq!(m2m1.junction_table, Some("apple_banana".to_string()));
let m2m2 = ManyToManyDefinition::new("Zebra".to_string(), "Apple".to_string());
assert_eq!(m2m2.junction_table, Some("apple_zebra".to_string()));
}
#[test]
fn test_many_to_many_definition_with_junction_table() {
let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string())
.with_junction_table("custom_user_roles".to_string());
assert_eq!(m2m.junction_table, Some("custom_user_roles".to_string()));
}
#[test]
fn test_many_to_many_definition_with_fields() {
let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string())
.with_fields("usr_id".to_string(), "role_identifier".to_string());
assert_eq!(m2m.entity_a_field, "usr_id");
assert_eq!(m2m.entity_b_field, "role_identifier");
}
#[test]
fn test_many_to_many_definition_with_cascade_actions() {
let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string())
.with_cascade_actions(CascadeAction::Restrict, CascadeAction::SetNull);
assert!(matches!(m2m.on_delete_a, CascadeAction::Restrict));
assert!(matches!(m2m.on_delete_b, CascadeAction::SetNull));
}
#[test]
fn test_many_to_many_definition_clone() {
let m2m = ManyToManyDefinition::new("User".to_string(), "Group".to_string());
let cloned = m2m.clone();
assert_eq!(m2m.entity_a, cloned.entity_a);
assert_eq!(m2m.junction_table, cloned.junction_table);
}
#[test]
fn test_many_to_many_definition_debug() {
let m2m = ManyToManyDefinition::new("Tag".to_string(), "Post".to_string());
let debug = format!("{:?}", m2m);
assert!(debug.contains("ManyToManyDefinition"));
assert!(debug.contains("Tag"));
}
#[test]
fn test_index_definition_clone() {
let idx = IndexDefinition {
name: "idx_email".to_string(),
fields: vec!["email".to_string()],
unique: true,
};
let cloned = idx.clone();
assert_eq!(idx.name, cloned.name);
assert_eq!(idx.unique, cloned.unique);
}
#[test]
fn test_index_definition_debug() {
let idx = IndexDefinition {
name: "idx_composite".to_string(),
fields: vec!["first_name".to_string(), "last_name".to_string()],
unique: false,
};
let debug = format!("{:?}", idx);
assert!(debug.contains("IndexDefinition"));
assert!(debug.contains("idx_composite"));
}
#[test]
fn test_index_definition_serialize() {
let idx = IndexDefinition {
name: "idx_test".to_string(),
fields: vec!["field1".to_string()],
unique: true,
};
let json = serde_json::to_string(&idx).unwrap();
assert!(json.contains("idx_test"));
assert!(json.contains("\"unique\":true"));
}
#[test]
fn test_unique_constraint_clone() {
let constraint = UniqueConstraint {
name: "uq_email".to_string(),
fields: vec!["email".to_string()],
};
let cloned = constraint.clone();
assert_eq!(constraint.name, cloned.name);
assert_eq!(constraint.fields, cloned.fields);
}
#[test]
fn test_unique_constraint_debug() {
let constraint = UniqueConstraint {
name: "uq_composite".to_string(),
fields: vec!["a".to_string(), "b".to_string()],
};
let debug = format!("{:?}", constraint);
assert!(debug.contains("UniqueConstraint"));
}
#[test]
fn test_auto_generation_rule_clone() {
let rule = AutoGenerationRule::Uuid;
let cloned = rule.clone();
assert!(matches!(cloned, AutoGenerationRule::Uuid));
}
#[test]
fn test_auto_generation_rule_debug() {
let rule = AutoGenerationRule::Timestamp;
let debug = format!("{:?}", rule);
assert!(debug.contains("Timestamp"));
}
#[test]
fn test_auto_generation_rule_serialize_uuid() {
let rule = AutoGenerationRule::Uuid;
let json = serde_json::to_string(&rule).unwrap();
assert!(json.contains("uuid"));
}
#[test]
fn test_auto_generation_rule_serialize_pattern() {
let rule = AutoGenerationRule::Pattern("USR-{increment:06}".to_string());
let json = serde_json::to_string(&rule).unwrap();
assert!(json.contains("pattern"));
assert!(json.contains("USR-{increment:06}"));
}
#[test]
fn test_auto_generation_rule_serialize_realistic() {
let rule = AutoGenerationRule::Realistic {
prefix: "cus".to_string(),
length: 14,
};
let json = serde_json::to_string(&rule).unwrap();
assert!(json.contains("realistic"));
assert!(json.contains("\"value\":{"));
assert!(json.contains("\"prefix\":\"cus\""));
assert!(json.contains("\"length\":14"));
}
#[test]
fn test_auto_generation_rule_all_variants() {
let rules = vec![
AutoGenerationRule::AutoIncrement,
AutoGenerationRule::Uuid,
AutoGenerationRule::Timestamp,
AutoGenerationRule::Date,
AutoGenerationRule::Custom("NOW()".to_string()),
AutoGenerationRule::Pattern("{uuid}".to_string()),
AutoGenerationRule::Realistic {
prefix: "test".to_string(),
length: 10,
},
];
for rule in rules {
let json = serde_json::to_string(&rule).unwrap();
assert!(!json.is_empty());
}
}
#[test]
fn test_vbr_schema_definition_new() {
let base = SchemaDefinition::new("User".to_string());
let schema = VbrSchemaDefinition::new(base);
assert_eq!(schema.primary_key, vec!["id"]);
assert!(schema.foreign_keys.is_empty());
assert!(schema.indexes.is_empty());
assert!(schema.unique_constraints.is_empty());
assert!(schema.auto_generation.is_empty());
assert!(schema.many_to_many.is_empty());
}
#[test]
fn test_vbr_schema_definition_with_primary_key() {
let schema = create_test_schema()
.with_primary_key(vec!["user_id".to_string(), "role_id".to_string()]);
assert_eq!(schema.primary_key, vec!["user_id", "role_id"]);
}
#[test]
fn test_vbr_schema_definition_with_foreign_key() {
let fk = ForeignKeyDefinition {
field: "user_id".to_string(),
target_entity: "User".to_string(),
target_field: "id".to_string(),
on_delete: CascadeAction::Cascade,
on_update: CascadeAction::NoAction,
};
let schema = create_test_schema().with_foreign_key(fk);
assert_eq!(schema.foreign_keys.len(), 1);
assert_eq!(schema.foreign_keys[0].field, "user_id");
}
#[test]
fn test_vbr_schema_definition_with_index() {
let idx = IndexDefinition {
name: "idx_email".to_string(),
fields: vec!["email".to_string()],
unique: true,
};
let schema = create_test_schema().with_index(idx);
assert_eq!(schema.indexes.len(), 1);
assert!(schema.indexes[0].unique);
}
#[test]
fn test_vbr_schema_definition_with_unique_constraint() {
let constraint = UniqueConstraint {
name: "uq_email".to_string(),
fields: vec!["email".to_string()],
};
let schema = create_test_schema().with_unique_constraint(constraint);
assert_eq!(schema.unique_constraints.len(), 1);
}
#[test]
fn test_vbr_schema_definition_with_auto_generation() {
let schema =
create_test_schema().with_auto_generation("id".to_string(), AutoGenerationRule::Uuid);
assert!(schema.auto_generation.contains_key("id"));
}
#[test]
fn test_vbr_schema_definition_with_many_to_many() {
let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string());
let schema = create_test_schema().with_many_to_many(m2m);
assert_eq!(schema.many_to_many.len(), 1);
}
#[test]
fn test_vbr_schema_definition_builder_chain() {
let schema = create_test_schema()
.with_primary_key(vec!["id".to_string()])
.with_auto_generation("id".to_string(), AutoGenerationRule::Uuid)
.with_auto_generation("created_at".to_string(), AutoGenerationRule::Timestamp)
.with_index(IndexDefinition {
name: "idx_email".to_string(),
fields: vec!["email".to_string()],
unique: true,
})
.with_unique_constraint(UniqueConstraint {
name: "uq_username".to_string(),
fields: vec!["username".to_string()],
})
.with_foreign_key(ForeignKeyDefinition {
field: "org_id".to_string(),
target_entity: "Organization".to_string(),
target_field: "id".to_string(),
on_delete: CascadeAction::SetNull,
on_update: CascadeAction::NoAction,
});
assert_eq!(schema.auto_generation.len(), 2);
assert_eq!(schema.indexes.len(), 1);
assert_eq!(schema.unique_constraints.len(), 1);
assert_eq!(schema.foreign_keys.len(), 1);
}
#[test]
fn test_vbr_schema_definition_clone() {
let schema = create_test_schema().with_primary_key(vec!["custom_id".to_string()]);
let cloned = schema.clone();
assert_eq!(schema.primary_key, cloned.primary_key);
}
#[test]
fn test_vbr_schema_definition_debug() {
let schema = create_test_schema();
let debug = format!("{:?}", schema);
assert!(debug.contains("VbrSchemaDefinition"));
}
}