lnmp_core/
record.rs

1//! Record and field structures for LNMP data.
2
3use crate::{FieldId, LnmpValue};
4
5/// A single field assignment (field ID + value pair)
6#[derive(Debug, Clone, PartialEq)]
7pub struct LnmpField {
8    /// Field identifier
9    pub fid: FieldId,
10    /// Field value
11    pub value: LnmpValue,
12}
13
14/// A complete LNMP record (collection of fields)
15#[derive(Debug, Clone, PartialEq, Default)]
16pub struct LnmpRecord {
17    fields: Vec<LnmpField>,
18}
19
20impl LnmpRecord {
21    /// Creates a new empty record
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Adds a field to the record
27    pub fn add_field(&mut self, field: LnmpField) {
28        self.fields.push(field);
29    }
30
31    /// Gets a field by field ID (returns the first match if duplicates exist)
32    pub fn get_field(&self, fid: FieldId) -> Option<&LnmpField> {
33        self.fields.iter().find(|f| f.fid == fid)
34    }
35
36    /// Removes all fields with the given field ID
37    pub fn remove_field(&mut self, fid: FieldId) {
38        self.fields.retain(|f| f.fid != fid);
39    }
40
41    /// Returns a slice of all fields in the record
42    pub fn fields(&self) -> &[LnmpField] {
43        &self.fields
44    }
45
46    /// Consumes the record and returns the fields vector
47    pub fn into_fields(self) -> Vec<LnmpField> {
48        self.fields
49    }
50
51    /// Returns fields sorted by field ID (stable sort preserves insertion order for duplicates)
52    pub fn sorted_fields(&self) -> Vec<LnmpField> {
53        let mut sorted = self.fields.clone();
54        sorted.sort_by_key(|f| f.fid);
55        sorted
56    }
57
58    /// Creates a record from a vector of fields (typically already sorted)
59    pub fn from_sorted_fields(fields: Vec<LnmpField>) -> Self {
60        Self { fields }
61    }
62
63    /// Validates this record against structural limits (depth, field counts, lengths).
64    pub fn validate_with_limits(
65        &self,
66        limits: &crate::limits::StructuralLimits,
67    ) -> Result<(), crate::limits::StructuralError> {
68        limits.validate_record(self)
69    }
70}
71
72#[cfg(test)]
73#[allow(clippy::approx_constant)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn test_new_record_is_empty() {
79        let record = LnmpRecord::new();
80        assert_eq!(record.fields().len(), 0);
81    }
82
83    #[test]
84    fn test_add_field() {
85        let mut record = LnmpRecord::new();
86        record.add_field(LnmpField {
87            fid: 12,
88            value: LnmpValue::Int(14532),
89        });
90
91        assert_eq!(record.fields().len(), 1);
92        assert_eq!(record.fields()[0].fid, 12);
93    }
94
95    #[test]
96    fn test_get_field() {
97        let mut record = LnmpRecord::new();
98        record.add_field(LnmpField {
99            fid: 12,
100            value: LnmpValue::Int(14532),
101        });
102        record.add_field(LnmpField {
103            fid: 7,
104            value: LnmpValue::Bool(true),
105        });
106
107        let field = record.get_field(12);
108        assert!(field.is_some());
109        assert_eq!(field.unwrap().value, LnmpValue::Int(14532));
110
111        let missing = record.get_field(99);
112        assert!(missing.is_none());
113    }
114
115    #[test]
116    fn test_get_field_with_duplicates() {
117        let mut record = LnmpRecord::new();
118        record.add_field(LnmpField {
119            fid: 5,
120            value: LnmpValue::String("first".to_string()),
121        });
122        record.add_field(LnmpField {
123            fid: 5,
124            value: LnmpValue::String("second".to_string()),
125        });
126
127        // Should return the first match
128        let field = record.get_field(5);
129        assert!(field.is_some());
130        assert_eq!(field.unwrap().value, LnmpValue::String("first".to_string()));
131    }
132
133    #[test]
134    fn test_fields_iteration() {
135        let mut record = LnmpRecord::new();
136        record.add_field(LnmpField {
137            fid: 1,
138            value: LnmpValue::Int(100),
139        });
140        record.add_field(LnmpField {
141            fid: 2,
142            value: LnmpValue::Float(3.14),
143        });
144        record.add_field(LnmpField {
145            fid: 3,
146            value: LnmpValue::Bool(false),
147        });
148
149        let fields = record.fields();
150        assert_eq!(fields.len(), 3);
151        assert_eq!(fields[0].fid, 1);
152        assert_eq!(fields[1].fid, 2);
153        assert_eq!(fields[2].fid, 3);
154    }
155
156    #[test]
157    fn test_into_fields() {
158        let mut record = LnmpRecord::new();
159        record.add_field(LnmpField {
160            fid: 10,
161            value: LnmpValue::String("test".to_string()),
162        });
163
164        let fields = record.into_fields();
165        assert_eq!(fields.len(), 1);
166        assert_eq!(fields[0].fid, 10);
167    }
168
169    #[test]
170    fn test_lnmp_value_equality() {
171        assert_eq!(LnmpValue::Int(42), LnmpValue::Int(42));
172        assert_ne!(LnmpValue::Int(42), LnmpValue::Int(43));
173
174        assert_eq!(LnmpValue::Float(3.14), LnmpValue::Float(3.14));
175
176        assert_eq!(LnmpValue::Bool(true), LnmpValue::Bool(true));
177        assert_ne!(LnmpValue::Bool(true), LnmpValue::Bool(false));
178
179        assert_eq!(
180            LnmpValue::String("hello".to_string()),
181            LnmpValue::String("hello".to_string())
182        );
183
184        assert_eq!(
185            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()]),
186            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
187        );
188    }
189
190    #[test]
191    fn test_lnmp_value_clone() {
192        let original = LnmpValue::StringArray(vec!["test".to_string()]);
193        let cloned = original.clone();
194        assert_eq!(original, cloned);
195    }
196
197    #[test]
198    fn test_empty_record() {
199        let record = LnmpRecord::new();
200        assert_eq!(record.fields().len(), 0);
201        assert!(record.get_field(1).is_none());
202    }
203
204    #[test]
205    fn test_record_with_all_value_types() {
206        let mut record = LnmpRecord::new();
207
208        record.add_field(LnmpField {
209            fid: 1,
210            value: LnmpValue::Int(-42),
211        });
212        record.add_field(LnmpField {
213            fid: 2,
214            value: LnmpValue::Float(3.14159),
215        });
216        record.add_field(LnmpField {
217            fid: 3,
218            value: LnmpValue::Bool(true),
219        });
220        record.add_field(LnmpField {
221            fid: 4,
222            value: LnmpValue::String("hello world".to_string()),
223        });
224        record.add_field(LnmpField {
225            fid: 5,
226            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
227        });
228
229        assert_eq!(record.fields().len(), 5);
230        assert_eq!(record.get_field(1).unwrap().value, LnmpValue::Int(-42));
231        assert_eq!(
232            record.get_field(2).unwrap().value,
233            LnmpValue::Float(3.14159)
234        );
235        assert_eq!(record.get_field(3).unwrap().value, LnmpValue::Bool(true));
236        assert_eq!(
237            record.get_field(4).unwrap().value,
238            LnmpValue::String("hello world".to_string())
239        );
240        assert_eq!(
241            record.get_field(5).unwrap().value,
242            LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()])
243        );
244    }
245
246    #[test]
247    fn test_sorted_fields_basic() {
248        let mut record = LnmpRecord::new();
249        record.add_field(LnmpField {
250            fid: 23,
251            value: LnmpValue::Int(3),
252        });
253        record.add_field(LnmpField {
254            fid: 7,
255            value: LnmpValue::Int(2),
256        });
257        record.add_field(LnmpField {
258            fid: 12,
259            value: LnmpValue::Int(1),
260        });
261
262        let sorted = record.sorted_fields();
263        assert_eq!(sorted.len(), 3);
264        assert_eq!(sorted[0].fid, 7);
265        assert_eq!(sorted[1].fid, 12);
266        assert_eq!(sorted[2].fid, 23);
267    }
268
269    #[test]
270    fn test_sorted_fields_preserves_duplicate_order() {
271        let mut record = LnmpRecord::new();
272        record.add_field(LnmpField {
273            fid: 5,
274            value: LnmpValue::String("first".to_string()),
275        });
276        record.add_field(LnmpField {
277            fid: 10,
278            value: LnmpValue::Int(100),
279        });
280        record.add_field(LnmpField {
281            fid: 5,
282            value: LnmpValue::String("second".to_string()),
283        });
284
285        let sorted = record.sorted_fields();
286        assert_eq!(sorted.len(), 3);
287        assert_eq!(sorted[0].fid, 5);
288        assert_eq!(sorted[0].value, LnmpValue::String("first".to_string()));
289        assert_eq!(sorted[1].fid, 5);
290        assert_eq!(sorted[1].value, LnmpValue::String("second".to_string()));
291        assert_eq!(sorted[2].fid, 10);
292    }
293
294    #[test]
295    fn test_sorted_fields_already_sorted() {
296        let mut record = LnmpRecord::new();
297        record.add_field(LnmpField {
298            fid: 1,
299            value: LnmpValue::Int(1),
300        });
301        record.add_field(LnmpField {
302            fid: 2,
303            value: LnmpValue::Int(2),
304        });
305        record.add_field(LnmpField {
306            fid: 3,
307            value: LnmpValue::Int(3),
308        });
309
310        let sorted = record.sorted_fields();
311        assert_eq!(sorted.len(), 3);
312        assert_eq!(sorted[0].fid, 1);
313        assert_eq!(sorted[1].fid, 2);
314        assert_eq!(sorted[2].fid, 3);
315    }
316
317    #[test]
318    fn test_sorted_fields_empty_record() {
319        let record = LnmpRecord::new();
320        let sorted = record.sorted_fields();
321        assert_eq!(sorted.len(), 0);
322    }
323
324    #[test]
325    fn test_sorted_fields_does_not_modify_original() {
326        let mut record = LnmpRecord::new();
327        record.add_field(LnmpField {
328            fid: 23,
329            value: LnmpValue::Int(3),
330        });
331        record.add_field(LnmpField {
332            fid: 7,
333            value: LnmpValue::Int(2),
334        });
335
336        let _sorted = record.sorted_fields();
337
338        // Original record should remain unchanged
339        assert_eq!(record.fields()[0].fid, 23);
340        assert_eq!(record.fields()[1].fid, 7);
341    }
342}