Skip to main content

edifact_randomize/
lib.rs

1pub mod generators;
2pub mod seed_data;
3
4use thiserror::Error;
5
6#[derive(Debug, Error)]
7pub enum RandomizeError {
8    #[error("no randomization function available for field '{bo4e_name}'")]
9    NoGeneratorForField { bo4e_name: String },
10}
11
12/// Derive a deterministic per-field sub-seed from a base seed and a field name.
13///
14/// Uses FNV-1a-style hashing so different field names get different seeds.
15pub fn field_seed(base_seed: u64, bo4e_name: &str) -> u64 {
16    let mut h = base_seed;
17    for b in bo4e_name.bytes() {
18        h = h.wrapping_mul(1_099_511_628_211).wrapping_add(b as u64);
19    }
20    h
21}
22
23/// Randomize a single field. Returns the generated string value.
24///
25/// Dispatch order:
26/// 1. Exact bo4e_name match → domain-specific generator
27/// 2. valid_codes non-empty → uniform random pick from the list
28/// 3. format == "date" → YYYY-MM-DD (HTML5 date input)
29/// 4. bo4e_name ends with date suffixes (datum/Datum/Ab/Bis/Von) or starts with "gueltig" → YYYY-MM-DD
30/// 5. bo4e_name ends with "Referenz" or "referenz" → reference ID
31/// 6. Otherwise → Err(RandomizeError::NoGeneratorForField)
32pub fn randomize_field(
33    bo4e_name: &str,
34    valid_codes: &[String],
35    format: Option<&str>,
36    seed: u64,
37) -> Result<String, RandomizeError> {
38    use generators::*;
39    use seed_data::*;
40
41    match bo4e_name {
42        // Location IDs
43        "marktlokationsId" => Ok(generate_malo_id(seed)),
44        "messlokationsId" => Ok(generate_melo_id(seed)),
45        "netzlokationsId" => Ok(generate_nelo_id(seed)),
46        "steuerbareRessourceId" => Ok(generate_steuress_id(seed)),
47        "technischeRessourceId" => Ok(generate_techress_id(seed)),
48        "tranchenId" => Ok(generate_malo_id(seed.wrapping_add(7))),
49
50        // GLN identifiers
51        "identifikation" | "absenderCode" | "empfaengerCode" => Ok(generate_gln(seed)),
52
53        // Interchange-level fields
54        "interchangeRef" => Ok(generate_reference_id(seed)),
55        "datum" => Ok(generate_datum(seed)),
56        "zeit" => Ok(generate_zeit(seed)),
57
58        // Name fields
59        "nachname" => Ok(pick(NACHNAMEN, seed).to_string()),
60        "vorname" => Ok(pick(VORNAMEN, seed).to_string()),
61        "anrede" => Ok(pick(ANREDEN, seed).to_string()),
62        "titel" => Ok(pick(TITEL, seed).to_string()),
63
64        // Address fields — coherent from same address tuple
65        "strasse" => Ok(pick_address(seed).strasse.to_string()),
66        "hausnummer" => Ok(pick_address(seed).hausnummer.to_string()),
67        "ort" => Ok(pick_address(seed).ort.to_string()),
68        "postleitzahl" => Ok(pick_address(seed).plz.to_string()),
69        "region" => Ok(pick_address(seed).bundesland.to_string()),
70        "land" => Ok("DE".to_string()),
71
72        // Reference IDs
73        "vorgangId" => Ok(generate_reference_id(seed)),
74
75        // Enum: uniform pick from valid_codes (checked before suffix rules so dropdown fields
76        // always get a valid code even when their name matches a suffix pattern)
77        _ if !valid_codes.is_empty() => {
78            let idx = seed as usize % valid_codes.len();
79            Ok(valid_codes[idx].clone())
80        }
81
82        // Date fields by format hint
83        _ if format == Some("date") => Ok(generate_date_ymd(seed)),
84
85        // Date fields by name suffix (when no format hint and no valid_codes)
86        _ if bo4e_name.ends_with("datum")
87            || bo4e_name.ends_with("Datum")
88            || bo4e_name.ends_with("Ab")
89            || bo4e_name.ends_with("Bis")
90            || bo4e_name.ends_with("Von")
91            || bo4e_name.starts_with("gueltig") =>
92        {
93            Ok(generate_date_ymd(seed))
94        }
95
96        // Suffix-based reference rule
97        _ if bo4e_name.ends_with("Referenz") || bo4e_name.ends_with("referenz") => {
98            Ok(generate_reference_id(seed))
99        }
100
101        // No generator available
102        _ => Err(RandomizeError::NoGeneratorForField {
103            bo4e_name: bo4e_name.to_string(),
104        }),
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_malo_id_dispatch() {
114        let result = randomize_field("marktlokationsId", &[], None, 42).unwrap();
115        assert_eq!(result.len(), 11, "MaLo ID must be 11 digits");
116        assert!(result.chars().all(|c| c.is_ascii_digit()));
117    }
118
119    #[test]
120    fn test_melo_id_dispatch() {
121        let result = randomize_field("messlokationsId", &[], None, 42).unwrap();
122        assert!(result.starts_with("DE"));
123        assert_eq!(result.len(), 33);
124    }
125
126    #[test]
127    fn test_gln_dispatch() {
128        let result = randomize_field("absenderCode", &[], None, 42).unwrap();
129        assert_eq!(result.len(), 13);
130    }
131
132    #[test]
133    fn test_datum_dispatch() {
134        let result = randomize_field("datum", &[], None, 42).unwrap();
135        assert_eq!(result.len(), 6, "datum must be YYMMDD");
136        assert!(result.chars().all(|c| c.is_ascii_digit()));
137    }
138
139    #[test]
140    fn test_zeit_dispatch() {
141        let result = randomize_field("zeit", &[], None, 42).unwrap();
142        assert_eq!(result.len(), 4, "zeit must be HHMM");
143    }
144
145    #[test]
146    fn test_enum_pick_dispatch() {
147        let codes = vec!["E01".to_string(), "ZW4".to_string(), "E03".to_string()];
148        let result = randomize_field("someEnumField", &codes, None, 0).unwrap();
149        assert!(codes.contains(&result), "result must be from valid_codes");
150    }
151
152    #[test]
153    fn test_enum_pick_uniform() {
154        let codes = vec!["A".to_string(), "B".to_string(), "C".to_string()];
155        let r0 = randomize_field("field", &codes, None, 0).unwrap();
156        let r1 = randomize_field("field", &codes, None, 1).unwrap();
157        let r2 = randomize_field("field", &codes, None, 2).unwrap();
158        assert_eq!(r0, "A"); // 0 % 3 = 0
159        assert_eq!(r1, "B"); // 1 % 3 = 1
160        assert_eq!(r2, "C"); // 2 % 3 = 2
161    }
162
163    #[test]
164    fn test_date_format_dispatch() {
165        let result = randomize_field("someDate", &[], Some("date"), 42).unwrap();
166        assert_eq!(
167            result.len(),
168            10,
169            "date_ymd must be YYYY-MM-DD, got: {result}"
170        );
171        assert!(result.contains('-'));
172    }
173
174    #[test]
175    fn test_referenz_suffix_dispatch() {
176        let result = randomize_field("nachrichtenReferenz", &[], None, 42).unwrap();
177        assert_eq!(result.len(), 12, "reference must be 12 chars");
178        assert!(!result.contains('+'));
179        assert!(!result.contains(':'));
180    }
181
182    #[test]
183    fn test_referenz_with_valid_codes_picks_code_not_reference() {
184        // A dropdown field whose name ends in "Referenz" must pick from valid_codes, not generate a ref ID
185        let codes = vec!["E01".to_string(), "E02".to_string()];
186        let result = randomize_field("nachrichtenReferenz", &codes, None, 0).unwrap();
187        assert!(
188            codes.contains(&result),
189            "must pick from valid_codes, got: {result}"
190        );
191    }
192
193    #[test]
194    fn test_datum_suffix_dispatch() {
195        let result = randomize_field("lieferdatum", &[], None, 42).unwrap();
196        assert_eq!(result.len(), 10, "must be YYYY-MM-DD, got: {result}");
197        assert!(result.contains('-'));
198    }
199
200    #[test]
201    fn test_bis_suffix_dispatch() {
202        let result = randomize_field("gueltigkeitsBis", &[], None, 42).unwrap();
203        assert_eq!(result.len(), 10, "must be YYYY-MM-DD, got: {result}");
204    }
205
206    #[test]
207    fn test_gueltig_prefix_dispatch() {
208        let result = randomize_field("gueltigAb", &[], None, 42).unwrap();
209        assert_eq!(result.len(), 10, "must be YYYY-MM-DD, got: {result}");
210    }
211
212    #[test]
213    fn test_unknown_field_error() {
214        let err = randomize_field("unknownField", &[], None, 42).unwrap_err();
215        assert!(err.to_string().contains("unknownField"));
216    }
217
218    #[test]
219    fn test_field_seed_deterministic() {
220        let s1 = field_seed(42, "marktlokationsId");
221        let s2 = field_seed(42, "marktlokationsId");
222        assert_eq!(s1, s2);
223    }
224
225    #[test]
226    fn test_field_seed_different_fields() {
227        let s1 = field_seed(42, "marktlokationsId");
228        let s2 = field_seed(42, "messlokationsId");
229        assert_ne!(s1, s2);
230    }
231
232    #[test]
233    fn test_randomize_field_deterministic() {
234        let r1 = randomize_field("marktlokationsId", &[], None, 99).unwrap();
235        let r2 = randomize_field("marktlokationsId", &[], None, 99).unwrap();
236        assert_eq!(r1, r2);
237    }
238}