1use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct RelationshipConfig {
12 pub relationship_types: Vec<RelationshipTypeConfig>,
14 pub allow_orphans: bool,
16 pub orphan_probability: f64,
18 pub allow_circular: bool,
20 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 pub fn with_types(types: Vec<RelationshipTypeConfig>) -> Self {
39 Self {
40 relationship_types: types,
41 ..Default::default()
42 }
43 }
44
45 pub fn allow_orphans(mut self, allow: bool) -> Self {
47 self.allow_orphans = allow;
48 self
49 }
50
51 pub fn orphan_probability(mut self, prob: f64) -> Self {
53 self.orphan_probability = prob.clamp(0.0, 1.0);
54 self
55 }
56
57 pub fn allow_circular(mut self, allow: bool) -> Self {
59 self.allow_circular = allow;
60 self
61 }
62
63 pub fn max_circular_depth(mut self, depth: u32) -> Self {
65 self.max_circular_depth = depth;
66 self
67 }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct RelationshipTypeConfig {
73 pub name: String,
75 pub source_type: String,
77 pub target_type: String,
79 pub cardinality: CardinalityRule,
81 pub weight: f64,
83 pub properties: Vec<PropertyGenerationRule>,
85 pub required: bool,
87 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 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 pub fn with_cardinality(mut self, cardinality: CardinalityRule) -> Self {
123 self.cardinality = cardinality;
124 self
125 }
126
127 pub fn with_weight(mut self, weight: f64) -> Self {
129 self.weight = weight.max(0.0);
130 self
131 }
132
133 pub fn with_property(mut self, property: PropertyGenerationRule) -> Self {
135 self.properties.push(property);
136 self
137 }
138
139 pub fn required(mut self, required: bool) -> Self {
141 self.required = required;
142 self
143 }
144
145 pub fn directed(mut self, directed: bool) -> Self {
147 self.directed = directed;
148 self
149 }
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum CardinalityRule {
156 OneToOne,
158 OneToMany {
160 min: u32,
162 max: u32,
164 },
165 ManyToOne {
167 min: u32,
169 max: u32,
171 },
172 ManyToMany {
174 min_per_source: u32,
176 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 pub fn one_to_one() -> Self {
190 Self::OneToOne
191 }
192
193 pub fn one_to_many(min: u32, max: u32) -> Self {
195 Self::OneToMany {
196 min,
197 max: max.max(min),
198 }
199 }
200
201 pub fn many_to_one(min: u32, max: u32) -> Self {
203 Self::ManyToOne {
204 min,
205 max: max.max(min),
206 }
207 }
208
209 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 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 pub fn is_multi_target(&self) -> bool {
232 matches!(self, Self::OneToMany { .. } | Self::ManyToMany { .. })
233 }
234
235 pub fn is_multi_source(&self) -> bool {
237 matches!(self, Self::ManyToOne { .. } | Self::ManyToMany { .. })
238 }
239}
240
241#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct PropertyGenerationRule {
244 pub name: String,
246 pub value_type: PropertyValueType,
248 pub generator: PropertyGenerator,
250}
251
252impl PropertyGenerationRule {
253 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 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum PropertyValueType {
309 String,
311 Integer,
313 Float,
315 Boolean,
317 DateTime,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323#[serde(rename_all = "snake_case")]
324pub enum PropertyGenerator {
325 Constant(Value),
327 RandomChoice(Vec<Value>),
329 Range {
331 min: f64,
333 max: f64,
335 },
336 FromSourceProperty(String),
338 FromTargetProperty(String),
340 Uuid,
342 Timestamp,
344}
345
346impl Default for PropertyGenerator {
347 fn default() -> Self {
348 Self::Constant(Value::Null)
349 }
350}
351
352#[derive(Debug, Clone)]
354pub struct RelationshipValidation {
355 pub valid: bool,
357 pub errors: Vec<String>,
359 pub warnings: Vec<String>,
361}
362
363impl RelationshipValidation {
364 pub fn valid() -> Self {
366 Self {
367 valid: true,
368 errors: Vec::new(),
369 warnings: Vec::new(),
370 }
371 }
372
373 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 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 pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
391 self.warnings.push(warning.into());
392 self
393 }
394}
395
396pub mod accounting {
398 use super::*;
399
400 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 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 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 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 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 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 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}