Skip to main content

serializer/
builder.rs

1//! Serializer Builder Pattern
2//!
3//! Provides a fluent API for configuring serialization options with sensible defaults.
4//! The builder pattern allows users to customize various aspects of serialization
5//! without needing to understand all the internal configuration types.
6//!
7//! ## Thread Safety
8//!
9//! Both [`SerializerBuilder`] and [`Serializer`] implement `Send + Sync` and can be
10//! safely shared between threads. The serializer methods are stateless and can be
11//! called concurrently:
12//!
13//! ```rust
14//! use std::sync::Arc;
15//! use std::thread;
16//! use serializer::{SerializerBuilder, DxDocument, DxLlmValue};
17//!
18//! // Create a shared serializer
19//! let serializer = Arc::new(SerializerBuilder::new().build());
20//!
21//! let handles: Vec<_> = (0..4).map(|i| {
22//!     let serializer = Arc::clone(&serializer);
23//!     thread::spawn(move || {
24//!         let mut doc = DxDocument::new();
25//!         doc.context.insert("id".to_string(), DxLlmValue::Num(i as f64));
26//!         serializer.serialize(&doc)
27//!     })
28//! }).collect();
29//!
30//! for handle in handles {
31//!     let result = handle.join().unwrap();
32//!     assert!(!result.is_empty());
33//! }
34//! ```
35//!
36//! ## Example
37//!
38//! ```rust
39//! use serializer::{SerializerBuilder, DxDocument, DxLlmValue};
40//!
41//! let mut doc = DxDocument::new();
42//! doc.context.insert("name".to_string(), DxLlmValue::Str("MyApp".to_string()));
43//!
44//! // Simple usage with defaults
45//! let serializer = SerializerBuilder::new().build();
46//! let text = serializer.serialize(&doc);
47//!
48//! // Advanced configuration
49//! let serializer = SerializerBuilder::new()
50//!     .indent_size(4)
51//!     .preserve_comments(true)
52//!     .expand_keys(false)
53//!     .validate_output(true)
54//!     .build();
55//! let text = serializer.serialize(&doc);
56//! ```
57
58use crate::llm::human_formatter::HumanFormatConfig;
59use crate::llm::pretty_printer::{PrettyPrinter, PrettyPrinterConfig};
60use crate::llm::serializer_output::{SerializerOutput, SerializerOutputConfig};
61use crate::llm::types::DxDocument;
62use crate::llm::{ConvertError, document_to_llm, llm_to_document};
63use std::path::PathBuf;
64
65/// Builder for configuring serialization options
66///
67/// The SerializerBuilder provides a fluent API for customizing serialization behavior.
68/// It combines configuration from multiple internal types into a single, easy-to-use interface.
69#[derive(Debug, Clone)]
70pub struct SerializerBuilder {
71    // Human format options
72    indent_size: usize,
73    expand_keys: bool,
74    use_list_format: bool,
75    space_around_equals: bool,
76
77    // Pretty printer options
78    validate_output: bool,
79    check_round_trip: bool,
80
81    // Output generation options
82    output_dir: Option<PathBuf>,
83    generate_llm: bool,
84    generate_machine: bool,
85
86    // General options
87    preserve_comments: bool,
88    compact_arrays: bool,
89}
90
91impl Default for SerializerBuilder {
92    fn default() -> Self {
93        Self {
94            // Human format defaults
95            indent_size: 0,
96            expand_keys: true,
97            use_list_format: true,
98            space_around_equals: true,
99
100            // Pretty printer defaults
101            validate_output: false, // Disabled by default since V3 round-trip not fully implemented
102            check_round_trip: false,
103
104            // Output generation defaults
105            output_dir: None,
106            generate_llm: true,
107            generate_machine: true,
108
109            // General defaults
110            preserve_comments: false,
111            compact_arrays: false,
112        }
113    }
114}
115
116impl SerializerBuilder {
117    /// Create a new SerializerBuilder with default settings
118    pub fn new() -> Self {
119        Self::default()
120    }
121
122    /// Set the indentation size for formatted output
123    ///
124    /// Controls the minimum padding for keys in human format.
125    /// Set to 0 for no padding (default).
126    ///
127    /// # Example
128    ///
129    /// ```rust
130    /// use serializer::SerializerBuilder;
131    ///
132    /// let serializer = SerializerBuilder::new()
133    ///     .indent_size(4)
134    ///     .build();
135    /// ```
136    pub fn indent_size(mut self, size: usize) -> Self {
137        self.indent_size = size;
138        self
139    }
140
141    /// Set whether to expand abbreviated keys to full names
142    ///
143    /// When true (default), keys like "nm" become "name" in human format.
144    /// When false, abbreviated keys are preserved.
145    ///
146    /// # Example
147    ///
148    /// ```rust
149    /// use serializer::SerializerBuilder;
150    ///
151    /// let serializer = SerializerBuilder::new()
152    ///     .expand_keys(false)  // Keep "nm" instead of expanding to "name"
153    ///     .build();
154    /// ```
155    pub fn expand_keys(mut self, expand: bool) -> Self {
156        self.expand_keys = expand;
157        self
158    }
159
160    /// Set whether to use list format for arrays
161    ///
162    /// When true (default), arrays are formatted as:
163    /// ```text
164    /// items:
165    /// - first
166    /// - second
167    /// ```
168    ///
169    /// When false, arrays are formatted inline:
170    /// ```text
171    /// items = first | second
172    /// ```
173    pub fn use_list_format(mut self, use_list: bool) -> Self {
174        self.use_list_format = use_list;
175        self
176    }
177
178    /// Set whether to add spaces around equals signs
179    ///
180    /// When true (default): `key = value`
181    /// When false: `key=value`
182    pub fn space_around_equals(mut self, space: bool) -> Self {
183        self.space_around_equals = space;
184        self
185    }
186
187    /// Set whether to validate output by parsing it back
188    ///
189    /// When enabled, the serializer will parse the formatted output
190    /// to ensure it's valid. This catches formatting bugs but is slower.
191    ///
192    /// Note: Currently disabled by default since V3 format round-trip
193    /// is not fully implemented.
194    pub fn validate_output(mut self, validate: bool) -> Self {
195        self.validate_output = validate;
196        self
197    }
198
199    /// Set whether to check round-trip consistency
200    ///
201    /// When enabled (requires validate_output), the serializer will
202    /// verify that parsing the formatted output produces an equivalent
203    /// document to the original.
204    pub fn check_round_trip(mut self, check: bool) -> Self {
205        self.check_round_trip = check;
206        self
207    }
208
209    /// Set the output directory for generated files
210    ///
211    /// When set, the serializer can generate .human and .machine files
212    /// in the specified directory. Default is ".dx/serializer".
213    ///
214    /// # Example
215    ///
216    /// ```rust
217    /// use serializer::SerializerBuilder;
218    ///
219    /// let serializer = SerializerBuilder::new()
220    ///     .output_dir("build/serializer")
221    ///     .build();
222    /// ```
223    pub fn output_dir<P: Into<PathBuf>>(mut self, dir: P) -> Self {
224        self.output_dir = Some(dir.into());
225        self
226    }
227
228    /// Set whether to generate LLM format files
229    ///
230    /// When true (default), .llm files are generated in .dx/serializer.
231    pub fn generate_llm(mut self, generate: bool) -> Self {
232        self.generate_llm = generate;
233        self
234    }
235
236    /// Set whether to generate machine format files
237    ///
238    /// When true (default), .machine files are generated for runtime use.
239    pub fn generate_machine(mut self, generate: bool) -> Self {
240        self.generate_machine = generate;
241        self
242    }
243
244    /// Set whether to preserve comments in output
245    ///
246    /// Note: Comment preservation is not yet fully implemented.
247    /// This option is reserved for future use.
248    pub fn preserve_comments(mut self, preserve: bool) -> Self {
249        self.preserve_comments = preserve;
250        self
251    }
252
253    /// Set whether to use compact array formatting
254    ///
255    /// When true, arrays are formatted more compactly.
256    /// This is equivalent to setting use_list_format(false).
257    pub fn compact_arrays(mut self, compact: bool) -> Self {
258        self.compact_arrays = compact;
259        if compact {
260            self.use_list_format = false;
261        }
262        self
263    }
264
265    /// Create a configuration optimized for tables and rules
266    ///
267    /// This preset is useful for multi-row sections like lint rules.
268    /// It sets appropriate padding and formatting for tabular data.
269    ///
270    /// # Example
271    ///
272    /// ```rust
273    /// use serializer::SerializerBuilder;
274    ///
275    /// let serializer = SerializerBuilder::new()
276    ///     .for_tables()
277    ///     .build();
278    /// ```
279    pub fn for_tables(mut self) -> Self {
280        self.indent_size = 20;
281        self.use_list_format = false;
282        self.expand_keys = true;
283        self.space_around_equals = true;
284        self
285    }
286
287    /// Create a configuration optimized for compact output
288    ///
289    /// This preset minimizes whitespace and uses abbreviated keys.
290    /// Useful for token-efficient LLM format.
291    pub fn for_compact(mut self) -> Self {
292        self.indent_size = 0;
293        self.expand_keys = false;
294        self.use_list_format = false;
295        self.space_around_equals = false;
296        self.compact_arrays = true;
297        self
298    }
299
300    /// Create a configuration optimized for human readability
301    ///
302    /// This preset maximizes readability with expanded keys,
303    /// proper spacing, and list formatting for arrays.
304    pub fn for_humans(mut self) -> Self {
305        self.indent_size = 0;
306        self.expand_keys = true;
307        self.use_list_format = true;
308        self.space_around_equals = true;
309        self.validate_output = false; // Keep disabled until V3 round-trip is implemented
310        self
311    }
312
313    /// Build the configured Serializer
314    ///
315    /// Creates a Serializer instance with all the specified options.
316    pub fn build(self) -> Serializer {
317        // Build human format config
318        let human_config = HumanFormatConfig {
319            key_padding: self.indent_size,
320        };
321
322        // Build pretty printer config
323        let pretty_config = PrettyPrinterConfig {
324            formatter_config: human_config,
325            validate_output: self.validate_output,
326            check_round_trip: self.check_round_trip,
327        };
328
329        // Build output config
330        let output_config = SerializerOutputConfig {
331            output_dir: self
332                .output_dir
333                .unwrap_or_else(|| PathBuf::from(".dx/serializer")),
334            generate_llm: self.generate_llm,
335            generate_machine: self.generate_machine,
336            compression: crate::llm::convert::CompressionAlgorithm::default(),
337            generate_metadata: false,
338        };
339
340        Serializer {
341            pretty_printer: PrettyPrinter::with_config(pretty_config),
342            output_generator: SerializerOutput::with_config(output_config),
343        }
344    }
345}
346
347/// Configured Serializer instance
348///
349/// A Serializer provides methods for converting documents to various formats
350/// using the configuration specified by the SerializerBuilder.
351pub struct Serializer {
352    pretty_printer: PrettyPrinter,
353    output_generator: SerializerOutput,
354}
355
356impl Serializer {
357    /// Serialize a DxDocument to LLM format string
358    ///
359    /// This is the primary serialization method that produces the
360    /// token-efficient LLM format.
361    ///
362    /// # Example
363    ///
364    /// ```rust
365    /// use serializer::{SerializerBuilder, DxDocument, DxLlmValue};
366    ///
367    /// let mut doc = DxDocument::new();
368    /// doc.context.insert("name".to_string(), DxLlmValue::Str("MyApp".to_string()));
369    ///
370    /// let serializer = SerializerBuilder::new().build();
371    /// let text = serializer.serialize(&doc);
372    /// ```
373    pub fn serialize(&self, doc: &DxDocument) -> String {
374        document_to_llm(doc)
375    }
376
377    /// Deserialize LLM format string to DxDocument
378    ///
379    /// Parses the token-efficient LLM format back into a structured document.
380    ///
381    /// # Errors
382    ///
383    /// Returns a `ConvertError` if the input is not valid LLM format.
384    pub fn deserialize(&self, input: &str) -> Result<DxDocument, ConvertError> {
385        llm_to_document(input)
386    }
387
388    /// Format a DxDocument to human-readable format
389    ///
390    /// Produces clean, hand-editable format using the configured options.
391    /// The output is validated if validation is enabled in the builder.
392    ///
393    /// # Example
394    ///
395    /// ```rust
396    /// use serializer::{SerializerBuilder, DxDocument, DxLlmValue};
397    ///
398    /// let mut doc = DxDocument::new();
399    /// doc.context.insert("name".to_string(), DxLlmValue::Str("MyApp".to_string()));
400    ///
401    /// let serializer = SerializerBuilder::new()
402    ///     .for_humans()
403    ///     .build();
404    /// let human_text = serializer.format_human(&doc).unwrap();
405    /// ```
406    pub fn format_human(
407        &self,
408        doc: &DxDocument,
409    ) -> Result<String, crate::llm::pretty_printer::PrettyPrintError> {
410        self.pretty_printer.format(doc)
411    }
412
413    /// Format a DxDocument to human format without validation
414    ///
415    /// Faster than format_human() but provides no guarantees about
416    /// the output being parseable.
417    pub fn format_human_unchecked(&self, doc: &DxDocument) -> String {
418        self.pretty_printer.format_unchecked(doc)
419    }
420
421    /// Generate output files for a DxDocument
422    ///
423    /// Creates .human and .machine files in the configured output directory.
424    ///
425    /// # Example
426    ///
427    /// ```rust,no_run
428    /// use serializer::{SerializerBuilder, DxDocument};
429    /// use std::path::Path;
430    ///
431    /// let doc = DxDocument::new();
432    /// let serializer = SerializerBuilder::new()
433    ///     .output_dir("build/serializer")
434    ///     .build();
435    ///
436    /// let result = serializer.generate_files(&doc, Path::new("config.sr")).unwrap();
437    /// println!("Generated {} bytes LLM, {} bytes machine",
438    ///          result.llm_size, result.machine_size);
439    /// ```
440    pub fn generate_files(
441        &self,
442        doc: &DxDocument,
443        source_path: &std::path::Path,
444    ) -> Result<
445        crate::llm::serializer_output::SerializerResult,
446        crate::llm::serializer_output::SerializerOutputError,
447    > {
448        self.output_generator.process_document(doc, source_path)
449    }
450
451    /// Get the pretty printer instance
452    ///
453    /// Provides access to the underlying PrettyPrinter for advanced use cases.
454    pub fn pretty_printer(&self) -> &PrettyPrinter {
455        &self.pretty_printer
456    }
457
458    /// Get the output generator instance
459    ///
460    /// Provides access to the underlying SerializerOutput for advanced use cases.
461    pub fn output_generator(&self) -> &SerializerOutput {
462        &self.output_generator
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469    use crate::llm::types::{DxLlmValue, DxSection};
470
471    #[test]
472    fn test_builder_default() {
473        let builder = SerializerBuilder::new();
474        assert_eq!(builder.indent_size, 0);
475        assert!(builder.expand_keys);
476        assert!(builder.use_list_format);
477        assert!(builder.space_around_equals);
478        assert!(!builder.validate_output); // Disabled by default
479        assert!(builder.generate_llm);
480        assert!(builder.generate_machine);
481    }
482
483    #[test]
484    fn test_builder_fluent_api() {
485        let builder = SerializerBuilder::new()
486            .indent_size(4)
487            .expand_keys(false)
488            .use_list_format(false)
489            .space_around_equals(false)
490            .validate_output(false) // Keep disabled
491            .generate_llm(false);
492
493        assert_eq!(builder.indent_size, 4);
494        assert!(!builder.expand_keys);
495        assert!(!builder.use_list_format);
496        assert!(!builder.space_around_equals);
497        assert!(!builder.validate_output);
498        assert!(!builder.generate_llm);
499    }
500
501    #[test]
502    fn test_builder_presets() {
503        // Test for_tables preset
504        let tables_builder = SerializerBuilder::new().for_tables();
505        assert_eq!(tables_builder.indent_size, 20);
506        assert!(!tables_builder.use_list_format);
507        assert!(tables_builder.expand_keys);
508
509        // Test for_compact preset
510        let compact_builder = SerializerBuilder::new().for_compact();
511        assert_eq!(compact_builder.indent_size, 0);
512        assert!(!compact_builder.expand_keys);
513        assert!(!compact_builder.use_list_format);
514        assert!(!compact_builder.space_around_equals);
515
516        // Test for_humans preset
517        let human_builder = SerializerBuilder::new().for_humans();
518        assert_eq!(human_builder.indent_size, 0);
519        assert!(human_builder.expand_keys);
520        assert!(human_builder.use_list_format);
521        assert!(human_builder.space_around_equals);
522    }
523
524    #[test]
525    fn test_serializer_basic_usage() {
526        let mut doc = DxDocument::new();
527        doc.context
528            .insert("name".to_string(), DxLlmValue::Str("TestApp".to_string()));
529        doc.context
530            .insert("version".to_string(), DxLlmValue::Str("1.0.0".to_string()));
531
532        let serializer = SerializerBuilder::new().build();
533
534        // Test serialize
535        let text = serializer.serialize(&doc);
536        assert!(!text.is_empty());
537
538        // Test deserialize
539        let parsed = serializer.deserialize(&text);
540        assert!(parsed.is_ok());
541        let parsed_doc = parsed.unwrap();
542        assert_eq!(parsed_doc.context.len(), doc.context.len());
543    }
544
545    #[test]
546    fn test_serializer_human_format() {
547        let mut doc = DxDocument::new();
548        doc.context
549            .insert("name".to_string(), DxLlmValue::Str("TestApp".to_string()));
550        doc.context.insert(
551            "editor".to_string(),
552            DxLlmValue::Arr(vec![
553                DxLlmValue::Str("neovim".to_string()),
554                DxLlmValue::Str("vscode".to_string()),
555            ]),
556        );
557
558        let serializer = SerializerBuilder::new().for_humans().build();
559
560        // Test human format (unchecked since validation is disabled)
561        let human_text = serializer.format_human_unchecked(&doc);
562        assert!(!human_text.is_empty(), "Human format should produce output");
563    }
564
565    #[test]
566    fn test_serializer_compact_format() {
567        let mut doc = DxDocument::new();
568        doc.context
569            .insert("name".to_string(), DxLlmValue::Str("TestApp".to_string()));
570        doc.context.insert(
571            "editor".to_string(),
572            DxLlmValue::Arr(vec![
573                DxLlmValue::Str("neovim".to_string()),
574                DxLlmValue::Str("vscode".to_string()),
575            ]),
576        );
577
578        let serializer = SerializerBuilder::new().for_compact().build();
579
580        let human_text = serializer.format_human_unchecked(&doc);
581        assert!(
582            !human_text.is_empty(),
583            "Compact format should produce output"
584        );
585    }
586
587    #[test]
588    fn test_serializer_with_sections() {
589        let mut doc = DxDocument::new();
590
591        let mut section = DxSection::new(vec!["id".to_string(), "name".to_string()]);
592        section.rows.push(vec![
593            DxLlmValue::Num(1.0),
594            DxLlmValue::Str("Alpha".to_string()),
595        ]);
596        section.rows.push(vec![
597            DxLlmValue::Num(2.0),
598            DxLlmValue::Str("Beta".to_string()),
599        ]);
600        doc.sections.insert('d', section);
601
602        let serializer = SerializerBuilder::new().for_tables().build();
603
604        let human_text = serializer.format_human_unchecked(&doc);
605        // Just verify output is not empty
606        assert!(!human_text.is_empty());
607    }
608
609    #[test]
610    fn test_compact_arrays_option() {
611        let builder = SerializerBuilder::new().compact_arrays(true);
612        assert!(builder.compact_arrays);
613        assert!(!builder.use_list_format); // Should be set to false when compact_arrays is true
614    }
615
616    #[test]
617    fn test_output_dir_option() {
618        let builder = SerializerBuilder::new().output_dir("custom/path");
619        assert_eq!(builder.output_dir, Some(PathBuf::from("custom/path")));
620    }
621}