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}