Skip to main content

datasynth_generators/relationships/
rules.rs

1//! Relationship rules and configuration.
2//!
3//! Provides cardinality rules, property generation rules, and relationship
4//! type configurations for the relationship generator.
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// Configuration for relationship generation.
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RelationshipConfig {
12    /// Relationship type definitions.
13    pub relationship_types: Vec<RelationshipTypeConfig>,
14    /// Allow orphan entities (entities with no relationships).
15    pub allow_orphans: bool,
16    /// Probability of creating an orphan entity.
17    pub orphan_probability: f64,
18    /// Allow circular relationships.
19    pub allow_circular: bool,
20    /// Maximum depth for circular relationship detection.
21    pub max_circular_depth: u32,
22}
23
24impl Default for RelationshipConfig {
25    fn default() -> Self {
26        Self {
27            relationship_types: Vec::new(),
28            allow_orphans: true,
29            orphan_probability: 0.01,
30            allow_circular: false,
31            max_circular_depth: 3,
32        }
33    }
34}
35
36impl RelationshipConfig {
37    /// Creates a new configuration with the given relationship types.
38    pub fn with_types(types: Vec<RelationshipTypeConfig>) -> Self {
39        Self {
40            relationship_types: types,
41            ..Default::default()
42        }
43    }
44
45    /// Sets whether orphan entities are allowed.
46    pub fn allow_orphans(mut self, allow: bool) -> Self {
47        self.allow_orphans = allow;
48        self
49    }
50
51    /// Sets the orphan probability.
52    pub fn orphan_probability(mut self, prob: f64) -> Self {
53        self.orphan_probability = prob.clamp(0.0, 1.0);
54        self
55    }
56
57    /// Sets whether circular relationships are allowed.
58    pub fn allow_circular(mut self, allow: bool) -> Self {
59        self.allow_circular = allow;
60        self
61    }
62
63    /// Sets the maximum circular depth.
64    pub fn max_circular_depth(mut self, depth: u32) -> Self {
65        self.max_circular_depth = depth;
66        self
67    }
68}
69
70/// Configuration for a specific relationship type.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RelationshipTypeConfig {
73    /// Name of the relationship type (e.g., "debits", "credits", "created").
74    pub name: String,
75    /// Source entity type (e.g., "journal_entry").
76    pub source_type: String,
77    /// Target entity type (e.g., "account").
78    pub target_type: String,
79    /// Cardinality rule for this relationship.
80    pub cardinality: CardinalityRule,
81    /// Weight for this relationship in random selection.
82    pub weight: f64,
83    /// Property generation rules for this relationship.
84    pub properties: Vec<PropertyGenerationRule>,
85    /// Whether this relationship is required.
86    pub required: bool,
87    /// Whether this relationship is directed.
88    pub directed: bool,
89}
90
91impl Default for RelationshipTypeConfig {
92    fn default() -> Self {
93        Self {
94            name: String::new(),
95            source_type: String::new(),
96            target_type: String::new(),
97            cardinality: CardinalityRule::OneToMany { min: 1, max: 5 },
98            weight: 1.0,
99            properties: Vec::new(),
100            required: false,
101            directed: true,
102        }
103    }
104}
105
106impl RelationshipTypeConfig {
107    /// Creates a new relationship type configuration.
108    pub fn new(
109        name: impl Into<String>,
110        source_type: impl Into<String>,
111        target_type: impl Into<String>,
112    ) -> Self {
113        Self {
114            name: name.into(),
115            source_type: source_type.into(),
116            target_type: target_type.into(),
117            ..Default::default()
118        }
119    }
120
121    /// Sets the cardinality rule.
122    pub fn with_cardinality(mut self, cardinality: CardinalityRule) -> Self {
123        self.cardinality = cardinality;
124        self
125    }
126
127    /// Sets the weight.
128    pub fn with_weight(mut self, weight: f64) -> Self {
129        self.weight = weight.max(0.0);
130        self
131    }
132
133    /// Adds a property generation rule.
134    pub fn with_property(mut self, property: PropertyGenerationRule) -> Self {
135        self.properties.push(property);
136        self
137    }
138
139    /// Sets whether this relationship is required.
140    pub fn required(mut self, required: bool) -> Self {
141        self.required = required;
142        self
143    }
144
145    /// Sets whether this relationship is directed.
146    pub fn directed(mut self, directed: bool) -> Self {
147        self.directed = directed;
148        self
149    }
150}
151
152/// Cardinality rule for relationships.
153#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum CardinalityRule {
156    /// One source to one target.
157    OneToOne,
158    /// One source to many targets.
159    OneToMany {
160        /// Minimum number of targets.
161        min: u32,
162        /// Maximum number of targets.
163        max: u32,
164    },
165    /// Many sources to one target.
166    ManyToOne {
167        /// Minimum number of sources.
168        min: u32,
169        /// Maximum number of sources.
170        max: u32,
171    },
172    /// Many sources to many targets.
173    ManyToMany {
174        /// Minimum targets per source.
175        min_per_source: u32,
176        /// Maximum targets per source.
177        max_per_source: u32,
178    },
179}
180
181impl Default for CardinalityRule {
182    fn default() -> Self {
183        Self::OneToMany { min: 1, max: 5 }
184    }
185}
186
187impl CardinalityRule {
188    /// Creates a OneToOne cardinality.
189    pub fn one_to_one() -> Self {
190        Self::OneToOne
191    }
192
193    /// Creates a OneToMany cardinality.
194    pub fn one_to_many(min: u32, max: u32) -> Self {
195        Self::OneToMany {
196            min,
197            max: max.max(min),
198        }
199    }
200
201    /// Creates a ManyToOne cardinality.
202    pub fn many_to_one(min: u32, max: u32) -> Self {
203        Self::ManyToOne {
204            min,
205            max: max.max(min),
206        }
207    }
208
209    /// Creates a ManyToMany cardinality.
210    pub fn many_to_many(min_per_source: u32, max_per_source: u32) -> Self {
211        Self::ManyToMany {
212            min_per_source,
213            max_per_source: max_per_source.max(min_per_source),
214        }
215    }
216
217    /// Returns the minimum and maximum counts for this cardinality.
218    pub fn bounds(&self) -> (u32, u32) {
219        match self {
220            Self::OneToOne => (1, 1),
221            Self::OneToMany { min, max } => (*min, *max),
222            Self::ManyToOne { min, max } => (*min, *max),
223            Self::ManyToMany {
224                min_per_source,
225                max_per_source,
226            } => (*min_per_source, *max_per_source),
227        }
228    }
229
230    /// Checks if this cardinality allows multiple targets.
231    pub fn is_multi_target(&self) -> bool {
232        matches!(self, Self::OneToMany { .. } | Self::ManyToMany { .. })
233    }
234
235    /// Checks if this cardinality allows multiple sources.
236    pub fn is_multi_source(&self) -> bool {
237        matches!(self, Self::ManyToOne { .. } | Self::ManyToMany { .. })
238    }
239}
240
241/// Property generation rule for relationships.
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct PropertyGenerationRule {
244    /// Property name.
245    pub name: String,
246    /// Property value type.
247    pub value_type: PropertyValueType,
248    /// Property generator.
249    pub generator: PropertyGenerator,
250}
251
252impl PropertyGenerationRule {
253    /// Creates a new property generation rule.
254    pub fn new(
255        name: impl Into<String>,
256        value_type: PropertyValueType,
257        generator: PropertyGenerator,
258    ) -> Self {
259        Self {
260            name: name.into(),
261            value_type,
262            generator,
263        }
264    }
265
266    /// Creates a constant string property.
267    pub fn constant_string(name: impl Into<String>, value: impl Into<String>) -> Self {
268        Self::new(
269            name,
270            PropertyValueType::String,
271            PropertyGenerator::Constant(Value::String(value.into())),
272        )
273    }
274
275    /// Creates a constant numeric property.
276    pub fn constant_number(name: impl Into<String>, value: f64) -> Self {
277        Self::new(
278            name,
279            PropertyValueType::Float,
280            PropertyGenerator::Constant(Value::Number(
281                serde_json::Number::from_f64(value).unwrap_or_else(|| serde_json::Number::from(0)),
282            )),
283        )
284    }
285
286    /// Creates a range property.
287    pub fn range(name: impl Into<String>, min: f64, max: f64) -> Self {
288        Self::new(
289            name,
290            PropertyValueType::Float,
291            PropertyGenerator::Range { min, max },
292        )
293    }
294
295    /// Creates a random choice property.
296    pub fn random_choice(name: impl Into<String>, choices: Vec<Value>) -> Self {
297        Self::new(
298            name,
299            PropertyValueType::String,
300            PropertyGenerator::RandomChoice(choices),
301        )
302    }
303}
304
305/// Property value type.
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum PropertyValueType {
309    /// String value.
310    String,
311    /// Integer value.
312    Integer,
313    /// Float value.
314    Float,
315    /// Boolean value.
316    Boolean,
317    /// Date/time value.
318    DateTime,
319}
320
321/// Property generator for relationship properties.
322#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub enum PropertyGenerator {
325    /// Constant value.
326    Constant(Value),
327    /// Random choice from a list.
328    RandomChoice(Vec<Value>),
329    /// Range of numeric values.
330    Range {
331        /// Minimum value.
332        min: f64,
333        /// Maximum value.
334        max: f64,
335    },
336    /// Copy from source node property.
337    FromSourceProperty(String),
338    /// Copy from target node property.
339    FromTargetProperty(String),
340    /// UUID generator.
341    Uuid,
342    /// Timestamp generator.
343    Timestamp,
344}
345
346impl Default for PropertyGenerator {
347    fn default() -> Self {
348        Self::Constant(Value::Null)
349    }
350}
351
352/// Relationship validation result.
353#[derive(Debug, Clone)]
354pub struct RelationshipValidation {
355    /// Whether the relationship is valid.
356    pub valid: bool,
357    /// Validation errors.
358    pub errors: Vec<String>,
359    /// Validation warnings.
360    pub warnings: Vec<String>,
361}
362
363impl RelationshipValidation {
364    /// Creates a valid result.
365    pub fn valid() -> Self {
366        Self {
367            valid: true,
368            errors: Vec::new(),
369            warnings: Vec::new(),
370        }
371    }
372
373    /// Creates an invalid result with the given error.
374    pub fn invalid(error: impl Into<String>) -> Self {
375        Self {
376            valid: false,
377            errors: vec![error.into()],
378            warnings: Vec::new(),
379        }
380    }
381
382    /// Adds an error.
383    pub fn with_error(mut self, error: impl Into<String>) -> Self {
384        self.valid = false;
385        self.errors.push(error.into());
386        self
387    }
388
389    /// Adds a warning.
390    pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
391        self.warnings.push(warning.into());
392        self
393    }
394}
395
396/// Common relationship type definitions for accounting domain.
397pub mod accounting {
398    use super::*;
399
400    /// Creates a "debits" relationship configuration.
401    pub fn debits_relationship() -> RelationshipTypeConfig {
402        RelationshipTypeConfig::new("debits", "journal_entry", "account")
403            .with_cardinality(CardinalityRule::one_to_many(1, 5))
404            .required(true)
405            .with_property(PropertyGenerationRule::range("amount", 0.01, 1_000_000.0))
406    }
407
408    /// Creates a "credits" relationship configuration.
409    pub fn credits_relationship() -> RelationshipTypeConfig {
410        RelationshipTypeConfig::new("credits", "journal_entry", "account")
411            .with_cardinality(CardinalityRule::one_to_many(1, 5))
412            .required(true)
413            .with_property(PropertyGenerationRule::range("amount", 0.01, 1_000_000.0))
414    }
415
416    /// Creates a "created_by" relationship configuration.
417    pub fn created_by_relationship() -> RelationshipTypeConfig {
418        RelationshipTypeConfig::new("created_by", "journal_entry", "user")
419            .with_cardinality(CardinalityRule::ManyToOne { min: 1, max: 1 })
420            .required(true)
421    }
422
423    /// Creates an "approved_by" relationship configuration.
424    pub fn approved_by_relationship() -> RelationshipTypeConfig {
425        RelationshipTypeConfig::new("approved_by", "journal_entry", "user")
426            .with_cardinality(CardinalityRule::ManyToOne { min: 0, max: 1 })
427    }
428
429    /// Creates a "belongs_to" relationship configuration for vendor to company.
430    pub fn vendor_belongs_to_company() -> RelationshipTypeConfig {
431        RelationshipTypeConfig::new("belongs_to", "vendor", "company")
432            .with_cardinality(CardinalityRule::ManyToOne { min: 1, max: 1 })
433            .required(true)
434    }
435
436    /// Creates a "references" relationship configuration for document chains.
437    pub fn document_references() -> RelationshipTypeConfig {
438        RelationshipTypeConfig::new("references", "document", "document")
439            .with_cardinality(CardinalityRule::ManyToMany {
440                min_per_source: 0,
441                max_per_source: 5,
442            })
443            .with_property(PropertyGenerationRule::random_choice(
444                "reference_type",
445                vec![
446                    Value::String("follow_on".into()),
447                    Value::String("reversal".into()),
448                    Value::String("payment".into()),
449                ],
450            ))
451    }
452
453    /// Creates a default accounting relationship configuration.
454    pub fn default_accounting_config() -> RelationshipConfig {
455        RelationshipConfig::with_types(vec![
456            debits_relationship(),
457            credits_relationship(),
458            created_by_relationship(),
459            approved_by_relationship(),
460        ])
461        .allow_orphans(true)
462        .orphan_probability(0.01)
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_cardinality_bounds() {
472        let one_to_one = CardinalityRule::one_to_one();
473        assert_eq!(one_to_one.bounds(), (1, 1));
474
475        let one_to_many = CardinalityRule::one_to_many(2, 5);
476        assert_eq!(one_to_many.bounds(), (2, 5));
477
478        let many_to_one = CardinalityRule::many_to_one(1, 3);
479        assert_eq!(many_to_one.bounds(), (1, 3));
480
481        let many_to_many = CardinalityRule::many_to_many(1, 10);
482        assert_eq!(many_to_many.bounds(), (1, 10));
483    }
484
485    #[test]
486    fn test_cardinality_multi() {
487        assert!(!CardinalityRule::one_to_one().is_multi_target());
488        assert!(!CardinalityRule::one_to_one().is_multi_source());
489
490        assert!(CardinalityRule::one_to_many(1, 5).is_multi_target());
491        assert!(!CardinalityRule::one_to_many(1, 5).is_multi_source());
492
493        assert!(!CardinalityRule::many_to_one(1, 5).is_multi_target());
494        assert!(CardinalityRule::many_to_one(1, 5).is_multi_source());
495
496        assert!(CardinalityRule::many_to_many(1, 5).is_multi_target());
497        assert!(CardinalityRule::many_to_many(1, 5).is_multi_source());
498    }
499
500    #[test]
501    fn test_relationship_type_config() {
502        let config = RelationshipTypeConfig::new("debits", "journal_entry", "account")
503            .with_cardinality(CardinalityRule::one_to_many(1, 5))
504            .with_weight(2.0)
505            .required(true)
506            .directed(true);
507
508        assert_eq!(config.name, "debits");
509        assert_eq!(config.source_type, "journal_entry");
510        assert_eq!(config.target_type, "account");
511        assert_eq!(config.weight, 2.0);
512        assert!(config.required);
513        assert!(config.directed);
514    }
515
516    #[test]
517    fn test_property_generation_rule() {
518        let constant = PropertyGenerationRule::constant_string("status", "active");
519        assert_eq!(constant.name, "status");
520
521        let range = PropertyGenerationRule::range("amount", 0.0, 1000.0);
522        assert_eq!(range.name, "amount");
523
524        let choice = PropertyGenerationRule::random_choice(
525            "type",
526            vec![Value::String("A".into()), Value::String("B".into())],
527        );
528        assert_eq!(choice.name, "type");
529    }
530
531    #[test]
532    fn test_relationship_config() {
533        let config = RelationshipConfig::default()
534            .allow_orphans(false)
535            .orphan_probability(0.05)
536            .allow_circular(true)
537            .max_circular_depth(5);
538
539        assert!(!config.allow_orphans);
540        assert_eq!(config.orphan_probability, 0.05);
541        assert!(config.allow_circular);
542        assert_eq!(config.max_circular_depth, 5);
543    }
544
545    #[test]
546    fn test_accounting_relationships() {
547        let config = accounting::default_accounting_config();
548        assert_eq!(config.relationship_types.len(), 4);
549
550        let debits = config
551            .relationship_types
552            .iter()
553            .find(|t| t.name == "debits")
554            .unwrap();
555        assert!(debits.required);
556        assert_eq!(debits.source_type, "journal_entry");
557        assert_eq!(debits.target_type, "account");
558    }
559
560    #[test]
561    fn test_validation() {
562        let valid = RelationshipValidation::valid();
563        assert!(valid.valid);
564        assert!(valid.errors.is_empty());
565
566        let invalid = RelationshipValidation::invalid("Missing source")
567            .with_warning("Consider adding target");
568        assert!(!invalid.valid);
569        assert_eq!(invalid.errors.len(), 1);
570        assert_eq!(invalid.warnings.len(), 1);
571    }
572}