Skip to main content

mockforge_vbr/
schema.rs

1//! VBR schema extensions
2//!
3//! This module extends the SchemaDefinition from mockforge-data with VBR-specific
4//! metadata including primary keys, foreign keys, indexes, unique constraints,
5//! and auto-generation rules.
6
7use mockforge_data::SchemaDefinition;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11/// VBR-specific schema metadata that extends SchemaDefinition
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct VbrSchemaDefinition {
14    /// Base schema definition from mockforge-data
15    #[serde(flatten)]
16    pub base: SchemaDefinition,
17
18    /// Primary key field name(s)
19    pub primary_key: Vec<String>,
20
21    /// Foreign key relationships
22    pub foreign_keys: Vec<ForeignKeyDefinition>,
23
24    /// Index definitions
25    pub indexes: Vec<IndexDefinition>,
26
27    /// Unique constraints
28    pub unique_constraints: Vec<UniqueConstraint>,
29
30    /// Auto-generation rules for fields
31    pub auto_generation: HashMap<String, AutoGenerationRule>,
32
33    /// Many-to-many relationships
34    pub many_to_many: Vec<ManyToManyDefinition>,
35}
36
37/// Foreign key relationship definition
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ForeignKeyDefinition {
40    /// Field name in this entity
41    pub field: String,
42
43    /// Target entity name
44    pub target_entity: String,
45
46    /// Target field name (usually "id")
47    pub target_field: String,
48
49    /// Cascade action on delete
50    #[serde(default)]
51    pub on_delete: CascadeAction,
52
53    /// Cascade action on update
54    #[serde(default)]
55    pub on_update: CascadeAction,
56}
57
58/// Many-to-many relationship definition
59///
60/// Represents a many-to-many relationship between two entities using a junction table.
61/// For example, Users and Roles with a user_roles junction table.
62#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct ManyToManyDefinition {
64    /// First entity name
65    pub entity_a: String,
66
67    /// Second entity name
68    pub entity_b: String,
69
70    /// Junction table name (auto-generated if not provided)
71    /// Format: "{entity_a}_{entity_b}" or "{entity_b}_{entity_a}" (alphabetically sorted)
72    pub junction_table: Option<String>,
73
74    /// Foreign key field name in junction table pointing to entity_a
75    pub entity_a_field: String,
76
77    /// Foreign key field name in junction table pointing to entity_b
78    pub entity_b_field: String,
79
80    /// Cascade action on delete for entity_a
81    #[serde(default)]
82    pub on_delete_a: CascadeAction,
83
84    /// Cascade action on delete for entity_b
85    #[serde(default)]
86    pub on_delete_b: CascadeAction,
87}
88
89impl ManyToManyDefinition {
90    /// Create a new many-to-many relationship definition
91    pub fn new(entity_a: String, entity_b: String) -> Self {
92        // Auto-generate junction table name (alphabetically sorted)
93        let (_field_a, _field_b) = if entity_a.to_lowercase() < entity_b.to_lowercase() {
94            (
95                format!("{}_id", entity_a.to_lowercase()),
96                format!("{}_id", entity_b.to_lowercase()),
97            )
98        } else {
99            (
100                format!("{}_id", entity_b.to_lowercase()),
101                format!("{}_id", entity_a.to_lowercase()),
102            )
103        };
104
105        let junction_table = if entity_a.to_lowercase() < entity_b.to_lowercase() {
106            Some(format!("{}_{}", entity_a.to_lowercase(), entity_b.to_lowercase()))
107        } else {
108            Some(format!("{}_{}", entity_b.to_lowercase(), entity_a.to_lowercase()))
109        };
110
111        Self {
112            entity_a: entity_a.clone(),
113            entity_b: entity_b.clone(),
114            junction_table,
115            entity_a_field: format!("{}_id", entity_a.to_lowercase()),
116            entity_b_field: format!("{}_id", entity_b.to_lowercase()),
117            on_delete_a: CascadeAction::Cascade,
118            on_delete_b: CascadeAction::Cascade,
119        }
120    }
121
122    /// Set the junction table name
123    pub fn with_junction_table(mut self, table_name: String) -> Self {
124        self.junction_table = Some(table_name);
125        self
126    }
127
128    /// Set the foreign key field names
129    pub fn with_fields(mut self, entity_a_field: String, entity_b_field: String) -> Self {
130        self.entity_a_field = entity_a_field;
131        self.entity_b_field = entity_b_field;
132        self
133    }
134
135    /// Set cascade actions
136    pub fn with_cascade_actions(
137        mut self,
138        on_delete_a: CascadeAction,
139        on_delete_b: CascadeAction,
140    ) -> Self {
141        self.on_delete_a = on_delete_a;
142        self.on_delete_b = on_delete_b;
143        self
144    }
145}
146
147/// Cascade action for foreign keys
148#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
149#[serde(rename_all = "UPPERCASE")]
150pub enum CascadeAction {
151    /// No action
152    #[default]
153    NoAction,
154    /// Cascade (delete/update related records)
155    Cascade,
156    /// Set null
157    SetNull,
158    /// Set default
159    SetDefault,
160    /// Restrict (prevent if related records exist)
161    Restrict,
162}
163
164/// Index definition
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct IndexDefinition {
167    /// Index name
168    pub name: String,
169
170    /// Fields included in the index
171    pub fields: Vec<String>,
172
173    /// Whether the index is unique
174    #[serde(default)]
175    pub unique: bool,
176}
177
178/// Unique constraint definition
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct UniqueConstraint {
181    /// Constraint name
182    pub name: String,
183
184    /// Fields that must be unique together
185    pub fields: Vec<String>,
186}
187
188/// Auto-generation rule for fields
189#[derive(Debug, Clone, Serialize, Deserialize)]
190#[serde(tag = "type", content = "value", rename_all = "snake_case")]
191pub enum AutoGenerationRule {
192    /// Auto-incrementing integer
193    AutoIncrement,
194    /// UUID generation
195    Uuid,
196    /// Current timestamp
197    Timestamp,
198    /// Current date
199    Date,
200    /// Custom function/expression
201    Custom(String),
202    /// Pattern-based ID generation
203    ///
204    /// Supports template variables:
205    /// - `{increment}` or `{increment:06}` - Auto-incrementing number with padding
206    /// - `{timestamp}` - Unix timestamp
207    /// - `{random}` - Random alphanumeric string
208    /// - `{uuid}` - UUID v4
209    ///
210    /// Examples:
211    /// - "USR-{increment:06}" -> "USR-000001"
212    /// - "ORD-{timestamp}" -> "ORD-1704067200"
213    Pattern(String),
214    /// Realistic-looking ID generation (Stripe-style)
215    ///
216    /// Generates IDs in the format: `{prefix}_{random_alphanumeric}`
217    ///
218    /// # Arguments
219    /// * `prefix` - Prefix for the ID (e.g., "cus", "ord")
220    /// * `length` - Total length of the random part (excluding prefix and underscore)
221    Realistic {
222        /// Prefix for the ID
223        prefix: String,
224        /// Length of the random alphanumeric part
225        length: usize,
226    },
227}
228
229impl VbrSchemaDefinition {
230    /// Create a new VBR schema definition from a base schema
231    pub fn new(base: SchemaDefinition) -> Self {
232        Self {
233            base,
234            primary_key: vec!["id".to_string()], // Default primary key
235            foreign_keys: Vec::new(),
236            indexes: Vec::new(),
237            unique_constraints: Vec::new(),
238            auto_generation: HashMap::new(),
239            many_to_many: Vec::new(),
240        }
241    }
242
243    /// Set the primary key field(s)
244    pub fn with_primary_key(mut self, fields: Vec<String>) -> Self {
245        self.primary_key = fields;
246        self
247    }
248
249    /// Add a foreign key relationship
250    pub fn with_foreign_key(mut self, fk: ForeignKeyDefinition) -> Self {
251        self.foreign_keys.push(fk);
252        self
253    }
254
255    /// Add an index
256    pub fn with_index(mut self, index: IndexDefinition) -> Self {
257        self.indexes.push(index);
258        self
259    }
260
261    /// Add a unique constraint
262    pub fn with_unique_constraint(mut self, constraint: UniqueConstraint) -> Self {
263        self.unique_constraints.push(constraint);
264        self
265    }
266
267    /// Set auto-generation rule for a field
268    pub fn with_auto_generation(mut self, field: String, rule: AutoGenerationRule) -> Self {
269        self.auto_generation.insert(field, rule);
270        self
271    }
272
273    /// Add a many-to-many relationship
274    pub fn with_many_to_many(mut self, m2m: ManyToManyDefinition) -> Self {
275        self.many_to_many.push(m2m);
276        self
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use mockforge_data::SchemaDefinition;
284
285    fn create_test_schema() -> VbrSchemaDefinition {
286        let base = SchemaDefinition::new("TestEntity".to_string());
287        VbrSchemaDefinition::new(base)
288    }
289
290    // CascadeAction tests
291    #[test]
292    fn test_cascade_action_default() {
293        let action = CascadeAction::default();
294        assert!(matches!(action, CascadeAction::NoAction));
295    }
296
297    #[test]
298    fn test_cascade_action_serialize() {
299        assert_eq!(serde_json::to_string(&CascadeAction::NoAction).unwrap(), "\"NOACTION\"");
300        assert_eq!(serde_json::to_string(&CascadeAction::Cascade).unwrap(), "\"CASCADE\"");
301        assert_eq!(serde_json::to_string(&CascadeAction::SetNull).unwrap(), "\"SETNULL\"");
302        assert_eq!(serde_json::to_string(&CascadeAction::SetDefault).unwrap(), "\"SETDEFAULT\"");
303        assert_eq!(serde_json::to_string(&CascadeAction::Restrict).unwrap(), "\"RESTRICT\"");
304    }
305
306    #[test]
307    fn test_cascade_action_deserialize() {
308        let action: CascadeAction = serde_json::from_str("\"CASCADE\"").unwrap();
309        assert!(matches!(action, CascadeAction::Cascade));
310    }
311
312    #[test]
313    fn test_cascade_action_clone() {
314        let action = CascadeAction::Cascade;
315        let cloned = action;
316        assert_eq!(action, cloned);
317    }
318
319    #[test]
320    fn test_cascade_action_debug() {
321        let action = CascadeAction::Restrict;
322        let debug = format!("{:?}", action);
323        assert!(debug.contains("Restrict"));
324    }
325
326    // ForeignKeyDefinition tests
327    #[test]
328    fn test_foreign_key_definition_clone() {
329        let fk = ForeignKeyDefinition {
330            field: "user_id".to_string(),
331            target_entity: "User".to_string(),
332            target_field: "id".to_string(),
333            on_delete: CascadeAction::Cascade,
334            on_update: CascadeAction::NoAction,
335        };
336
337        let cloned = fk.clone();
338        assert_eq!(fk.field, cloned.field);
339        assert_eq!(fk.target_entity, cloned.target_entity);
340    }
341
342    #[test]
343    fn test_foreign_key_definition_debug() {
344        let fk = ForeignKeyDefinition {
345            field: "post_id".to_string(),
346            target_entity: "Post".to_string(),
347            target_field: "id".to_string(),
348            on_delete: CascadeAction::default(),
349            on_update: CascadeAction::default(),
350        };
351
352        let debug = format!("{:?}", fk);
353        assert!(debug.contains("ForeignKeyDefinition"));
354        assert!(debug.contains("post_id"));
355    }
356
357    #[test]
358    fn test_foreign_key_definition_serialize() {
359        let fk = ForeignKeyDefinition {
360            field: "author_id".to_string(),
361            target_entity: "Author".to_string(),
362            target_field: "id".to_string(),
363            on_delete: CascadeAction::Cascade,
364            on_update: CascadeAction::NoAction,
365        };
366
367        let json = serde_json::to_string(&fk).unwrap();
368        assert!(json.contains("author_id"));
369        assert!(json.contains("Author"));
370    }
371
372    // ManyToManyDefinition tests
373    #[test]
374    fn test_many_to_many_definition_new() {
375        let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string());
376        assert_eq!(m2m.entity_a, "User");
377        assert_eq!(m2m.entity_b, "Role");
378        assert_eq!(m2m.junction_table, Some("role_user".to_string())); // Alphabetical order
379        assert_eq!(m2m.entity_a_field, "user_id");
380        assert_eq!(m2m.entity_b_field, "role_id");
381    }
382
383    #[test]
384    fn test_many_to_many_definition_alphabetical_order() {
385        // When entity_a comes before entity_b alphabetically
386        let m2m1 = ManyToManyDefinition::new("Apple".to_string(), "Banana".to_string());
387        assert_eq!(m2m1.junction_table, Some("apple_banana".to_string()));
388
389        // When entity_b comes before entity_a alphabetically
390        let m2m2 = ManyToManyDefinition::new("Zebra".to_string(), "Apple".to_string());
391        assert_eq!(m2m2.junction_table, Some("apple_zebra".to_string()));
392    }
393
394    #[test]
395    fn test_many_to_many_definition_with_junction_table() {
396        let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string())
397            .with_junction_table("custom_user_roles".to_string());
398        assert_eq!(m2m.junction_table, Some("custom_user_roles".to_string()));
399    }
400
401    #[test]
402    fn test_many_to_many_definition_with_fields() {
403        let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string())
404            .with_fields("usr_id".to_string(), "role_identifier".to_string());
405        assert_eq!(m2m.entity_a_field, "usr_id");
406        assert_eq!(m2m.entity_b_field, "role_identifier");
407    }
408
409    #[test]
410    fn test_many_to_many_definition_with_cascade_actions() {
411        let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string())
412            .with_cascade_actions(CascadeAction::Restrict, CascadeAction::SetNull);
413        assert!(matches!(m2m.on_delete_a, CascadeAction::Restrict));
414        assert!(matches!(m2m.on_delete_b, CascadeAction::SetNull));
415    }
416
417    #[test]
418    fn test_many_to_many_definition_clone() {
419        let m2m = ManyToManyDefinition::new("User".to_string(), "Group".to_string());
420        let cloned = m2m.clone();
421        assert_eq!(m2m.entity_a, cloned.entity_a);
422        assert_eq!(m2m.junction_table, cloned.junction_table);
423    }
424
425    #[test]
426    fn test_many_to_many_definition_debug() {
427        let m2m = ManyToManyDefinition::new("Tag".to_string(), "Post".to_string());
428        let debug = format!("{:?}", m2m);
429        assert!(debug.contains("ManyToManyDefinition"));
430        assert!(debug.contains("Tag"));
431    }
432
433    // IndexDefinition tests
434    #[test]
435    fn test_index_definition_clone() {
436        let idx = IndexDefinition {
437            name: "idx_email".to_string(),
438            fields: vec!["email".to_string()],
439            unique: true,
440        };
441
442        let cloned = idx.clone();
443        assert_eq!(idx.name, cloned.name);
444        assert_eq!(idx.unique, cloned.unique);
445    }
446
447    #[test]
448    fn test_index_definition_debug() {
449        let idx = IndexDefinition {
450            name: "idx_composite".to_string(),
451            fields: vec!["first_name".to_string(), "last_name".to_string()],
452            unique: false,
453        };
454
455        let debug = format!("{:?}", idx);
456        assert!(debug.contains("IndexDefinition"));
457        assert!(debug.contains("idx_composite"));
458    }
459
460    #[test]
461    fn test_index_definition_serialize() {
462        let idx = IndexDefinition {
463            name: "idx_test".to_string(),
464            fields: vec!["field1".to_string()],
465            unique: true,
466        };
467
468        let json = serde_json::to_string(&idx).unwrap();
469        assert!(json.contains("idx_test"));
470        assert!(json.contains("\"unique\":true"));
471    }
472
473    // UniqueConstraint tests
474    #[test]
475    fn test_unique_constraint_clone() {
476        let constraint = UniqueConstraint {
477            name: "uq_email".to_string(),
478            fields: vec!["email".to_string()],
479        };
480
481        let cloned = constraint.clone();
482        assert_eq!(constraint.name, cloned.name);
483        assert_eq!(constraint.fields, cloned.fields);
484    }
485
486    #[test]
487    fn test_unique_constraint_debug() {
488        let constraint = UniqueConstraint {
489            name: "uq_composite".to_string(),
490            fields: vec!["a".to_string(), "b".to_string()],
491        };
492
493        let debug = format!("{:?}", constraint);
494        assert!(debug.contains("UniqueConstraint"));
495    }
496
497    // AutoGenerationRule tests
498    #[test]
499    fn test_auto_generation_rule_clone() {
500        let rule = AutoGenerationRule::Uuid;
501        let cloned = rule.clone();
502        assert!(matches!(cloned, AutoGenerationRule::Uuid));
503    }
504
505    #[test]
506    fn test_auto_generation_rule_debug() {
507        let rule = AutoGenerationRule::Timestamp;
508        let debug = format!("{:?}", rule);
509        assert!(debug.contains("Timestamp"));
510    }
511
512    #[test]
513    fn test_auto_generation_rule_serialize_uuid() {
514        let rule = AutoGenerationRule::Uuid;
515        let json = serde_json::to_string(&rule).unwrap();
516        assert!(json.contains("uuid"));
517    }
518
519    #[test]
520    fn test_auto_generation_rule_serialize_pattern() {
521        let rule = AutoGenerationRule::Pattern("USR-{increment:06}".to_string());
522        let json = serde_json::to_string(&rule).unwrap();
523        assert!(json.contains("pattern"));
524        assert!(json.contains("USR-{increment:06}"));
525    }
526
527    #[test]
528    fn test_auto_generation_rule_serialize_realistic() {
529        let rule = AutoGenerationRule::Realistic {
530            prefix: "cus".to_string(),
531            length: 14,
532        };
533        let json = serde_json::to_string(&rule).unwrap();
534        assert!(json.contains("realistic"));
535        // With adjacently tagged repr, struct fields are nested under "value"
536        assert!(json.contains("\"value\":{"));
537        assert!(json.contains("\"prefix\":\"cus\""));
538        assert!(json.contains("\"length\":14"));
539    }
540
541    #[test]
542    fn test_auto_generation_rule_all_variants() {
543        let rules = vec![
544            AutoGenerationRule::AutoIncrement,
545            AutoGenerationRule::Uuid,
546            AutoGenerationRule::Timestamp,
547            AutoGenerationRule::Date,
548            AutoGenerationRule::Custom("NOW()".to_string()),
549            AutoGenerationRule::Pattern("{uuid}".to_string()),
550            AutoGenerationRule::Realistic {
551                prefix: "test".to_string(),
552                length: 10,
553            },
554        ];
555
556        for rule in rules {
557            let json = serde_json::to_string(&rule).unwrap();
558            assert!(!json.is_empty());
559        }
560    }
561
562    // VbrSchemaDefinition tests
563    #[test]
564    fn test_vbr_schema_definition_new() {
565        let base = SchemaDefinition::new("User".to_string());
566        let schema = VbrSchemaDefinition::new(base);
567
568        assert_eq!(schema.primary_key, vec!["id"]);
569        assert!(schema.foreign_keys.is_empty());
570        assert!(schema.indexes.is_empty());
571        assert!(schema.unique_constraints.is_empty());
572        assert!(schema.auto_generation.is_empty());
573        assert!(schema.many_to_many.is_empty());
574    }
575
576    #[test]
577    fn test_vbr_schema_definition_with_primary_key() {
578        let schema = create_test_schema()
579            .with_primary_key(vec!["user_id".to_string(), "role_id".to_string()]);
580        assert_eq!(schema.primary_key, vec!["user_id", "role_id"]);
581    }
582
583    #[test]
584    fn test_vbr_schema_definition_with_foreign_key() {
585        let fk = ForeignKeyDefinition {
586            field: "user_id".to_string(),
587            target_entity: "User".to_string(),
588            target_field: "id".to_string(),
589            on_delete: CascadeAction::Cascade,
590            on_update: CascadeAction::NoAction,
591        };
592
593        let schema = create_test_schema().with_foreign_key(fk);
594        assert_eq!(schema.foreign_keys.len(), 1);
595        assert_eq!(schema.foreign_keys[0].field, "user_id");
596    }
597
598    #[test]
599    fn test_vbr_schema_definition_with_index() {
600        let idx = IndexDefinition {
601            name: "idx_email".to_string(),
602            fields: vec!["email".to_string()],
603            unique: true,
604        };
605
606        let schema = create_test_schema().with_index(idx);
607        assert_eq!(schema.indexes.len(), 1);
608        assert!(schema.indexes[0].unique);
609    }
610
611    #[test]
612    fn test_vbr_schema_definition_with_unique_constraint() {
613        let constraint = UniqueConstraint {
614            name: "uq_email".to_string(),
615            fields: vec!["email".to_string()],
616        };
617
618        let schema = create_test_schema().with_unique_constraint(constraint);
619        assert_eq!(schema.unique_constraints.len(), 1);
620    }
621
622    #[test]
623    fn test_vbr_schema_definition_with_auto_generation() {
624        let schema =
625            create_test_schema().with_auto_generation("id".to_string(), AutoGenerationRule::Uuid);
626        assert!(schema.auto_generation.contains_key("id"));
627    }
628
629    #[test]
630    fn test_vbr_schema_definition_with_many_to_many() {
631        let m2m = ManyToManyDefinition::new("User".to_string(), "Role".to_string());
632        let schema = create_test_schema().with_many_to_many(m2m);
633        assert_eq!(schema.many_to_many.len(), 1);
634    }
635
636    #[test]
637    fn test_vbr_schema_definition_builder_chain() {
638        let schema = create_test_schema()
639            .with_primary_key(vec!["id".to_string()])
640            .with_auto_generation("id".to_string(), AutoGenerationRule::Uuid)
641            .with_auto_generation("created_at".to_string(), AutoGenerationRule::Timestamp)
642            .with_index(IndexDefinition {
643                name: "idx_email".to_string(),
644                fields: vec!["email".to_string()],
645                unique: true,
646            })
647            .with_unique_constraint(UniqueConstraint {
648                name: "uq_username".to_string(),
649                fields: vec!["username".to_string()],
650            })
651            .with_foreign_key(ForeignKeyDefinition {
652                field: "org_id".to_string(),
653                target_entity: "Organization".to_string(),
654                target_field: "id".to_string(),
655                on_delete: CascadeAction::SetNull,
656                on_update: CascadeAction::NoAction,
657            });
658
659        assert_eq!(schema.auto_generation.len(), 2);
660        assert_eq!(schema.indexes.len(), 1);
661        assert_eq!(schema.unique_constraints.len(), 1);
662        assert_eq!(schema.foreign_keys.len(), 1);
663    }
664
665    #[test]
666    fn test_vbr_schema_definition_clone() {
667        let schema = create_test_schema().with_primary_key(vec!["custom_id".to_string()]);
668
669        let cloned = schema.clone();
670        assert_eq!(schema.primary_key, cloned.primary_key);
671    }
672
673    #[test]
674    fn test_vbr_schema_definition_debug() {
675        let schema = create_test_schema();
676        let debug = format!("{:?}", schema);
677        assert!(debug.contains("VbrSchemaDefinition"));
678    }
679}