lnmp_codec/binary/
encoder.rs

1//! Binary encoder for converting LNMP text format to binary format.
2//!
3//! The BinaryEncoder converts LNMP records from text format (v0.3) to binary format (v0.4).
4//! It ensures canonical form by sorting fields by FID before encoding.
5
6use super::delta::{DeltaConfig, DeltaEncoder};
7use super::error::BinaryError;
8use super::frame::BinaryFrame;
9use crate::config::{ParserConfig, ParsingMode, TextInputMode};
10use crate::parser::Parser;
11use lnmp_core::{LnmpField, LnmpRecord};
12
13/// Configuration for binary encoding
14#[derive(Debug, Clone)]
15pub struct EncoderConfig {
16    /// Whether to validate canonical form before encoding
17    pub validate_canonical: bool,
18    /// Whether to sort fields by FID (ensures canonical binary)
19    pub sort_fields: bool,
20    /// How to preprocess incoming text before parsing
21    pub text_input_mode: TextInputMode,
22    /// Optional semantic dictionary for value normalization prior to encoding
23    pub semantic_dictionary: Option<lnmp_sfe::SemanticDictionary>,
24
25    // v0.5 fields
26    /// Whether to enable nested binary structure encoding (v0.5)
27    pub enable_nested_binary: bool,
28    /// Maximum nesting depth for nested structures (v0.5)
29    pub max_depth: usize,
30    /// Whether to enable streaming mode for large payloads (v0.5)
31    pub streaming_mode: bool,
32    /// Whether to enable delta encoding mode (v0.5)
33    pub delta_mode: bool,
34    /// Chunk size for streaming mode in bytes (v0.5)
35    pub chunk_size: usize,
36}
37
38impl Default for EncoderConfig {
39    fn default() -> Self {
40        Self {
41            validate_canonical: false,
42            sort_fields: true,
43            text_input_mode: TextInputMode::Strict,
44            semantic_dictionary: None,
45            // v0.5 defaults
46            enable_nested_binary: false,
47            max_depth: 32,
48            streaming_mode: false,
49            delta_mode: false,
50            chunk_size: 4096,
51        }
52    }
53}
54
55impl EncoderConfig {
56    /// Creates a new encoder configuration with default settings
57    pub fn new() -> Self {
58        Self::default()
59    }
60
61    /// Sets whether to validate canonical form before encoding
62    pub fn with_validate_canonical(mut self, validate: bool) -> Self {
63        self.validate_canonical = validate;
64        self.sort_fields = true;
65        self
66    }
67
68    /// Sets whether to sort fields by FID
69    pub fn with_sort_fields(mut self, sort: bool) -> Self {
70        self.sort_fields = sort;
71        self
72    }
73
74    /// Sets how text input should be pre-processed before parsing.
75    pub fn with_text_input_mode(mut self, mode: TextInputMode) -> Self {
76        self.text_input_mode = mode;
77        self
78    }
79
80    /// Attaches a semantic dictionary for normalization prior to encoding.
81    pub fn with_semantic_dictionary(mut self, dict: lnmp_sfe::SemanticDictionary) -> Self {
82        self.semantic_dictionary = Some(dict);
83        self
84    }
85
86    // v0.5 builder methods
87
88    /// Enables nested binary structure encoding (v0.5)
89    pub fn with_nested_binary(mut self, enable: bool) -> Self {
90        self.enable_nested_binary = enable;
91        self
92    }
93
94    /// Sets maximum nesting depth for nested structures (v0.5)
95    pub fn with_max_depth(mut self, depth: usize) -> Self {
96        self.max_depth = depth;
97        self
98    }
99
100    /// Enables streaming mode for large payloads (v0.5)
101    pub fn with_streaming_mode(mut self, enable: bool) -> Self {
102        self.streaming_mode = enable;
103        self
104    }
105
106    /// Enables delta encoding mode (v0.5)
107    pub fn with_delta_mode(mut self, enable: bool) -> Self {
108        self.delta_mode = enable;
109        self
110    }
111
112    /// Sets chunk size for streaming mode in bytes (v0.5)
113    pub fn with_chunk_size(mut self, size: usize) -> Self {
114        self.chunk_size = size;
115        self
116    }
117
118    /// Configures the encoder for v0.4 compatibility mode
119    ///
120    /// This disables all v0.5 features (nested structures, streaming, delta encoding)
121    /// to ensure the output is compatible with v0.4 decoders.
122    ///
123    /// # Examples
124    ///
125    /// ```
126    /// use lnmp_codec::binary::EncoderConfig;
127    ///
128    /// let config = EncoderConfig::new().with_v0_4_compatibility();
129    /// assert!(!config.enable_nested_binary);
130    /// assert!(!config.streaming_mode);
131    /// assert!(!config.delta_mode);
132    /// ```
133    pub fn with_v0_4_compatibility(mut self) -> Self {
134        self.enable_nested_binary = false;
135        self.streaming_mode = false;
136        self.delta_mode = false;
137        self
138    }
139}
140
141/// Binary encoder for LNMP v0.4
142///
143/// Converts LNMP records from text format (v0.3) to binary format (v0.4).
144/// The encoder ensures canonical form by sorting fields by FID before encoding.
145///
146/// # Examples
147///
148/// ```
149/// use lnmp_codec::binary::BinaryEncoder;
150/// use lnmp_core::{LnmpRecord, LnmpField, LnmpValue};
151///
152/// let mut record = LnmpRecord::new();
153/// record.add_field(LnmpField {
154///     fid: 7,
155///     value: LnmpValue::Bool(true),
156/// });
157/// record.add_field(LnmpField {
158///     fid: 12,
159///     value: LnmpValue::Int(14532),
160/// });
161///
162/// let encoder = BinaryEncoder::new();
163/// let binary = encoder.encode(&record).unwrap();
164/// ```
165#[derive(Debug)]
166pub struct BinaryEncoder {
167    config: EncoderConfig,
168    normalizer: Option<crate::normalizer::ValueNormalizer>,
169}
170
171impl BinaryEncoder {
172    /// Creates a new binary encoder with default configuration
173    ///
174    /// Default configuration:
175    /// - `validate_canonical`: false
176    /// - `sort_fields`: true
177    pub fn new() -> Self {
178        Self {
179            config: EncoderConfig::default(),
180            normalizer: None,
181        }
182    }
183
184    /// Creates a binary encoder with custom configuration
185    pub fn with_config(config: EncoderConfig) -> Self {
186        let normalizer = config.semantic_dictionary.as_ref().map(|dict| {
187            crate::normalizer::ValueNormalizer::new(crate::normalizer::NormalizationConfig {
188                semantic_dictionary: Some(dict.clone()),
189                ..crate::normalizer::NormalizationConfig::default()
190            })
191        });
192        Self { config, normalizer }
193    }
194
195    /// Sets delta mode on the encoder instance in a fluent interface style.
196    pub fn with_delta_mode(mut self, enable: bool) -> Self {
197        self.config.delta_mode = enable;
198        self
199    }
200
201    /// Encodes an LnmpRecord to binary format
202    ///
203    /// The encoder will:
204    /// 1. Sort fields by FID (if sort_fields is enabled)
205    /// 2. Convert the record to a BinaryFrame
206    /// 3. Encode the frame to bytes
207    ///
208    /// # Arguments
209    ///
210    /// * `record` - The LNMP record to encode
211    ///
212    /// # Returns
213    ///
214    /// A vector of bytes representing the binary-encoded record
215    ///
216    /// # Errors
217    ///
218    /// Returns `BinaryError` if:
219    /// - The record contains nested structures when v0.4 compatibility is enabled
220    /// - Field conversion fails
221    pub fn encode(&self, record: &LnmpRecord) -> Result<Vec<u8>, BinaryError> {
222        // Guardrails for unimplemented v0.5 features
223        if self.config.streaming_mode {
224            return Err(BinaryError::UnsupportedFeature {
225                feature: "binary streaming mode".to_string(),
226            });
227        }
228        if self.config.enable_nested_binary {
229            return Err(BinaryError::UnsupportedFeature {
230                feature: "nested binary encoding".to_string(),
231            });
232        }
233        if self.config.chunk_size == 0 {
234            return Err(BinaryError::UnsupportedFeature {
235                feature: "chunk_size=0 is invalid".to_string(),
236            });
237        }
238
239        // Check for v0.4 compatibility mode
240        if !self.config.enable_nested_binary {
241            // In v0.4 compatibility mode, validate that the record doesn't contain nested structures
242            self.validate_v0_4_compatibility(record)?;
243        }
244
245        // Apply semantic normalization if configured
246        let normalized_record = if let Some(norm) = &self.normalizer {
247            let mut out = LnmpRecord::new();
248            for field in record.fields() {
249                let normalized_value = norm.normalize_with_fid(Some(field.fid), &field.value);
250                out.add_field(LnmpField {
251                    fid: field.fid,
252                    value: normalized_value,
253                });
254            }
255            out
256        } else {
257            record.clone()
258        };
259
260        // Convert record to BinaryFrame (this automatically sorts by FID)
261        let frame = BinaryFrame::from_record(&normalized_record)?;
262
263        // Encode frame to bytes
264        Ok(frame.encode())
265    }
266
267    /// Validates that a record is compatible with v0.4 binary format
268    ///
269    /// This checks that the record doesn't contain any nested structures (NestedRecord or NestedArray),
270    /// which are not supported in v0.4.
271    ///
272    /// # Errors
273    ///
274    /// Returns `BinaryError::InvalidValue` if the record contains nested structures
275    fn validate_v0_4_compatibility(&self, record: &LnmpRecord) -> Result<(), BinaryError> {
276        use lnmp_core::LnmpValue;
277
278        for field in record.fields() {
279            match &field.value {
280                LnmpValue::NestedRecord(_) => {
281                    return Err(BinaryError::InvalidValue {
282                        field_id: field.fid,
283                        type_tag: 0x06,
284                        reason: "Nested records not supported in v0.4 binary format. Use v0.5 with enable_nested_binary=true or convert to flat structure.".to_string(),
285                    });
286                }
287                LnmpValue::NestedArray(_) => {
288                    return Err(BinaryError::InvalidValue {
289                        field_id: field.fid,
290                        type_tag: 0x07,
291                        reason: "Nested arrays not supported in v0.4 binary format. Use v0.5 with enable_nested_binary=true or convert to flat structure.".to_string(),
292                    });
293                }
294                _ => {} // Other types are fine
295            }
296        }
297
298        Ok(())
299    }
300
301    /// Encodes text format directly to binary
302    ///
303    /// This method:
304    /// 1. Parses the text using the v0.3 parser
305    /// 2. Converts the parsed record to binary format
306    /// 3. Ensures fields are sorted by FID
307    ///
308    /// # Arguments
309    ///
310    /// * `text` - LNMP text format string (v0.3)
311    ///
312    /// # Returns
313    ///
314    /// A vector of bytes representing the binary-encoded record
315    ///
316    /// # Errors
317    ///
318    /// Returns `BinaryError` if:
319    /// - Text parsing fails
320    /// - The record contains nested structures (not supported in v0.4)
321    /// - Field conversion fails
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// use lnmp_codec::binary::BinaryEncoder;
327    ///
328    /// let text = "F7=1;F12=14532;F23=[\"admin\",\"dev\"]";
329    /// let encoder = BinaryEncoder::new();
330    /// let binary = encoder.encode_text(text).unwrap();
331    /// ```
332    pub fn encode_text(&self, text: &str) -> Result<Vec<u8>, BinaryError> {
333        self.encode_text_with_mode(text, self.config.text_input_mode)
334    }
335
336    /// Encodes text using strict input rules (no sanitization).
337    pub fn encode_text_strict(&self, text: &str) -> Result<Vec<u8>, BinaryError> {
338        self.encode_text_with_mode(text, TextInputMode::Strict)
339    }
340
341    /// Encodes text using lenient sanitization before parsing.
342    pub fn encode_text_lenient(&self, text: &str) -> Result<Vec<u8>, BinaryError> {
343        self.encode_text_with_mode(text, TextInputMode::Lenient)
344    }
345
346    /// Convenience: enforce strict input + strict grammar.
347    pub fn encode_text_strict_profile(&self, text: &str) -> Result<Vec<u8>, BinaryError> {
348        self.encode_text_with_profile(text, TextInputMode::Strict, ParsingMode::Strict)
349    }
350
351    /// Convenience: lenient input + loose grammar (LLM-facing).
352    pub fn encode_text_llm_profile(&self, text: &str) -> Result<Vec<u8>, BinaryError> {
353        self.encode_text_with_profile(text, TextInputMode::Lenient, ParsingMode::Loose)
354    }
355
356    /// Encodes text with both text input mode and parsing mode specified.
357    ///
358    /// This is useful to enforce strict grammar (ParsingMode::Strict) together with strict input,
359    /// or to provide a fully lenient LLM-facing path (ParsingMode::Loose + Lenient input).
360    pub fn encode_text_with_profile(
361        &self,
362        text: &str,
363        text_mode: TextInputMode,
364        parsing_mode: ParsingMode,
365    ) -> Result<Vec<u8>, BinaryError> {
366        self.encode_text_internal(text, text_mode, parsing_mode)
367    }
368
369    fn encode_text_with_mode(
370        &self,
371        text: &str,
372        mode: TextInputMode,
373    ) -> Result<Vec<u8>, BinaryError> {
374        self.encode_text_internal(text, mode, ParserConfig::default().mode)
375    }
376
377    fn encode_text_internal(
378        &self,
379        text: &str,
380        text_mode: TextInputMode,
381        parsing_mode: ParsingMode,
382    ) -> Result<Vec<u8>, BinaryError> {
383        let parser_config = ParserConfig {
384            mode: parsing_mode,
385            text_input_mode: text_mode,
386            ..ParserConfig::default()
387        };
388
389        let mut parser = Parser::with_config(text, parser_config)
390            .map_err(|e| BinaryError::TextFormatError { source: e })?;
391        let record = parser
392            .parse_record()
393            .map_err(|e| BinaryError::TextFormatError { source: e })?;
394        self.encode(&record)
395    }
396
397    /// Encodes delta packet (binary) from a base record and updated record.
398    ///
399    /// If the encoder config has `delta_mode=true` or a non-None `delta_config` is provided,
400    /// this method computes a delta using `DeltaEncoder` and returns the encoded delta bytes.
401    /// Otherwise it returns an error.
402    pub fn encode_delta_from(
403        &self,
404        base: &LnmpRecord,
405        updated: &LnmpRecord,
406        delta_config: Option<DeltaConfig>,
407    ) -> Result<Vec<u8>, BinaryError> {
408        // Determine config to use. Merge encoder config delta_mode into the provided delta_config
409        let mut config = delta_config.unwrap_or_default();
410        // If encoder's delta_mode is enabled, also enable delta in the delta config
411        if self.config.delta_mode {
412            config.enable_delta = true;
413        }
414
415        // Validate that delta is enabled in either encoder config or provided delta config
416        if !self.config.delta_mode && !config.enable_delta {
417            return Err(BinaryError::DeltaError {
418                reason: "Delta mode not enabled in encoder or provided delta config".to_string(),
419            });
420        }
421
422        // Check configs
423
424        let delta_encoder = DeltaEncoder::with_config(config);
425        let compute_result = delta_encoder.compute_delta(base, updated);
426        match compute_result {
427            Ok(ops) => match delta_encoder.encode_delta(&ops) {
428                Ok(bytes) => Ok(bytes),
429                Err(e) => Err(BinaryError::DeltaError {
430                    reason: format!("encode_delta failed: {}", e),
431                }),
432            },
433            Err(e) => Err(BinaryError::DeltaError {
434                reason: format!("compute_delta failed: {}", e),
435            }),
436        }
437    }
438}
439
440impl Default for BinaryEncoder {
441    fn default() -> Self {
442        Self::new()
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    #![allow(clippy::approx_constant)]
449
450    use super::*;
451    use crate::binary::BinaryDecoder;
452    use lnmp_core::{LnmpField, LnmpValue};
453
454    #[test]
455    fn test_new_encoder() {
456        let encoder = BinaryEncoder::new();
457        assert!(encoder.config.sort_fields);
458        assert!(!encoder.config.validate_canonical);
459        // v0.5 defaults
460        assert_eq!(encoder.config.text_input_mode, TextInputMode::Strict);
461        assert!(!encoder.config.enable_nested_binary);
462        assert_eq!(encoder.config.max_depth, 32);
463        assert!(!encoder.config.streaming_mode);
464        assert!(!encoder.config.delta_mode);
465        assert_eq!(encoder.config.chunk_size, 4096);
466    }
467
468    #[test]
469    fn test_encoder_with_config() {
470        let config = EncoderConfig::new()
471            .with_validate_canonical(true)
472            .with_sort_fields(false);
473
474        let encoder = BinaryEncoder::with_config(config);
475        assert!(!encoder.config.sort_fields);
476        assert!(encoder.config.validate_canonical);
477    }
478
479    #[test]
480    fn test_encode_empty_record() {
481        let record = LnmpRecord::new();
482        let encoder = BinaryEncoder::new();
483        let binary = encoder.encode(&record).unwrap();
484
485        // Should have VERSION, FLAGS, and ENTRY_COUNT=0
486        assert_eq!(binary.len(), 3);
487        assert_eq!(binary[0], 0x04); // VERSION
488        assert_eq!(binary[1], 0x00); // FLAGS
489        assert_eq!(binary[2], 0x00); // ENTRY_COUNT=0
490    }
491
492    #[test]
493    fn test_encode_single_field() {
494        let mut record = LnmpRecord::new();
495        record.add_field(LnmpField {
496            fid: 7,
497            value: LnmpValue::Bool(true),
498        });
499
500        let encoder = BinaryEncoder::new();
501        let binary = encoder.encode(&record).unwrap();
502
503        // Should have VERSION, FLAGS, ENTRY_COUNT=1, and entry data
504        assert!(binary.len() > 3);
505        assert_eq!(binary[0], 0x04); // VERSION
506        assert_eq!(binary[1], 0x00); // FLAGS
507        assert_eq!(binary[2], 0x01); // ENTRY_COUNT=1
508    }
509
510    #[test]
511    fn test_encode_multiple_fields() {
512        let mut record = LnmpRecord::new();
513        record.add_field(LnmpField {
514            fid: 7,
515            value: LnmpValue::Bool(true),
516        });
517        record.add_field(LnmpField {
518            fid: 12,
519            value: LnmpValue::Int(14532),
520        });
521        record.add_field(LnmpField {
522            fid: 23,
523            value: LnmpValue::StringArray(vec!["admin".to_string(), "dev".to_string()]),
524        });
525
526        let encoder = BinaryEncoder::new();
527        let binary = encoder.encode(&record).unwrap();
528
529        assert_eq!(binary[0], 0x04); // VERSION
530        assert_eq!(binary[1], 0x00); // FLAGS
531        assert_eq!(binary[2], 0x03); // ENTRY_COUNT=3
532    }
533
534    #[test]
535    fn test_encode_sorts_fields() {
536        let mut record = LnmpRecord::new();
537        // Add fields in non-sorted order
538        record.add_field(LnmpField {
539            fid: 23,
540            value: LnmpValue::StringArray(vec!["admin".to_string()]),
541        });
542        record.add_field(LnmpField {
543            fid: 7,
544            value: LnmpValue::Bool(true),
545        });
546        record.add_field(LnmpField {
547            fid: 12,
548            value: LnmpValue::Int(14532),
549        });
550
551        let encoder = BinaryEncoder::new();
552        let binary = encoder.encode(&record).unwrap();
553
554        // Decode to verify field order
555        use super::super::frame::BinaryFrame;
556        let frame = BinaryFrame::decode(&binary).unwrap();
557        let decoded_record = frame.to_record();
558
559        // Fields should be in sorted order: 7, 12, 23
560        let fields = decoded_record.fields();
561        assert_eq!(fields[0].fid, 7);
562        assert_eq!(fields[1].fid, 12);
563        assert_eq!(fields[2].fid, 23);
564    }
565
566    #[test]
567    fn test_encode_delta_integration() {
568        use crate::binary::{DeltaConfig, DeltaDecoder};
569        use lnmp_core::{LnmpField, LnmpValue};
570
571        let mut base = LnmpRecord::new();
572        base.add_field(LnmpField {
573            fid: 1,
574            value: LnmpValue::Int(1),
575        });
576        base.add_field(LnmpField {
577            fid: 2,
578            value: LnmpValue::String("v1".to_string()),
579        });
580
581        let mut updated = base.clone();
582        updated.remove_field(1);
583        updated.add_field(LnmpField {
584            fid: 1,
585            value: LnmpValue::Int(2),
586        });
587
588        // BinaryEncoder delta_mode disabled should return DeltaError
589        let encoder = BinaryEncoder::new();
590        let err = encoder
591            .encode_delta_from(&base, &updated, None)
592            .unwrap_err();
593        assert!(matches!(err, BinaryError::DeltaError { .. }));
594
595        // Provide delta config to enable delta
596        let config = DeltaConfig::new().with_enable_delta(true);
597        let bytes = encoder
598            .encode_delta_from(&base, &updated, Some(config))
599            .unwrap();
600        assert_eq!(bytes[0], crate::binary::DELTA_TAG);
601
602        // Use delta decoder to decode and apply
603        let delta_decoder = DeltaDecoder::with_config(DeltaConfig::new().with_enable_delta(true));
604        let ops = delta_decoder.decode_delta(&bytes).unwrap();
605        let mut result = base.clone();
606        delta_decoder.apply_delta(&mut result, &ops).unwrap();
607
608        // After applying delta, result should equal updated
609        assert_eq!(result.fields(), updated.fields());
610    }
611
612    #[test]
613    fn test_encode_delta_from_encoder_config_enabled() {
614        use crate::binary::EncoderConfig;
615        use lnmp_core::{LnmpField, LnmpValue};
616
617        let mut base = LnmpRecord::new();
618        base.add_field(LnmpField {
619            fid: 1,
620            value: LnmpValue::Int(1),
621        });
622        base.add_field(LnmpField {
623            fid: 2,
624            value: LnmpValue::String("v1".to_string()),
625        });
626
627        let mut updated = base.clone();
628        updated.remove_field(1);
629        updated.add_field(LnmpField {
630            fid: 1,
631            value: LnmpValue::Int(2),
632        });
633
634        // BinaryEncoder with delta mode enabled should succeed when using encode_delta_from and None delta_config
635        let config = EncoderConfig::new().with_delta_mode(true);
636        let encoder = BinaryEncoder::with_config(config);
637        let bytes = encoder.encode_delta_from(&base, &updated, None).unwrap();
638        assert_eq!(bytes[0], crate::binary::DELTA_TAG);
639    }
640
641    #[test]
642    fn test_encode_text_simple() {
643        let text = "F7=1";
644        let encoder = BinaryEncoder::new();
645        let binary = encoder.encode_text(text).unwrap();
646
647        assert_eq!(binary[0], 0x04); // VERSION
648        assert_eq!(binary[1], 0x00); // FLAGS
649        assert_eq!(binary[2], 0x01); // ENTRY_COUNT=1
650    }
651
652    #[test]
653    fn test_encode_text_multiple_fields() {
654        let text = "F7=1;F12=14532;F23=[\"admin\",\"dev\"]";
655        let encoder = BinaryEncoder::new();
656        let binary = encoder.encode_text(text).unwrap();
657
658        assert_eq!(binary[0], 0x04); // VERSION
659        assert_eq!(binary[1], 0x00); // FLAGS
660        assert_eq!(binary[2], 0x03); // ENTRY_COUNT=3
661    }
662
663    #[test]
664    fn test_encode_text_unsorted() {
665        let text = "F23=[\"admin\"];F7=1;F12=14532";
666        let encoder = BinaryEncoder::new();
667        let binary = encoder.encode_text(text).unwrap();
668
669        // Decode to verify fields are sorted
670        use super::super::frame::BinaryFrame;
671        let frame = BinaryFrame::decode(&binary).unwrap();
672        let decoded_record = frame.to_record();
673
674        let fields = decoded_record.fields();
675        assert_eq!(fields[0].fid, 7);
676        assert_eq!(fields[1].fid, 12);
677        assert_eq!(fields[2].fid, 23);
678    }
679
680    #[test]
681    fn test_encode_text_with_newlines() {
682        let text = "F7=1\nF12=14532\nF23=[\"admin\",\"dev\"]";
683        let encoder = BinaryEncoder::new();
684        let binary = encoder.encode_text(text).unwrap();
685
686        assert_eq!(binary[0], 0x04); // VERSION
687        assert_eq!(binary[1], 0x00); // FLAGS
688        assert_eq!(binary[2], 0x03); // ENTRY_COUNT=3
689    }
690
691    #[test]
692    fn test_encode_text_lenient_repairs_quotes() {
693        let text = "F7=\"hello";
694        let encoder = BinaryEncoder::with_config(
695            EncoderConfig::new().with_text_input_mode(TextInputMode::Lenient),
696        );
697        let binary = encoder.encode_text(text).unwrap();
698
699        assert_eq!(binary[0], 0x04); // VERSION
700    }
701
702    #[test]
703    fn test_encode_text_all_types() {
704        let text = "F1=-42;F2=3.14;F3=0;F4=\"hello\";F5=[\"a\",\"b\"]";
705        let encoder = BinaryEncoder::new();
706        let binary = encoder.encode_text(text).unwrap();
707
708        // Decode and verify all types
709        use super::super::frame::BinaryFrame;
710        let frame = BinaryFrame::decode(&binary).unwrap();
711        let decoded_record = frame.to_record();
712
713        assert_eq!(
714            decoded_record.get_field(1).unwrap().value,
715            LnmpValue::Int(-42)
716        );
717        assert_eq!(
718            decoded_record.get_field(2).unwrap().value,
719            LnmpValue::Float(3.14)
720        );
721        assert_eq!(
722            decoded_record.get_field(3).unwrap().value,
723            LnmpValue::Bool(false)
724        );
725        assert_eq!(
726            decoded_record.get_field(4).unwrap().value,
727            LnmpValue::String("hello".to_string())
728        );
729        assert_eq!(
730            decoded_record.get_field(5).unwrap().value,
731            LnmpValue::StringArray(vec!["a".to_string(), "b".to_string()])
732        );
733    }
734
735    #[test]
736    fn test_encode_text_strict_profile_rejects_loose_input() {
737        let text = "F7=1;F12=14532; # comment should fail strict grammar";
738        let encoder = BinaryEncoder::new();
739        let err = encoder.encode_text_strict_profile(text).unwrap_err();
740        match err {
741            BinaryError::TextFormatError { .. } => {}
742            other => panic!("expected TextFormatError, got {:?}", other),
743        }
744    }
745
746    #[test]
747    fn test_encode_text_llm_profile_lenient_succeeds() {
748        let text = "F7=\"hello;world\";F8=unquoted token";
749        let encoder = BinaryEncoder::new();
750        let binary = encoder.encode_text_llm_profile(text).unwrap();
751        assert_eq!(binary[0], 0x04);
752    }
753
754    #[test]
755    fn test_encode_text_invalid() {
756        let text = "INVALID";
757        let encoder = BinaryEncoder::new();
758        let result = encoder.encode_text(text);
759
760        assert!(result.is_err());
761    }
762
763    #[test]
764    fn test_encode_text_empty() {
765        let text = "";
766        let encoder = BinaryEncoder::new();
767        let binary = encoder.encode_text(text).unwrap();
768
769        // Empty text should produce empty record
770        assert_eq!(binary.len(), 3);
771        assert_eq!(binary[0], 0x04); // VERSION
772        assert_eq!(binary[1], 0x00); // FLAGS
773        assert_eq!(binary[2], 0x00); // ENTRY_COUNT=0
774    }
775
776    #[test]
777    fn test_encode_all_value_types() {
778        let mut record = LnmpRecord::new();
779        record.add_field(LnmpField {
780            fid: 1,
781            value: LnmpValue::Int(42),
782        });
783        record.add_field(LnmpField {
784            fid: 2,
785            value: LnmpValue::Float(2.718),
786        });
787        record.add_field(LnmpField {
788            fid: 3,
789            value: LnmpValue::Bool(false),
790        });
791        record.add_field(LnmpField {
792            fid: 4,
793            value: LnmpValue::String("world".to_string()),
794        });
795        record.add_field(LnmpField {
796            fid: 5,
797            value: LnmpValue::StringArray(vec!["x".to_string(), "y".to_string()]),
798        });
799
800        let encoder = BinaryEncoder::new();
801        let binary = encoder.encode(&record).unwrap();
802
803        // Decode and verify
804        use super::super::frame::BinaryFrame;
805        let frame = BinaryFrame::decode(&binary).unwrap();
806        let decoded_record = frame.to_record();
807
808        assert_eq!(decoded_record.fields().len(), 5);
809        assert_eq!(
810            decoded_record.get_field(1).unwrap().value,
811            LnmpValue::Int(42)
812        );
813        assert_eq!(
814            decoded_record.get_field(2).unwrap().value,
815            LnmpValue::Float(2.718)
816        );
817        assert_eq!(
818            decoded_record.get_field(3).unwrap().value,
819            LnmpValue::Bool(false)
820        );
821        assert_eq!(
822            decoded_record.get_field(4).unwrap().value,
823            LnmpValue::String("world".to_string())
824        );
825        assert_eq!(
826            decoded_record.get_field(5).unwrap().value,
827            LnmpValue::StringArray(vec!["x".to_string(), "y".to_string()])
828        );
829    }
830
831    #[test]
832    fn test_default_encoder() {
833        let encoder = BinaryEncoder::default();
834        assert!(encoder.config.sort_fields);
835        assert!(!encoder.config.validate_canonical);
836    }
837
838    #[test]
839    fn test_encoder_config_builder() {
840        let config = EncoderConfig::new()
841            .with_validate_canonical(true)
842            .with_sort_fields(true);
843
844        assert!(config.validate_canonical);
845        assert!(config.sort_fields);
846    }
847
848    #[test]
849    fn test_encoder_config_v05_fields() {
850        let config = EncoderConfig::new()
851            .with_nested_binary(true)
852            .with_max_depth(64)
853            .with_streaming_mode(true)
854            .with_delta_mode(true)
855            .with_chunk_size(8192);
856
857        assert!(config.enable_nested_binary);
858        assert_eq!(config.max_depth, 64);
859        assert!(config.streaming_mode);
860        assert!(config.delta_mode);
861        assert_eq!(config.chunk_size, 8192);
862    }
863
864    #[test]
865    fn test_encoder_config_v05_defaults() {
866        let config = EncoderConfig::default();
867
868        assert!(!config.enable_nested_binary);
869        assert_eq!(config.max_depth, 32);
870        assert!(!config.streaming_mode);
871        assert!(!config.delta_mode);
872        assert_eq!(config.chunk_size, 4096);
873    }
874
875    #[test]
876    fn test_encoder_config_backward_compatibility() {
877        // v0.4 configurations should work without any changes
878        let v04_config = EncoderConfig::new()
879            .with_validate_canonical(true)
880            .with_sort_fields(true);
881
882        // v0.4 fields should work as before
883        assert!(v04_config.validate_canonical);
884        assert!(v04_config.sort_fields);
885
886        // v0.5 fields should have safe defaults (disabled)
887        assert!(!v04_config.enable_nested_binary);
888        assert!(!v04_config.streaming_mode);
889        assert!(!v04_config.delta_mode);
890    }
891
892    #[test]
893    fn test_encoder_config_mixed_v04_v05() {
894        // Test that v0.4 and v0.5 configurations can be mixed
895        let config = EncoderConfig::new()
896            .with_validate_canonical(true) // v0.4
897            .with_nested_binary(true) // v0.5
898            .with_sort_fields(true) // v0.4
899            .with_streaming_mode(true); // v0.5
900
901        assert!(config.validate_canonical);
902        assert!(config.sort_fields);
903        assert!(config.enable_nested_binary);
904        assert!(config.streaming_mode);
905    }
906
907    #[test]
908    fn test_encoder_applies_semantic_dictionary() {
909        let mut record = LnmpRecord::new();
910        record.add_field(LnmpField {
911            fid: 23,
912            value: LnmpValue::StringArray(vec!["Admin".to_string()]),
913        });
914
915        let mut dict = lnmp_sfe::SemanticDictionary::new();
916        dict.add_equivalence(23, "Admin".to_string(), "admin".to_string());
917
918        let config = EncoderConfig::new()
919            .with_semantic_dictionary(dict)
920            .with_validate_canonical(true);
921        let encoder = BinaryEncoder::with_config(config);
922
923        let binary = encoder.encode(&record).unwrap();
924        let decoder = BinaryDecoder::new();
925        let decoded = decoder.decode(&binary).unwrap();
926
927        match decoded.get_field(23).unwrap().value.clone() {
928            LnmpValue::StringArray(vals) => assert_eq!(vals, vec!["admin".to_string()]),
929            other => panic!("unexpected value {:?}", other),
930        }
931    }
932
933    #[test]
934    fn test_encoder_rejects_streaming_mode_until_implemented() {
935        let config = EncoderConfig::new().with_streaming_mode(true);
936        let encoder = BinaryEncoder::with_config(config);
937        let mut record = LnmpRecord::new();
938        record.add_field(LnmpField {
939            fid: 1,
940            value: LnmpValue::Int(1),
941        });
942        let err = encoder.encode(&record).unwrap_err();
943        assert!(matches!(err, BinaryError::UnsupportedFeature { .. }));
944    }
945
946    #[test]
947    fn test_encoder_rejects_nested_binary_flag_until_implemented() {
948        let config = EncoderConfig::new().with_nested_binary(true);
949        let encoder = BinaryEncoder::with_config(config);
950        let mut record = LnmpRecord::new();
951        record.add_field(LnmpField {
952            fid: 1,
953            value: LnmpValue::Int(1),
954        });
955        let err = encoder.encode(&record).unwrap_err();
956        assert!(matches!(err, BinaryError::UnsupportedFeature { .. }));
957    }
958
959    #[test]
960    fn test_encoder_rejects_zero_chunk_size() {
961        let config = EncoderConfig::new().with_chunk_size(0);
962        let encoder = BinaryEncoder::with_config(config);
963        let mut record = LnmpRecord::new();
964        record.add_field(LnmpField {
965            fid: 1,
966            value: LnmpValue::Int(1),
967        });
968        let err = encoder.encode(&record).unwrap_err();
969        assert!(matches!(err, BinaryError::UnsupportedFeature { .. }));
970    }
971
972    #[test]
973    fn test_encoder_v04_mode_encoding() {
974        // Test that encoder with v0.5 disabled behaves like v0.4
975        let config = EncoderConfig::new()
976            .with_nested_binary(false)
977            .with_streaming_mode(false)
978            .with_delta_mode(false);
979
980        let encoder = BinaryEncoder::with_config(config);
981
982        // Should encode a simple record just like v0.4
983        let mut record = LnmpRecord::new();
984        record.add_field(LnmpField {
985            fid: 7,
986            value: LnmpValue::Bool(true),
987        });
988
989        let binary = encoder.encode(&record).unwrap();
990
991        // Should produce v0.4 compatible output
992        assert_eq!(binary[0], 0x04); // VERSION
993        assert_eq!(binary[1], 0x00); // FLAGS
994        assert_eq!(binary[2], 0x01); // ENTRY_COUNT=1
995    }
996}