jgd_rs/type_spec/entity.rs
1use std::collections::{HashMap, HashSet};
2
3use indexmap::IndexMap;
4use rand::{rngs::StdRng, SeedableRng};
5use serde::Deserialize;
6use serde_json::Value;
7use crate::{type_spec::{Count, Field, GetCount, JsonGenerator}, JgdGeneratorError, LocalConfig};
8
9/// Creates a fingerprint for uniqueness checking based on specified fields.
10///
11/// This function extracts values from the specified fields in the JSON object
12/// and creates a string fingerprint that can be used to check for duplicates.
13///
14/// # Arguments
15///
16/// * `obj` - The JSON object to create a fingerprint for
17/// * `unique_fields` - Vector of field names to include in the fingerprint
18///
19/// # Returns
20///
21/// A string fingerprint representing the combination of unique field values
22fn fingerprint(obj: &Value, unique_fields: &[String]) -> String {
23 let mut parts = Vec::new();
24
25 if let Value::Object(map) = obj {
26 for field in unique_fields {
27 if let Some(value) = map.get(field) {
28 // Convert the value to a string representation for fingerprinting
29 let value_str = match value {
30 Value::String(s) => s.clone(),
31 Value::Number(n) => n.to_string(),
32 Value::Bool(b) => b.to_string(),
33 Value::Null => "null".to_string(),
34 _ => serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string()),
35 };
36 parts.push(format!("{}:{}", field, value_str));
37 } else {
38 parts.push(format!("{}:missing", field));
39 }
40 }
41 }
42
43 parts.join("|")
44}
45
46/// Represents an entity specification for generating structured data objects in JGD schemas.
47///
48/// `Entity` defines a collection of named fields that together form a structured data object.
49/// It supports generating single objects or arrays of objects, with optional uniqueness
50/// constraints to ensure no duplicate entities based on specified field combinations.
51///
52/// # JGD Schema Integration
53///
54/// Entities are the primary building blocks for creating structured data in JGD schemas.
55/// They correspond to objects in JSON and can contain multiple fields of different types,
56/// creating realistic data structures for testing and development.
57///
58/// # Features
59///
60/// - **Multiple Fields**: Define complex objects with various field types
61/// - **Count Control**: Generate single objects or arrays of objects
62/// - **Uniqueness Constraints**: Ensure entities are unique based on specified fields
63/// - **Seeding Support**: Optional seed for deterministic generation
64/// - **Cross-References**: Fields can reference other generated entities
65///
66/// # JSON Schema Representation
67///
68/// ```json
69/// {
70/// "entity": {
71/// "count": 10,
72/// "unique_by": ["id", "email"],
73/// "fields": {
74/// "id": "${uuid}",
75/// "name": "${name.fullName}",
76/// "email": "${internet.email}",
77/// "age": {
78/// "number": {
79/// "min": 18,
80/// "max": 65,
81/// "integer": true
82/// }
83/// }
84/// }
85/// }
86/// }
87/// ```
88///
89/// # Uniqueness Constraints
90///
91/// The `unique_by` field allows specifying which field combinations must be unique
92/// across all generated entities. This is useful for:
93/// - Primary key constraints (IDs, usernames, emails)
94/// - Composite uniqueness (user + project combinations)
95/// - Realistic data constraints (no duplicate SSNs, etc.)
96///
97/// # Examples
98///
99/// ```rust,ignore
100/// use jgd_rs::{Entity, Field, Count, JsonGenerator, GeneratorConfig};
101/// use indexmap::IndexMap;
102///
103/// let mut config = GeneratorConfig::new("EN", Some(42));
104///
105/// // Create user entities with unique emails
106/// let mut fields = IndexMap::new();
107/// fields.insert("id".to_string(), Field::Str("${uuid}".to_string()));
108/// fields.insert("name".to_string(), Field::Str("${name.fullName}".to_string()));
109/// fields.insert("email".to_string(), Field::Str("${internet.email}".to_string()));
110///
111/// let entity = Entity {
112/// count: Some(Count::Fixed(5)),
113/// seed: None,
114/// unique_by: vec!["email".to_string()],
115/// fields,
116/// };
117///
118/// let result = entity.generate(&mut config);
119/// // Generates an array of 5 user objects with unique emails
120/// ```
121#[derive(Debug, Deserialize, Clone)]
122pub struct Entity {
123 /// Optional count specification for the number of entities to generate.
124 ///
125 /// Determines whether to generate a single entity object or an array of entities:
126 /// - **Some(count)**: Generates an array with the specified count
127 /// - **None**: Generates a single entity object (not wrapped in an array)
128 ///
129 /// This field works with the `GetCount` trait to provide flexible count specifications
130 /// including fixed counts and ranges.
131 ///
132 /// # JSON Schema Mapping
133 ///
134 /// ```json
135 /// {
136 /// "entity": {
137 /// "count": 5, // Generate 5 entities
138 /// "fields": { ... }
139 /// }
140 /// }
141 /// ```
142 ///
143 /// Or omitted for single entity:
144 /// ```json
145 /// {
146 /// "entity": {
147 /// "fields": { ... } // Generate single entity
148 /// }
149 /// }
150 /// ```
151 pub count: Option<Count>,
152
153 /// Optional seed for deterministic entity generation.
154 ///
155 /// When specified, this seed can be used to ensure reproducible entity generation
156 /// for testing and debugging purposes. Currently preserved for future implementation
157 /// of per-entity seeding.
158 ///
159 /// # Future Enhancement
160 ///
161 /// This field is planned for use in scenarios where specific entities need
162 /// their own deterministic generation context, separate from the global
163 /// generator configuration seed.
164 ///
165 /// # JSON Schema Mapping
166 ///
167 /// ```json
168 /// {
169 /// "entity": {
170 /// "seed": 12345,
171 /// "fields": { ... }
172 /// }
173 /// }
174 /// ```
175 #[serde(default)]
176 pub seed: Option<u64>,
177
178 /// Fields that must be unique across all generated entities.
179 ///
180 /// This vector specifies which field combinations must be unique when generating
181 /// multiple entities. The uniqueness check creates a fingerprint from the specified
182 /// fields and ensures no duplicates are generated.
183 ///
184 /// # Uniqueness Behavior
185 ///
186 /// - **Empty vector**: No uniqueness constraints (default)
187 /// - **Single field**: Ensures that field is unique across entities
188 /// - **Multiple fields**: Ensures the combination of fields is unique
189 ///
190 /// # Performance Considerations
191 ///
192 /// Uniqueness checking has a maximum retry limit (1000 attempts) to prevent
193 /// infinite loops when constraints are too restrictive relative to the possible
194 /// value space.
195 ///
196 /// # JSON Schema Mapping
197 ///
198 /// ```json
199 /// {
200 /// "entity": {
201 /// "unique_by": ["id"], // Single field uniqueness
202 /// "fields": { ... }
203 /// }
204 /// }
205 /// ```
206 ///
207 /// Or multiple fields:
208 /// ```json
209 /// {
210 /// "entity": {
211 /// "unique_by": ["user_id", "project_id"], // Composite uniqueness
212 /// "fields": { ... }
213 /// }
214 /// }
215 /// ```
216 ///
217 /// # Examples
218 ///
219 /// ```rust,ignore
220 /// // Ensure email uniqueness
221 /// unique_by: vec!["email".to_string()]
222 ///
223 /// // Ensure user+project combination uniqueness
224 /// unique_by: vec!["user_id".to_string(), "project_id".to_string()]
225 /// ```
226 #[serde(default)]
227 pub unique_by: Vec<String>,
228
229 /// The collection of fields that make up the entity structure.
230 ///
231 /// This `IndexMap` defines the schema for the generated entities, mapping field
232 /// names to their generation specifications. The use of `IndexMap` preserves
233 /// the field ordering defined in the schema.
234 ///
235 /// # Field Types
236 ///
237 /// Each field can be any valid `Field` type:
238 /// - **Primitive fields**: Strings, numbers, booleans
239 /// - **Complex fields**: Nested entities, arrays
240 /// - **Template fields**: String templates with placeholder substitution
241 /// - **Reference fields**: Cross-references to other generated data
242 /// - **Optional fields**: Fields that may or may not be present
243 ///
244 /// # Field Independence
245 ///
246 /// Each field is generated independently, allowing for:
247 /// - Different fake data for each field
248 /// - Independent randomization
249 /// - Cross-field references when needed
250 ///
251 /// # JSON Schema Mapping
252 ///
253 /// ```json
254 /// {
255 /// "entity": {
256 /// "fields": {
257 /// "id": "${uuid}",
258 /// "name": "${name.fullName}",
259 /// "age": {
260 /// "number": {
261 /// "min": 18,
262 /// "max": 65,
263 /// "integer": true
264 /// }
265 /// },
266 /// "address": {
267 /// "entity": {
268 /// "fields": {
269 /// "street": "${address.streetAddress}",
270 /// "city": "${address.cityName}"
271 /// }
272 /// }
273 /// }
274 /// }
275 /// }
276 /// }
277 /// ```
278 pub fields: IndexMap<String, Field>,
279}
280
281impl JsonGenerator for Entity {
282 /// Generates entities according to the entity specification with uniqueness constraints.
283 ///
284 /// This method creates either a single entity object or an array of entities,
285 /// depending on the count specification. When uniqueness constraints are specified,
286 /// it ensures that no duplicate entities are generated based on the unique field
287 /// combinations.
288 ///
289 /// # Generation Process
290 ///
291 /// 1. **Count Determination**: Uses count specification to determine output format
292 /// 2. **Uniqueness Setup**: Initializes tracking for unique field combinations
293 /// 3. **Entity Generation**: Creates entities with uniqueness validation
294 /// 4. **Output Formation**: Returns single object or array based on count
295 ///
296 /// # Arguments
297 ///
298 /// * `config` - Mutable reference to the generator configuration containing
299 /// RNG, fake data generators, and cross-reference state
300 ///
301 /// # Returns
302 ///
303 /// - **Single Entity**: `Value::Object` when count is None
304 /// - **Entity Array**: `Value::Array` when count is specified
305 ///
306 /// # Uniqueness Algorithm
307 ///
308 /// When `unique_by` fields are specified:
309 /// 1. Generate a candidate entity
310 /// 2. Create a fingerprint from the unique fields
311 /// 3. Check if fingerprint already exists
312 /// 4. If unique, add to results; if duplicate, retry
313 /// 5. Maximum 1000 attempts per entity to prevent infinite loops
314 ///
315 /// # Error Handling
316 ///
317 /// - **Uniqueness Failures**: Logs warning and stops generation after max attempts
318 /// - **Missing Fields**: Includes "missing" in fingerprint for absent unique fields
319 /// - **Complex Values**: Serializes complex field values for fingerprinting
320 ///
321 /// # Examples
322 ///
323 /// ```rust,ignore
324 /// use jgd_rs::{Entity, Field, Count, JsonGenerator, GeneratorConfig};
325 /// use indexmap::IndexMap;
326 ///
327 /// let mut config = GeneratorConfig::new("EN", Some(42));
328 ///
329 /// // Single entity generation
330 /// let mut fields = IndexMap::new();
331 /// fields.insert("name".to_string(), Field::Str("${name.fullName}".to_string()));
332 /// fields.insert("age".to_string(), Field::I64(25));
333 ///
334 /// let entity = Entity {
335 /// count: None, // Single entity
336 /// seed: None,
337 /// unique_by: vec![],
338 /// fields,
339 /// };
340 ///
341 /// let result = entity.generate(&mut config);
342 /// // Returns: {"name": "John Doe", "age": 25}
343 ///
344 /// // Array of unique entities
345 /// let entity_array = Entity {
346 /// count: Some(Count::Fixed(3)),
347 /// unique_by: vec!["name".to_string()],
348 /// fields: fields.clone(),
349 /// };
350 ///
351 /// let result = entity_array.generate(&mut config);
352 /// // Returns: [{"name": "John", "age": 25}, {"name": "Jane", "age": 25}, ...]
353 /// ```
354 ///
355 /// # Performance Notes
356 ///
357 /// - **Memory Efficiency**: Pre-allocates result vector with known capacity
358 /// - **Uniqueness Overhead**: Fingerprinting and set operations add computational cost
359 /// - **Retry Limits**: Maximum attempts prevent infinite loops in constrained scenarios
360 /// - **Fingerprint Storage**: Memory usage grows with unique entity count
361 ///
362 /// # Uniqueness Constraints
363 ///
364 /// Consider the value space when setting uniqueness constraints:
365 /// - **Large Value Space**: Easy to generate unique entities
366 /// - **Small Value Space**: May hit retry limits with restrictive constraints
367 /// - **Template Variety**: Ensure fake data templates provide sufficient variation
368 fn generate(&self, config: &mut super::GeneratorConfig, local_config: Option<&mut LocalConfig>
369 ) -> Result<Value, JgdGeneratorError> {
370 let count_items = self.count.count(config);
371
372 let mut items = Vec::with_capacity(count_items as usize);
373 let mut unique_sets: HashMap<String, HashSet<String>> = HashMap::new();
374
375 let rng = self.seed.map(StdRng::seed_from_u64);
376
377 let mut local_config =
378 LocalConfig::from_current_with_config(rng, Some(count_items), local_config);
379
380 let mut _attempts = 0;
381 const MAX_ATTEMPTS: usize = 1000; // Prevent infinite loops
382
383 for i in 0..count_items {
384 let mut obj = None;
385 local_config.set_index(i as usize);
386
387 // Try to generate a unique object
388 for _ in 0..MAX_ATTEMPTS {
389 _attempts += 1;
390 let candidate = self.fields.generate(config, Some(&mut local_config))?;
391
392 if !self.unique_by.is_empty() {
393 let fp = fingerprint(&candidate, &self.unique_by);
394 let set = unique_sets.entry(self.unique_by.join("|"))
395 .or_default();
396
397 if !set.contains(&fp) {
398 set.insert(fp);
399 obj = Some(candidate);
400 break;
401 }
402 // If fingerprint already exists, try again
403 } else {
404 // No uniqueness constraints
405 obj = Some(candidate);
406 break;
407 }
408 }
409
410 if let Some(generated_obj) = obj {
411 if self.count.is_none() {
412 return Ok(generated_obj);
413 }
414 items.push(generated_obj);
415 } else {
416 // Failed to generate a unique object after MAX_ATTEMPTS
417 // This can happen if the uniqueness constraints are too restrictive
418 // relative to the possible value space
419 eprintln!("Warning: Failed to generate unique entity after {} attempts. Uniqueness constraints may be too restrictive.", MAX_ATTEMPTS);
420 break;
421 }
422 }
423
424 Ok(Value::Array(items))
425 }
426}
427
428impl JsonGenerator for IndexMap<String, Entity> {
429 /// Generates a collection of named entities and manages cross-references.
430 ///
431 /// This implementation generates multiple entities defined in an `IndexMap`,
432 /// where each entity has a name and specification. It handles both the generation
433 /// of individual entities and the management of cross-references between them.
434 ///
435 /// # Cross-Reference Management
436 ///
437 /// Each generated entity is stored in the global `gen_value` map using its name
438 /// as the key. This enables:
439 /// - References between entities using path notation
440 /// - Consistent data relationships across the generated dataset
441 /// - Template substitution with previously generated values
442 ///
443 /// # Generation Process
444 ///
445 /// 1. **Entity Generation**: Generate each named entity independently
446 /// 2. **Cross-Reference Storage**: Store each entity in the global reference map
447 /// 3. **Object Construction**: Build the final JSON object with all entities
448 ///
449 /// # Arguments
450 ///
451 /// * `config` - Mutable reference to the generator configuration containing
452 /// the global state for cross-references and generation context
453 ///
454 /// # Returns
455 ///
456 /// A `Value::Object` containing all generated entities, where keys are entity
457 /// names and values are the generated entity data (objects or arrays).
458 ///
459 /// # Cross-Reference Usage
460 ///
461 /// Once entities are generated and stored, they can be referenced in templates:
462 /// - `"author": "${users.name}"` - Reference user entity's name
463 /// - `"post_author": "${posts.author}"` - Reference post entity's author
464 ///
465 /// # JGD Schema Example
466 /// ```json
467 /// {
468 /// "root": {
469 /// "users": {
470 /// "count": { "fixed": 2 },
471 /// "fields": {
472 /// "id": { "number": { "from": 1, "to": 1000, "type": "integer" } },
473 /// "name": { "fake": "name.firstName" }
474 /// }
475 /// },
476 /// "posts": {
477 /// "count": { "fixed": 3 },
478 /// "fields": {
479 /// "user_id": "${users.id}",
480 /// "title": { "fake": "lorem.sentence(3)" }
481 /// }
482 /// }
483 /// }
484 /// }
485 /// ```
486 ///
487 /// # Examples
488 ///
489 /// ```rust,ignore
490 /// use jgd_rs::{Entity, Field, JsonGenerator, GeneratorConfig};
491 /// use indexmap::IndexMap;
492 ///
493 /// let mut config = GeneratorConfig::new("EN", Some(42));
494 /// let mut entities = IndexMap::new();
495 ///
496 /// // Define user entity
497 /// let mut user_fields = IndexMap::new();
498 /// user_fields.insert("id".to_string(), Field::Str("${uuid}".to_string()));
499 /// user_fields.insert("name".to_string(), Field::Str("${name.fullName}".to_string()));
500 ///
501 /// entities.insert("users".to_string(), Entity {
502 /// count: Some(Count::Fixed(2)),
503 /// seed: None,
504 /// unique_by: vec!["id".to_string()],
505 /// fields: user_fields,
506 /// });
507 ///
508 /// // Define post entity that references users
509 /// let mut post_fields = IndexMap::new();
510 /// post_fields.insert("title".to_string(), Field::Str("${lorem.sentence}".to_string()));
511 /// post_fields.insert("author_id".to_string(), Field::Ref {
512 /// r#ref: "users.id".to_string()
513 /// });
514 ///
515 /// entities.insert("posts".to_string(), Entity {
516 /// count: Some(Count::Fixed(3)),
517 /// seed: None,
518 /// unique_by: vec![],
519 /// fields: post_fields,
520 /// });
521 ///
522 /// let result = entities.generate(&mut config);
523 /// // Returns: {
524 /// // "users": [{"id": "...", "name": "..."}, ...],
525 /// // "posts": [{"title": "...", "author_id": "..."}, ...]
526 /// // }
527 /// ```
528 ///
529 /// # Performance Notes
530 ///
531 /// - **Sequential Generation**: Entities are generated in insertion order
532 /// - **Reference Storage**: Each entity is cloned for storage in gen_value
533 /// - **Memory Usage**: Stores both final result and reference copies
534 /// - **Order Dependency**: Earlier entities can be referenced by later ones
535 fn generate(&self, config: &mut super::GeneratorConfig, local_config: Option<&mut LocalConfig>
536 ) -> Result<Value, JgdGeneratorError> {
537 let mut local_config =
538 LocalConfig::from_current_with_config(None, None, local_config);
539
540 let mut map = serde_json::Map::new();
541 for (name, entity) in self {
542 local_config.entity_name = Some(name.clone());
543 let generated = entity.generate(config, Some(&mut local_config))?;
544 map.insert(name.clone(), generated.clone());
545
546 config.gen_value.insert(name.clone(), generated);
547 }
548
549 Ok(Value::Object(map))
550 }
551}
552
553#[cfg(test)]
554mod tests {
555 use super::*;
556 use crate::type_spec::{GeneratorConfig, NumberSpec};
557
558 fn create_test_config(seed: Option<u64>) -> GeneratorConfig {
559 GeneratorConfig::new("EN", seed)
560 }
561
562 #[test]
563 fn test_fingerprint_single_field() {
564 let obj = serde_json::json!({
565 "id": "12345",
566 "name": "John Doe"
567 });
568
569 let fp = fingerprint(&obj, &["id".to_string()]);
570 assert_eq!(fp, "id:12345");
571 }
572
573 #[test]
574 fn test_fingerprint_multiple_fields() {
575 let obj = serde_json::json!({
576 "id": "12345",
577 "name": "John Doe",
578 "email": "john@example.com"
579 });
580
581 let fp = fingerprint(&obj, &["id".to_string(), "email".to_string()]);
582 assert_eq!(fp, "id:12345|email:john@example.com");
583 }
584
585 #[test]
586 fn test_fingerprint_missing_field() {
587 let obj = serde_json::json!({
588 "id": "12345",
589 "name": "John Doe"
590 });
591
592 let fp = fingerprint(&obj, &["id".to_string(), "missing".to_string()]);
593 assert_eq!(fp, "id:12345|missing:missing");
594 }
595
596 #[test]
597 fn test_fingerprint_different_types() {
598 let obj = serde_json::json!({
599 "id": 12345,
600 "active": true,
601 "score": 98.5,
602 "data": null,
603 "tags": ["rust", "json"]
604 });
605
606 let fp = fingerprint(&obj, &[
607 "id".to_string(),
608 "active".to_string(),
609 "score".to_string(),
610 "data".to_string(),
611 "tags".to_string()
612 ]);
613
614 assert!(fp.contains("id:12345"));
615 assert!(fp.contains("active:true"));
616 assert!(fp.contains("score:98.5"));
617 assert!(fp.contains("data:null"));
618 assert!(fp.contains("tags:"));
619 }
620
621 #[test]
622 fn test_entity_single_generation() {
623 let mut config = create_test_config(Some(42));
624 let mut fields = IndexMap::new();
625 fields.insert("name".to_string(), Field::Str("John".to_string()));
626 fields.insert("age".to_string(), Field::I64(30));
627
628 let entity = Entity {
629 count: None,
630 seed: None,
631 unique_by: vec![],
632 fields,
633 };
634
635 let result = entity.generate(&mut config, None);
636 assert!(result.is_ok());
637
638 if let Ok(result) = result {
639 match result {
640 Value::Object(obj) => {
641 assert_eq!(obj.get("name"), Some(&Value::String("John".to_string())));
642 assert_eq!(obj.get("age"), Some(&Value::Number(serde_json::Number::from(30))));
643 }
644 _ => panic!("Expected object for single entity"),
645 }
646 }
647 }
648
649 #[test]
650 fn test_entity_array_generation() {
651 let mut config = create_test_config(Some(42));
652 let mut fields = IndexMap::new();
653 fields.insert("id".to_string(), Field::Number {
654 number: NumberSpec::new_integer(1.0, 1000.0)
655 });
656
657 let entity = Entity {
658 count: Some(Count::Fixed(3)),
659 seed: None,
660 unique_by: vec![],
661 fields,
662 };
663
664 let result = entity.generate(&mut config, None);
665 assert!(result.is_ok());
666
667 if let Ok(result) = result {
668 match result {
669 Value::Array(arr) => {
670 assert_eq!(arr.len(), 3);
671 for item in arr {
672 assert!(item.is_object());
673 if let Value::Object(obj) = item {
674 assert!(obj.contains_key("id"));
675 assert!(obj.get("id").unwrap().is_number());
676 }
677 }
678 }
679 _ => panic!("Expected array for entity with count"),
680 }
681 }
682 }
683
684 #[test]
685 fn test_entity_uniqueness_constraint() {
686 let mut config = create_test_config(Some(42));
687 let mut fields = IndexMap::new();
688 fields.insert("id".to_string(), Field::Number {
689 number: NumberSpec::new_integer(1.0, 3.0) // Small range to force uniqueness testing
690 });
691 fields.insert("name".to_string(), Field::Str("Test".to_string()));
692
693 let entity = Entity {
694 count: Some(Count::Fixed(3)),
695 seed: None,
696 unique_by: vec!["id".to_string()],
697 fields,
698 };
699
700 let result = entity.generate(&mut config, None);
701 assert!(result.is_ok());
702
703 if let Ok(result) = result {
704 match result {
705 Value::Array(arr) => {
706 // Should generate up to 3 unique entities (might be less due to small range)
707 assert!(arr.len() <= 3);
708
709 // Verify uniqueness
710 let mut seen_ids = std::collections::HashSet::new();
711 for item in arr {
712 if let Value::Object(obj) = item {
713 if let Some(Value::Number(id)) = obj.get("id") {
714 let id_value = id.as_i64().unwrap();
715 assert!(!seen_ids.contains(&id_value), "Duplicate ID found: {}", id_value);
716 seen_ids.insert(id_value);
717 }
718 }
719 }
720 }
721 _ => panic!("Expected array for entity with count"),
722 }
723 }
724 }
725
726 #[test]
727 fn test_entity_composite_uniqueness() {
728 let mut config = create_test_config(Some(42));
729 let mut fields = IndexMap::new();
730 fields.insert("category".to_string(), Field::Number {
731 number: NumberSpec::new_integer(1.0, 2.0)
732 });
733 fields.insert("subcategory".to_string(), Field::Number {
734 number: NumberSpec::new_integer(1.0, 2.0)
735 });
736
737 let entity = Entity {
738 count: Some(Count::Fixed(5)),
739 seed: None,
740 unique_by: vec!["category".to_string(), "subcategory".to_string()],
741 fields,
742 };
743
744 let result = entity.generate(&mut config, None);
745 assert!(result.is_ok());
746
747 if let Ok(result) = result {
748 match result {
749 Value::Array(arr) => {
750 // With 2x2 combinations, maximum should be 4 unique entities
751 assert!(arr.len() <= 4);
752
753 // Verify composite uniqueness
754 let mut seen_combinations = std::collections::HashSet::new();
755 for item in arr {
756 if let Value::Object(obj) = item {
757 let cat = obj.get("category").unwrap().as_i64().unwrap();
758 let subcat = obj.get("subcategory").unwrap().as_i64().unwrap();
759 let combination = (cat, subcat);
760
761 assert!(!seen_combinations.contains(&combination),
762 "Duplicate combination found: {:?}", combination);
763 seen_combinations.insert(combination);
764 }
765 }
766 }
767 _ => panic!("Expected array for entity with count"),
768 }
769 }
770 }
771
772 #[test]
773 fn test_entity_map_generation() {
774 let mut config = create_test_config(Some(42));
775 let mut entities = IndexMap::new();
776
777 // First entity
778 let mut user_fields = IndexMap::new();
779 user_fields.insert("id".to_string(), Field::I64(1));
780 user_fields.insert("name".to_string(), Field::Str("User".to_string()));
781
782 entities.insert("users".to_string(), Entity {
783 count: Some(Count::Fixed(1)),
784 seed: None,
785 unique_by: vec![],
786 fields: user_fields,
787 });
788
789 // Second entity
790 let mut post_fields = IndexMap::new();
791 post_fields.insert("title".to_string(), Field::Str("Post".to_string()));
792
793 entities.insert("posts".to_string(), Entity {
794 count: None,
795 seed: None,
796 unique_by: vec![],
797 fields: post_fields,
798 });
799
800 let result = entities.generate(&mut config, None);
801 assert!(result.is_ok());
802
803 if let Ok(result) = result {
804 match result {
805 Value::Object(obj) => {
806 assert_eq!(obj.len(), 2);
807 assert!(obj.contains_key("users"));
808 assert!(obj.contains_key("posts"));
809
810 // Users should be an array
811 assert!(obj.get("users").unwrap().is_array());
812
813 // Posts should be an object (no count specified)
814 assert!(obj.get("posts").unwrap().is_object());
815 }
816 _ => panic!("Expected object for entity map"),
817 }
818 }
819 }
820
821 #[test]
822 fn test_entity_cross_reference_storage() {
823 let mut config = create_test_config(Some(42));
824 let mut entities = IndexMap::new();
825
826 let mut user_fields = IndexMap::new();
827 user_fields.insert("name".to_string(), Field::Str("TestUser".to_string()));
828
829 entities.insert("users".to_string(), Entity {
830 count: None,
831 seed: None,
832 unique_by: vec![],
833 fields: user_fields,
834 });
835
836 let _ = entities.generate(&mut config, None);
837
838 // Verify that the entity was stored in gen_value for cross-references
839 assert!(config.gen_value.contains_key("users"));
840
841 let stored_user = config.gen_value.get("users").unwrap();
842 match stored_user {
843 Value::Object(obj) => {
844 assert_eq!(obj.get("name"), Some(&Value::String("TestUser".to_string())));
845 }
846 _ => panic!("Expected stored user to be an object"),
847 }
848 }
849}