Skip to main content

serializer/
encoder.rs

1//! Encoder for DX Machine format
2//!
3//! Converts Rust data structures into highly optimized DX bytecode.
4//! Automatically uses aliases, ditto marks, and compression.
5//! DX ∞: Base62 encoding for integers, auto-increment detection.
6
7use crate::base62::encode_base62;
8use crate::error::Result;
9use crate::schema::TypeHint;
10use crate::types::{DxArray, DxObject, DxTable, DxValue};
11use rustc_hash::FxHashMap;
12use std::io::Write;
13
14/// Encoder configuration
15#[derive(Debug, Clone)]
16pub struct EncoderConfig {
17    /// Enable alias generation for repeated keys
18    pub use_aliases: bool,
19    /// Enable ditto marks for repeated values
20    pub use_ditto: bool,
21    /// Minimum key length to create alias
22    pub alias_min_length: usize,
23    /// Pretty print (adds spacing)
24    pub pretty: bool,
25}
26
27impl Default for EncoderConfig {
28    fn default() -> Self {
29        Self {
30            use_aliases: true,
31            use_ditto: true,
32            alias_min_length: 6,
33            pretty: false,
34        }
35    }
36}
37
38/// DX encoder
39pub struct Encoder {
40    config: EncoderConfig,
41    /// Generated aliases (full_key -> alias)
42    aliases: FxHashMap<String, String>,
43    /// Next alias id
44    next_alias: usize,
45    /// Previous row values for ditto detection
46    prev_row: Option<Vec<DxValue>>,
47}
48
49impl Encoder {
50    /// Create an encoder with an explicit configuration.
51    pub fn new(config: EncoderConfig) -> Self {
52        Self {
53            config,
54            aliases: FxHashMap::default(),
55            next_alias: 0,
56            prev_row: None,
57        }
58    }
59
60    /// Create an encoder with default production settings.
61    pub fn with_defaults() -> Self {
62        Self::new(EncoderConfig::default())
63    }
64
65    /// Encode a value to bytes
66    pub fn encode(&mut self, value: &DxValue) -> Result<Vec<u8>> {
67        let mut output = Vec::new();
68        self.encode_to_writer(value, &mut output)?;
69        Ok(output)
70    }
71
72    /// Encode to a writer
73    pub fn encode_to_writer<W: Write>(&mut self, value: &DxValue, writer: &mut W) -> Result<()> {
74        match value {
75            DxValue::Object(obj) => self.encode_object(obj, writer, ""),
76            _ => {
77                // Single value - wrap in object
78                let mut obj = DxObject::new();
79                obj.insert("value".to_string(), value.clone());
80                self.encode_object(&obj, writer, "")
81            }
82        }
83    }
84
85    /// Encode an object
86    fn encode_object<W: Write>(
87        &mut self,
88        obj: &DxObject,
89        writer: &mut W,
90        prefix: &str,
91    ) -> Result<()> {
92        // First pass: find keys to alias
93        if self.config.use_aliases {
94            self.generate_aliases(obj);
95        }
96
97        // Second pass: write aliases
98        for (alias, full_key) in &self.aliases {
99            writeln!(writer, "${}={}", alias, full_key)?;
100        }
101
102        // Third pass: write values
103        for (key, value) in obj.iter() {
104            let full_key = if prefix.is_empty() {
105                key.clone()
106            } else {
107                format!("{}.{}", prefix, key)
108            };
109
110            self.encode_field(&full_key, value, writer)?;
111        }
112
113        Ok(())
114    }
115
116    /// Generate aliases for repeated/long keys
117    fn generate_aliases(&mut self, obj: &DxObject) {
118        let mut key_freq: FxHashMap<String, usize> = FxHashMap::default();
119
120        for (key, _) in obj.iter() {
121            if key.len() >= self.config.alias_min_length {
122                *key_freq.entry(key.clone()).or_insert(0) += 1;
123            }
124        }
125
126        // Create aliases for frequent/long keys
127        for (key, _count) in key_freq.iter() {
128            if !self.aliases.contains_key(key) {
129                let alias = format!("k{}", self.next_alias);
130                self.next_alias += 1;
131                self.aliases.insert(alias, key.clone());
132            }
133        }
134    }
135
136    /// Encode a field
137    fn encode_field<W: Write>(&mut self, key: &str, value: &DxValue, writer: &mut W) -> Result<()> {
138        // Write key (with alias if available)
139        let key_to_write = self
140            .aliases
141            .iter()
142            .find(|(_, v)| v.as_str() == key)
143            .map(|(k, _)| format!("${}", k))
144            .unwrap_or_else(|| key.to_string());
145
146        match value {
147            DxValue::Table(table) => {
148                // Table with schema
149                write!(writer, "{}=", key_to_write)?;
150                self.encode_table(table, writer)?;
151            }
152            DxValue::Array(arr) if arr.is_stream => {
153                // Stream array
154                write!(writer, "{}> ", key_to_write)?;
155                self.encode_stream_array(arr, writer)?;
156                writeln!(writer)?;
157            }
158            DxValue::Bool(true) => {
159                // Shorthand: key!
160                writeln!(writer, "{}!", key_to_write)?;
161            }
162            DxValue::Null => {
163                // Shorthand: key?
164                writeln!(writer, "{}?", key_to_write)?;
165            }
166            _ => {
167                // Standard key:value
168                write!(writer, "{}:", key_to_write)?;
169                self.encode_value(value, writer)?;
170                writeln!(writer)?;
171            }
172        }
173
174        Ok(())
175    }
176
177    /// Encode a table
178    fn encode_table<W: Write>(&mut self, table: &DxTable, writer: &mut W) -> Result<()> {
179        // Write schema
180        for (i, col) in table.schema.columns.iter().enumerate() {
181            if i > 0 {
182                write!(writer, " ")?;
183            }
184            write!(writer, "{}", col.name)?;
185            if col.type_hint != TypeHint::Auto {
186                write!(writer, "%{}", col.type_hint.to_byte() as char)?;
187            }
188        }
189        writeln!(writer)?;
190
191        // Write rows
192        self.prev_row = None;
193        for row in &table.rows {
194            self.encode_row(row, &table.schema, writer)?;
195            self.prev_row = Some(row.clone());
196        }
197
198        Ok(())
199    }
200
201    /// Encode a table row
202    fn encode_row<W: Write>(
203        &mut self,
204        row: &[DxValue],
205        schema: &crate::schema::Schema,
206        writer: &mut W,
207    ) -> Result<()> {
208        for (i, value) in row.iter().enumerate() {
209            // Skip auto-increment columns (they're generated on parse)
210            if i < schema.columns.len() && schema.columns[i].type_hint == TypeHint::AutoIncrement {
211                continue;
212            }
213
214            if i > 0 {
215                write!(writer, " ")?;
216            }
217
218            // Use ditto if value matches previous row
219            if self.config.use_ditto {
220                if let Some(prev) = &self.prev_row {
221                    if i < prev.len() && &prev[i] == value {
222                        write!(writer, "_")?;
223                        continue;
224                    }
225                }
226            }
227
228            // Use Base62 encoding if column type is Base62
229            if i < schema.columns.len() && schema.columns[i].type_hint == TypeHint::Base62 {
230                if let DxValue::Int(n) = value {
231                    if *n >= 0 {
232                        write!(writer, "{}", encode_base62(*n as u64))?;
233                        continue;
234                    }
235                }
236            }
237
238            self.encode_value(value, writer)?;
239        }
240        writeln!(writer)?;
241
242        Ok(())
243    }
244
245    /// Encode stream array
246    fn encode_stream_array<W: Write>(&mut self, arr: &DxArray, writer: &mut W) -> Result<()> {
247        for (i, value) in arr.values.iter().enumerate() {
248            if i > 0 {
249                write!(writer, "|")?;
250            }
251            self.encode_value(value, writer)?;
252        }
253        Ok(())
254    }
255
256    /// Encode a value
257    fn encode_value<W: Write>(&mut self, value: &DxValue, writer: &mut W) -> Result<()> {
258        match value {
259            DxValue::Null => write!(writer, "~")?,
260            DxValue::Bool(true) => write!(writer, "+")?,
261            DxValue::Bool(false) => write!(writer, "-")?,
262            DxValue::Int(i) => write!(writer, "{}", i)?,
263            DxValue::Float(f) => write!(writer, "{}", f)?,
264            DxValue::String(s) => {
265                // No quotes in machine format
266                write!(writer, "{}", s)?;
267            }
268            DxValue::Array(arr) => {
269                if arr.is_stream {
270                    self.encode_stream_array(arr, writer)?;
271                } else {
272                    // Vertical array - not supported in inline
273                    write!(writer, "[]")?;
274                }
275            }
276            DxValue::Object(_) => write!(writer, "{{}}")?,
277            DxValue::Table(_) => write!(writer, "[[]]")?,
278            DxValue::Ref(id) => write!(writer, "@{}", id)?,
279        }
280        Ok(())
281    }
282}
283
284/// Encode a value with default config
285///
286/// Converts a [`DxValue`] into DX machine format bytes using default settings.
287/// This is the inverse of [`parse()`].
288///
289/// # Example
290///
291/// ```rust
292/// use serializer::{encode, DxValue, DxObject};
293///
294/// let mut obj = DxObject::new();
295/// obj.insert("name".to_string(), DxValue::String("Alice".to_string()));
296/// obj.insert("age".to_string(), DxValue::Int(30));
297///
298/// let bytes = encode(&DxValue::Object(obj)).unwrap();
299/// ```
300///
301/// # Errors
302///
303/// Returns a `DxError` in the following cases:
304///
305/// - `DxError::Io` - Failed to write to the internal buffer. This is rare
306///   since encoding writes to an in-memory `Vec<u8>`, but can occur if
307///   memory allocation fails.
308///
309/// # Note
310///
311/// The encoding process is generally infallible for valid `DxValue` inputs.
312/// The `Result` return type is used for consistency with the streaming
313/// `encode_to_writer()` function and to handle potential I/O errors.
314///
315/// [`DxValue`]: crate::types::DxValue
316/// [`parse()`]: crate::parser::parse
317#[must_use = "encoding result should be used"]
318pub fn encode(value: &DxValue) -> Result<Vec<u8>> {
319    let mut encoder = Encoder::with_defaults();
320    encoder.encode(value)
321}
322
323/// Encode to a writer with default config
324///
325/// Writes the encoded DX machine format directly to any [`Write`] implementor.
326/// This is more efficient than `encode()` when writing to files or network
327/// streams, as it avoids an intermediate buffer.
328///
329/// # Example
330///
331/// ```rust
332/// use serializer::{encode_to_writer, DxValue, DxObject};
333/// use std::io::Cursor;
334///
335/// let mut obj = DxObject::new();
336/// obj.insert("name".to_string(), DxValue::String("Test".to_string()));
337///
338/// let mut buffer = Vec::new();
339/// encode_to_writer(&DxValue::Object(obj), &mut buffer).unwrap();
340/// ```
341///
342/// # Errors
343///
344/// Returns a `DxError` in the following cases:
345///
346/// - `DxError::Io` - Failed to write to the output stream. This can occur
347///   when writing to files (disk full, permission denied), network streams
348///   (connection closed), or any other I/O operation that fails.
349///
350/// [`Write`]: std::io::Write
351pub fn encode_to_writer<W: Write>(value: &DxValue, writer: &mut W) -> Result<()> {
352    let mut encoder = Encoder::with_defaults();
353    encoder.encode_to_writer(value, writer)
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::parser::parse;
360
361    #[test]
362    fn test_encode_simple() {
363        let mut obj = DxObject::new();
364        obj.insert("name".to_string(), DxValue::String("Alice".to_string()));
365        obj.insert("age".to_string(), DxValue::Int(30));
366        obj.insert("active".to_string(), DxValue::Bool(true));
367
368        let value = DxValue::Object(obj);
369        let encoded = encode(&value).unwrap();
370        let encoded_str = std::str::from_utf8(&encoded).unwrap();
371
372        assert!(encoded_str.contains("name:Alice"));
373        assert!(encoded_str.contains("age:30"));
374        // active is aliased to $k0 because it's >= 6 chars
375        // Check that either active! or $k0! is present (aliased form)
376        assert!(
377            encoded_str.contains("!"),
378            "Expected boolean true to be encoded with !"
379        );
380    }
381
382    #[test]
383    fn test_round_trip() {
384        // Use short key names to avoid aliasing (alias_min_length is 6)
385        let input = b"name:Test
386score:9.5
387ok:+";
388
389        let parsed = parse(input).unwrap();
390        let encoded = encode(&parsed).unwrap();
391        let reparsed = parse(&encoded).unwrap();
392
393        assert_eq!(parsed, reparsed);
394    }
395}