lnmp_codec/
lib.rs

1#![allow(clippy::approx_constant)]
2
3//! # lnmp-codec
4//!
5//! Parser and encoder implementations for LNMP (LLM Native Minimal Protocol) text format.
6//!
7//! This crate provides:
8//! - [`Parser`]: Converts LNMP text format into structured [`LnmpRecord`] objects
9//! - [`Encoder`]: Converts [`LnmpRecord`] objects into LNMP text format (canonical v0.2 format)
10//! - [`ParsingMode`]: Strict or loose parsing modes for validation
11//! - [`EncoderConfig`]: Configuration for encoder behavior (type hints, etc.)
12//! - [`LnmpError`]: Error types for parsing and encoding operations
13//!
14//! ## LNMP v0.2 Features
15//!
16//! Version 0.2 introduces semantic stability with:
17//! - **Deterministic serialization**: Fields are always sorted by FID
18//! - **Canonical format**: Newline-separated, no extra whitespace
19//! - **Type hints**: Optional type annotations (`:i`, `:f`, `:b`, `:s`, `:sa`)
20//! - **Strict mode**: Validates canonical format compliance
21//! - **Loose mode**: Accepts format variations (default)
22//!
23//! ## Parsing Examples
24//!
25//! ### Basic Parsing (Loose Mode)
26//!
27//! ```
28//! use lnmp_codec::Parser;
29//!
30//! // Loose mode accepts various formats
31//! let lnmp_text = r#"F12=14532;F7=1;F23=["admin","dev"]"#;
32//!
33//! let mut parser = Parser::new(lnmp_text).unwrap();
34//! let record = parser.parse_record().unwrap();
35//!
36//! println!("Parsed {} fields", record.fields().len());
37//! ```
38//!
39//! ### Strict Mode Parsing
40//!
41//! ```
42//! use lnmp_codec::{Parser, ParsingMode};
43//!
44//! // Strict mode requires canonical format (sorted, newlines)
45//! let canonical = "F7=1\nF12=14532\nF23=[admin,dev]";
46//!
47//! let mut parser = Parser::with_mode(canonical, ParsingMode::Strict).unwrap();
48//! let record = parser.parse_record().unwrap();
49//! ```
50//!
51//! ### Parsing with Type Hints
52//!
53//! ```
54//! use lnmp_codec::Parser;
55//!
56//! let with_hints = "F12:i=14532\nF5:f=3.14\nF7:b=1";
57//!
58//! let mut parser = Parser::new(with_hints).unwrap();
59//! let record = parser.parse_record().unwrap();
60//! ```
61//!
62//! ## Encoding Examples
63//!
64//! ### Canonical Format (v0.2 Default)
65//!
66//! ```
67//! use lnmp_codec::Encoder;
68//! use lnmp_core::{LnmpField, LnmpRecord, LnmpValue};
69//!
70//! let mut record = LnmpRecord::new();
71//! record.add_field(LnmpField {
72//!     fid: 12,
73//!     value: LnmpValue::Int(14532),
74//! });
75//! record.add_field(LnmpField {
76//!     fid: 7,
77//!     value: LnmpValue::Bool(true),
78//! });
79//!
80//! // Canonical format: sorted by FID, newline-separated
81//! let encoder = Encoder::new();
82//! let output = encoder.encode(&record);
83//! println!("{}", output); // F7=1\nF12=14532
84//! ```
85//!
86//! ### Encoding with Type Hints
87//!
88//! ```
89//! use lnmp_codec::{Encoder, EncoderConfig};
90//! use lnmp_core::{LnmpField, LnmpRecord, LnmpValue};
91//!
92//! let mut record = LnmpRecord::new();
93//! record.add_field(LnmpField {
94//!     fid: 12,
95//!     value: LnmpValue::Int(14532),
96//! });
97//!
98//! let config = EncoderConfig::new()
99//!     .with_type_hints(true)
100//!     .with_canonical(true);
101//! let encoder = Encoder::with_config(config);
102//! let output = encoder.encode(&record);
103//! println!("{}", output); // F12:i=14532
104//! ```
105//!
106//! ## Deterministic Round-Trip
107//!
108//! ```
109//! use lnmp_codec::{Parser, Encoder};
110//!
111//! // Any input becomes canonical after encode
112//! let loose_input = "F23=[a,b];F7=1;F12=100"; // Unsorted, semicolons
113//!
114//! let mut parser = Parser::new(loose_input).unwrap();
115//! let record = parser.parse_record().unwrap();
116//!
117//! let encoder = Encoder::new();
118//! let canonical = encoder.encode(&record);
119//! // Output: F7=1\nF12=100\nF23=[a,b]  (sorted, newlines)
120//!
121//! // Multiple encodes produce identical output
122//! assert_eq!(canonical, encoder.encode(&record));
123//! ```
124//!
125//! ## Migration from v0.1
126//!
127//! v0.2 is backward compatible with v0.1 for parsing, but encoding behavior has changed:
128//!
129//! - **v0.1**: Fields in insertion order, semicolons optional
130//! - **v0.2**: Fields sorted by FID, newlines only (canonical)
131//!
132//! To maintain v0.1 behavior temporarily, use the deprecated `with_semicolons()` method,
133//! but note that fields will still be sorted in v0.2.
134//!
135//! ## Error Handling
136//!
137//! ```
138//! use lnmp_codec::{Parser, LnmpError};
139//!
140//! let invalid_lnmp = "F99999=test"; // Field ID out of range
141//!
142//! match Parser::new(invalid_lnmp) {
143//!     Ok(mut parser) => match parser.parse_record() {
144//!         Ok(record) => println!("Success!"),
145//!         Err(e) => eprintln!("Parse error: {}", e),
146//!     },
147//!     Err(e) => eprintln!("Parser init error: {}", e),
148//! }
149//! ```
150
151#![warn(missing_docs)]
152#![warn(clippy::all)]
153
154pub mod binary;
155pub mod config;
156pub mod container;
157pub mod encoder;
158pub mod equivalence;
159pub mod error;
160pub mod lexer;
161pub mod normalizer;
162pub mod parser;
163
164pub use binary::delta::DeltaApplyContext;
165pub use config::{EncoderConfig, ParsingMode, TextInputMode};
166pub use container::{
167    delta_apply_context_from_metadata, parse_delta_metadata, parse_stream_metadata, ContainerBody,
168    ContainerBuilder, ContainerDecodeError, ContainerEncodeError, ContainerFrame,
169    ContainerFrameError, DeltaMetadata, MetadataError, StreamMetadata,
170};
171pub use encoder::{canonicalize_record, Encoder};
172pub use equivalence::EquivalenceMapper;
173pub use error::LnmpError;
174pub use normalizer::{NormalizationConfig, StringCaseRule, ValueNormalizer};
175pub use parser::Parser;