Skip to main content

facet_styx/
serializer.rs

1//! Styx serialization implementation.
2
3use std::borrow::Cow;
4
5use crate::trace;
6use facet_core::Facet;
7use facet_format::{
8    FieldKey, FieldLocationHint, FormatSerializer, ScalarValue, SerializeError, serialize_root,
9};
10use facet_reflect::{HasFields, Peek};
11use styx_format::{FormatOptions, StyxWriter};
12
13// Re-export FormatOptions as SerializeOptions for backwards compatibility
14pub use styx_format::FormatOptions as SerializeOptions;
15
16/// Extract a FieldKey from a Peek value (typically a map key).
17///
18/// Handles metadata containers like `Documented<ObjectKey>` by extracting
19/// doc comments, tag, and the actual key name.
20fn extract_field_key<'mem, 'facet>(key: Peek<'mem, 'facet>) -> Option<FieldKey<'mem>> {
21    // Try to extract from metadata container
22    if key.shape().is_metadata_container()
23        && let Ok(container) = key.into_struct()
24    {
25        let mut doc_lines: Vec<Cow<'mem, str>> = Vec::new();
26        let mut tag_value: Option<Cow<'mem, str>> = None;
27        let mut name_value: Option<Cow<'mem, str>> = None;
28
29        for (f, field_value) in container.fields() {
30            if f.metadata_kind() == Some("doc") {
31                // Extract doc lines
32                if let Ok(opt) = field_value.into_option()
33                    && let Some(inner) = opt.value()
34                    && let Ok(list) = inner.into_list_like()
35                {
36                    for item in list.iter() {
37                        if let Some(line) = item.as_str() {
38                            doc_lines.push(Cow::Borrowed(line));
39                        }
40                    }
41                }
42            } else if f.metadata_kind() == Some("tag") {
43                // Extract tag
44                if let Ok(opt) = field_value.into_option()
45                    && let Some(inner) = opt.value()
46                    && let Some(s) = inner.as_str()
47                {
48                    tag_value = Some(Cow::Borrowed(s));
49                }
50            } else if f.metadata_kind().is_none() {
51                // This is the value field - might be another metadata container
52                let (inner_name, inner_tag) = extract_name_and_tag(field_value);
53                if inner_name.is_some() {
54                    name_value = inner_name;
55                }
56                if inner_tag.is_some() {
57                    tag_value = inner_tag;
58                }
59            }
60        }
61
62        // Construct FieldKey using available constructors
63        return Some(match (name_value, tag_value) {
64            (Some(name), Some(tag)) => {
65                // Both name and tag - use tagged_with_name_and_doc
66                FieldKey::tagged_with_name_and_doc(
67                    tag,
68                    name,
69                    FieldLocationHint::KeyValue,
70                    doc_lines,
71                )
72            }
73            (Some(name), None) => {
74                // Name only - use with_doc if we have doc lines
75                FieldKey::with_doc(name, FieldLocationHint::KeyValue, doc_lines)
76            }
77            (None, Some(tag)) => {
78                // Tag only - use tagged_with_doc
79                FieldKey::tagged_with_doc(tag, FieldLocationHint::KeyValue, doc_lines)
80            }
81            (None, None) => {
82                // Unit key with optional doc
83                FieldKey::unit_with_doc(FieldLocationHint::KeyValue, doc_lines)
84            }
85        });
86    }
87
88    // Try Option<String> - None becomes unit key (@)
89    if let Ok(opt) = key.into_option() {
90        return match opt.value() {
91            Some(inner) => inner
92                .as_str()
93                .map(|s| FieldKey::new(s, FieldLocationHint::KeyValue)),
94            None => {
95                // None -> unit key (@)
96                Some(FieldKey::unit(FieldLocationHint::KeyValue))
97            }
98        };
99    }
100
101    // Try direct string
102    if let Some(s) = key.as_str() {
103        return Some(FieldKey::new(s, FieldLocationHint::KeyValue));
104    }
105
106    None
107}
108
109/// Extract name and tag from a value (possibly a nested metadata container).
110fn extract_name_and_tag<'mem, 'facet>(
111    value: Peek<'mem, 'facet>,
112) -> (Option<Cow<'mem, str>>, Option<Cow<'mem, str>>) {
113    // Direct string
114    if let Some(s) = value.as_str() {
115        return (Some(Cow::Borrowed(s)), None);
116    }
117
118    // Option<String>
119    if let Ok(opt) = value.into_option() {
120        return match opt.value() {
121            Some(inner) => {
122                if let Some(s) = inner.as_str() {
123                    (Some(Cow::Borrowed(s)), None)
124                } else {
125                    (None, None)
126                }
127            }
128            None => (None, None),
129        };
130    }
131
132    // Nested metadata container (like ObjectKey)
133    if value.shape().is_metadata_container()
134        && let Ok(container) = value.into_struct()
135    {
136        let mut name: Option<Cow<'mem, str>> = None;
137        let mut tag: Option<Cow<'mem, str>> = None;
138
139        for (f, field_value) in container.fields() {
140            if f.metadata_kind() == Some("tag") {
141                if let Ok(opt) = field_value.into_option()
142                    && let Some(inner) = opt.value()
143                    && let Some(s) = inner.as_str()
144                {
145                    tag = Some(Cow::Borrowed(s));
146                }
147            } else if f.metadata_kind().is_none() {
148                // Value field
149                if let Some(s) = field_value.as_str() {
150                    name = Some(Cow::Borrowed(s));
151                } else if let Ok(opt) = field_value.into_option()
152                    && let Some(inner) = opt.value()
153                    && let Some(s) = inner.as_str()
154                {
155                    name = Some(Cow::Borrowed(s));
156                }
157            }
158        }
159
160        return (name, tag);
161    }
162
163    (None, None)
164}
165
166/// Error type for Styx serialization.
167#[derive(Debug)]
168pub struct StyxSerializeError {
169    msg: Cow<'static, str>,
170}
171
172impl StyxSerializeError {
173    fn new(msg: impl Into<Cow<'static, str>>) -> Self {
174        Self { msg: msg.into() }
175    }
176}
177
178impl core::fmt::Display for StyxSerializeError {
179    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
180        f.write_str(&self.msg)
181    }
182}
183
184impl std::error::Error for StyxSerializeError {}
185
186/// Styx serializer with configurable formatting options.
187pub struct StyxSerializer {
188    writer: StyxWriter,
189    /// Track if we're at root level (for struct unwrapping)
190    at_root: bool,
191    /// Track if we just wrote a variant tag (to skip None payload)
192    just_wrote_tag: bool,
193}
194
195impl StyxSerializer {
196    /// Create a new Styx serializer with default options.
197    pub fn new() -> Self {
198        Self::with_options(FormatOptions::default())
199    }
200
201    /// Create a new Styx serializer with the given options.
202    pub fn with_options(options: FormatOptions) -> Self {
203        Self {
204            writer: StyxWriter::with_options(options),
205            at_root: true,
206            just_wrote_tag: false,
207        }
208    }
209
210    /// Consume the serializer and return the output bytes, ensuring trailing newline.
211    pub fn finish(self) -> Vec<u8> {
212        self.writer.finish_document()
213    }
214}
215
216impl Default for StyxSerializer {
217    fn default() -> Self {
218        Self::new()
219    }
220}
221
222impl FormatSerializer for StyxSerializer {
223    type Error = StyxSerializeError;
224
225    fn begin_struct(&mut self) -> Result<(), Self::Error> {
226        let is_root = self.at_root;
227        trace!(is_root, "begin_struct");
228        self.at_root = false;
229        self.just_wrote_tag = false;
230        self.writer.clear_skip_before_value();
231        self.writer.begin_struct(is_root);
232        Ok(())
233    }
234
235    fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
236        trace!(key, "field_key");
237        self.just_wrote_tag = false;
238        self.writer.clear_skip_before_value();
239        self.writer.field_key(key).map_err(StyxSerializeError::new)
240    }
241
242    fn emit_field_key(&mut self, key: &facet_format::FieldKey<'_>) -> Result<(), Self::Error> {
243        trace!(?key, "emit_field_key");
244        self.just_wrote_tag = false;
245        self.writer.clear_skip_before_value();
246
247        let doc_lines: Vec<&str> = key
248            .doc()
249            .map(|d| d.iter().map(|s| s.as_ref()).collect())
250            .unwrap_or_default();
251
252        // Build the key based on tag and name:
253        // - `@` → tag=Some(""), name=None
254        // - `@tag` → tag=Some("tag"), name=None
255        // - `name` → tag=None, name=Some("name")
256        // - `@tag"name"` → tag=Some("tag"), name=Some("name")
257        match (
258            key.tag().map(|c| c.as_ref()),
259            key.name().map(|c| c.as_ref()),
260        ) {
261            (Some(tag), Some(name)) => {
262                // @tag"name" - tagged with value
263                let key_str = if tag.is_empty() {
264                    format!("@\"{}\"", name)
265                } else {
266                    format!("@{}\"{}\"", tag, name)
267                };
268                if !doc_lines.is_empty() {
269                    self.writer
270                        .write_doc_comment_and_key_raw(&doc_lines.join("\n"), &key_str);
271                } else {
272                    self.writer
273                        .field_key_raw(&key_str)
274                        .map_err(StyxSerializeError::new)?;
275                }
276            }
277            (Some(tag), None) => {
278                // @tag or @ - typed catch-all or unit
279                let key_str = if tag.is_empty() {
280                    "@".to_string()
281                } else {
282                    format!("@{}", tag)
283                };
284                if !doc_lines.is_empty() {
285                    self.writer
286                        .write_doc_comment_and_key_raw(&doc_lines.join("\n"), &key_str);
287                } else {
288                    self.writer
289                        .field_key_raw(&key_str)
290                        .map_err(StyxSerializeError::new)?;
291                }
292            }
293            (None, Some(name)) => {
294                // name - regular named field
295                if !doc_lines.is_empty() {
296                    self.writer
297                        .write_doc_comment_and_key(&doc_lines.join("\n"), name);
298                } else {
299                    self.writer
300                        .field_key(name)
301                        .map_err(StyxSerializeError::new)?;
302                }
303            }
304            (None, None) => {
305                // Shouldn't happen, but fall back to @
306                if !doc_lines.is_empty() {
307                    self.writer
308                        .write_doc_comment_and_key_raw(&doc_lines.join("\n"), "@");
309                } else {
310                    self.writer
311                        .field_key_raw("@")
312                        .map_err(StyxSerializeError::new)?;
313                }
314            }
315        }
316        Ok(())
317    }
318
319    fn end_struct(&mut self) -> Result<(), Self::Error> {
320        trace!("end_struct");
321        self.writer.end_struct().map_err(StyxSerializeError::new)
322    }
323
324    fn begin_seq(&mut self) -> Result<(), Self::Error> {
325        trace!("begin_seq");
326        self.at_root = false;
327        self.just_wrote_tag = false;
328        self.writer.clear_skip_before_value();
329        self.writer.begin_seq();
330        Ok(())
331    }
332
333    fn end_seq(&mut self) -> Result<(), Self::Error> {
334        trace!("end_seq");
335        self.writer.end_seq().map_err(StyxSerializeError::new)
336    }
337
338    fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
339        trace!(?scalar, just_wrote_tag = self.just_wrote_tag, "scalar");
340        self.at_root = false;
341        // If we just wrote a tag and the value is unit, skip writing (e.g., @ok instead of @ok@)
342        if self.just_wrote_tag && matches!(scalar, ScalarValue::Unit | ScalarValue::Null) {
343            self.just_wrote_tag = false;
344            self.writer.clear_skip_before_value();
345            return Ok(());
346        }
347        self.just_wrote_tag = false;
348        match scalar {
349            ScalarValue::Unit | ScalarValue::Null => self.writer.write_null(),
350            ScalarValue::Bool(v) => self.writer.write_bool(v),
351            ScalarValue::Char(c) => self.writer.write_char(c),
352            ScalarValue::I64(v) => self.writer.write_i64(v),
353            ScalarValue::U64(v) => self.writer.write_u64(v),
354            ScalarValue::I128(v) => self.writer.write_i128(v),
355            ScalarValue::U128(v) => self.writer.write_u128(v),
356            ScalarValue::F64(v) => self.writer.write_f64(v),
357            ScalarValue::Str(s) => self.writer.write_string(&s),
358            ScalarValue::Bytes(bytes) => self.writer.write_bytes(&bytes),
359        }
360        Ok(())
361    }
362
363    fn serialize_none(&mut self) -> Result<(), Self::Error> {
364        trace!(just_wrote_tag = self.just_wrote_tag, "serialize_none");
365        // If we just wrote a tag, skip the None payload (e.g., @string instead of @string@)
366        if self.just_wrote_tag {
367            self.just_wrote_tag = false;
368            // Clear the skip flag so the next element gets proper spacing
369            self.writer.clear_skip_before_value();
370            return Ok(());
371        }
372        self.at_root = false;
373        self.writer.write_null();
374        Ok(())
375    }
376
377    fn write_variant_tag(&mut self, variant_name: &str) -> Result<bool, Self::Error> {
378        trace!(variant_name, "write_variant_tag");
379        self.at_root = false;
380        if self.just_wrote_tag {
381            self.writer.write_tag_chain_segment(variant_name);
382        } else {
383            self.writer.write_tag(variant_name);
384        }
385        self.just_wrote_tag = true;
386        Ok(true)
387    }
388
389    fn begin_struct_after_tag(&mut self) -> Result<(), Self::Error> {
390        trace!("begin_struct_after_tag");
391        self.just_wrote_tag = false;
392        self.writer.begin_struct_after_tag(false);
393        Ok(())
394    }
395
396    fn begin_seq_after_tag(&mut self) -> Result<(), Self::Error> {
397        trace!("begin_seq_after_tag");
398        self.just_wrote_tag = false;
399        self.writer.begin_seq_after_tag();
400        Ok(())
401    }
402
403    fn finish_variant_tag_unit_payload(&mut self) -> Result<(), Self::Error> {
404        trace!("finish_variant_tag_unit_payload");
405        // Clear the flags that were set by write_variant_tag, since no payload follows.
406        // This ensures the next value gets proper spacing.
407        self.just_wrote_tag = false;
408        self.writer.clear_skip_before_value();
409        Ok(())
410    }
411
412    fn raw_serialize_shape(&self) -> Option<&'static facet_core::Shape> {
413        Some(crate::RawStyx::SHAPE)
414    }
415
416    fn raw_scalar(&mut self, content: &str) -> Result<(), Self::Error> {
417        trace!(content, "raw_scalar");
418        // For RawStyx, output the content directly without quoting
419        self.at_root = false;
420        self.just_wrote_tag = false;
421        self.writer.before_value();
422        self.writer.write_str(content);
423        Ok(())
424    }
425
426    fn serialize_map_key(&mut self, key: Peek<'_, '_>) -> Result<bool, Self::Error> {
427        trace!(shape = key.shape().type_identifier, "serialize_map_key");
428
429        // Try to extract a FieldKey from the map key
430        if let Some(field_key) = extract_field_key(key) {
431            trace!(?field_key, "serialize_map_key: extracted FieldKey");
432            self.emit_field_key(&field_key)?;
433            return Ok(true);
434        }
435
436        // Fall back to default behavior for other key types
437        trace!("serialize_map_key: falling back to default");
438        Ok(false)
439    }
440
441    fn serialize_metadata_container(
442        &mut self,
443        container: &facet_reflect::PeekStruct<'_, '_>,
444    ) -> Result<bool, Self::Error> {
445        trace!("serialize_metadata_container");
446
447        // Extract tag from the metadata container
448        for (f, field_value) in container.fields() {
449            if f.metadata_kind() == Some("tag") {
450                // Extract tag value
451                if let Ok(opt) = field_value.into_option()
452                    && let Some(inner) = opt.value()
453                    && let Some(s) = inner.as_str()
454                {
455                    // Emit the tag before the value
456                    self.write_variant_tag(s)?;
457                }
458                break;
459            }
460        }
461
462        // Return false to let the caller serialize the value field
463        // (the fallback behavior in serialize_impl)
464        Ok(false)
465    }
466
467    fn field_metadata_with_value(
468        &mut self,
469        field_item: &facet_reflect::FieldItem,
470        value: Peek<'_, '_>,
471    ) -> Result<bool, Self::Error> {
472        let is_metadata_container = value.shape().is_metadata_container();
473        trace!(
474            field_name = field_item.effective_name(),
475            is_metadata_container,
476            value_shape = value.shape().type_identifier,
477            "field_metadata_with_value"
478        );
479
480        // First, check if the field value is a metadata container (like Documented<T>)
481        // This takes precedence over Field::doc since it's runtime data
482        if is_metadata_container && let Ok(container) = value.into_struct() {
483            // Collect doc lines from the metadata container
484            let mut doc_lines: Vec<&str> = Vec::new();
485            for (f, field_value) in container.fields() {
486                trace!(
487                    metadata_kind = ?f.metadata_kind(),
488                    field = f.effective_name(),
489                    "field_metadata_with_value: inspecting container field"
490                );
491                if f.metadata_kind() == Some("doc")
492                    && let Ok(opt) = field_value.into_option()
493                    && let Some(inner) = opt.value()
494                    && let Ok(list) = inner.into_list_like()
495                {
496                    for item in list.iter() {
497                        if let Some(line) = item.as_str() {
498                            doc_lines.push(line);
499                        }
500                    }
501                }
502            }
503
504            // If we have doc lines from the container, use them
505            if !doc_lines.is_empty() {
506                trace!(doc_lines = ?doc_lines, "field_metadata_with_value: emitting doc comment");
507                let doc = doc_lines.join("\n");
508                self.writer
509                    .write_doc_comment_and_key(&doc, field_item.effective_name());
510                return Ok(true);
511            }
512        }
513
514        // Note: We intentionally do NOT emit Field::doc (Rust doc comments) when serializing
515        // regular values. Doc comments should only be emitted when:
516        // 1. The field value is a metadata container (like Documented<T>) - checked above
517        // 2. When serializing schemas (where we use Documented<Schema> to carry the docs)
518
519        Ok(false)
520    }
521}
522
523// ─────────────────────────────────────────────────────────────────────────────
524// Public API
525// ─────────────────────────────────────────────────────────────────────────────
526
527/// Serialize a value to a Styx string.
528///
529/// # Example
530///
531/// ```
532/// use facet::Facet;
533/// use facet_styx::to_string;
534///
535/// #[derive(Facet)]
536/// struct Config {
537///     name: String,
538///     port: u16,
539/// }
540///
541/// let config = Config { name: "myapp".into(), port: 8080 };
542/// let styx = to_string(&config).unwrap();
543/// assert!(styx.contains("name myapp"));
544/// assert!(styx.contains("port 8080"));
545/// ```
546pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<StyxSerializeError>>
547where
548    T: Facet<'facet> + ?Sized,
549{
550    to_string_with_options(value, &FormatOptions::default())
551}
552
553/// Serialize a value to a compact Styx string (single line, comma separators).
554///
555/// # Example
556///
557/// ```
558/// use facet::Facet;
559/// use facet_styx::to_string_compact;
560///
561/// #[derive(Facet)]
562/// struct Point { x: i32, y: i32 }
563///
564/// let point = Point { x: 10, y: 20 };
565/// let styx = to_string_compact(&point).unwrap();
566/// assert_eq!(styx, "{x 10, y 20}");
567/// ```
568pub fn to_string_compact<'facet, T>(value: &T) -> Result<String, SerializeError<StyxSerializeError>>
569where
570    T: Facet<'facet> + ?Sized,
571{
572    // For compact mode, we don't want the root to be unwrapped
573    let options = FormatOptions::default().inline();
574    let mut serializer = CompactStyxSerializer::with_options(options);
575    serialize_root(&mut serializer, Peek::new(value))?;
576    let bytes = serializer.finish();
577    Ok(String::from_utf8(bytes).expect("Styx output should always be valid UTF-8"))
578}
579
580/// Serialize a value to a Styx string with custom options.
581pub fn to_string_with_options<'facet, T>(
582    value: &T,
583    options: &FormatOptions,
584) -> Result<String, SerializeError<StyxSerializeError>>
585where
586    T: Facet<'facet> + ?Sized,
587{
588    let mut serializer = StyxSerializer::with_options(options.clone());
589    serialize_root(&mut serializer, Peek::new(value))?;
590    let bytes = serializer.finish();
591    Ok(String::from_utf8(bytes).expect("Styx output should always be valid UTF-8"))
592}
593
594/// Serialize a `Peek` instance to a Styx string.
595pub fn peek_to_string<'input, 'facet>(
596    peek: Peek<'input, 'facet>,
597) -> Result<String, SerializeError<StyxSerializeError>> {
598    peek_to_string_with_options(peek, &FormatOptions::default())
599}
600
601/// Serialize a `Peek` instance to a Styx string with custom options.
602pub fn peek_to_string_with_options<'input, 'facet>(
603    peek: Peek<'input, 'facet>,
604    options: &FormatOptions,
605) -> Result<String, SerializeError<StyxSerializeError>> {
606    let mut serializer = StyxSerializer::with_options(options.clone());
607    serialize_root(&mut serializer, peek)?;
608    let bytes = serializer.finish();
609    Ok(String::from_utf8(bytes).expect("Styx output should always be valid UTF-8"))
610}
611
612/// Serialize a `Peek` instance to a Styx expression string.
613///
614/// Unlike `peek_to_string`, this always wraps objects in braces `{}`,
615/// making it suitable for embedding as a value within a larger document.
616pub fn peek_to_string_expr<'input, 'facet>(
617    peek: Peek<'input, 'facet>,
618) -> Result<String, SerializeError<StyxSerializeError>> {
619    let options = FormatOptions::default().inline();
620    let mut serializer = CompactStyxSerializer::with_options(options);
621    serialize_root(&mut serializer, peek)?;
622    let bytes = serializer.finish();
623    Ok(String::from_utf8(bytes).expect("Styx output should always be valid UTF-8"))
624}
625
626// ─────────────────────────────────────────────────────────────────────────────
627// Compact serializer (always uses braces, never unwraps root)
628// ─────────────────────────────────────────────────────────────────────────────
629
630/// A variant of StyxSerializer that always wraps in braces (for compact mode).
631struct CompactStyxSerializer {
632    writer: StyxWriter,
633    just_wrote_tag: bool,
634}
635
636impl CompactStyxSerializer {
637    fn with_options(options: FormatOptions) -> Self {
638        Self {
639            writer: StyxWriter::with_options(options),
640            just_wrote_tag: false,
641        }
642    }
643
644    fn finish(self) -> Vec<u8> {
645        // Compact mode is for inline embedding - no trailing newline
646        self.writer.finish()
647    }
648}
649
650impl FormatSerializer for CompactStyxSerializer {
651    type Error = StyxSerializeError;
652
653    fn begin_struct(&mut self) -> Result<(), Self::Error> {
654        // Never treat as root in compact mode
655        self.just_wrote_tag = false;
656        self.writer.clear_skip_before_value();
657        self.writer.begin_struct(false);
658        Ok(())
659    }
660
661    fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
662        self.just_wrote_tag = false;
663        self.writer.clear_skip_before_value();
664        self.writer.field_key(key).map_err(StyxSerializeError::new)
665    }
666
667    fn end_struct(&mut self) -> Result<(), Self::Error> {
668        self.writer.end_struct().map_err(StyxSerializeError::new)
669    }
670
671    fn begin_seq(&mut self) -> Result<(), Self::Error> {
672        self.just_wrote_tag = false;
673        self.writer.clear_skip_before_value();
674        self.writer.begin_seq();
675        Ok(())
676    }
677
678    fn end_seq(&mut self) -> Result<(), Self::Error> {
679        self.writer.end_seq().map_err(StyxSerializeError::new)
680    }
681
682    fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
683        if self.just_wrote_tag && matches!(scalar, ScalarValue::Unit | ScalarValue::Null) {
684            self.just_wrote_tag = false;
685            self.writer.clear_skip_before_value();
686            return Ok(());
687        }
688        self.just_wrote_tag = false;
689        match scalar {
690            ScalarValue::Unit | ScalarValue::Null => self.writer.write_null(),
691            ScalarValue::Bool(v) => self.writer.write_bool(v),
692            ScalarValue::Char(c) => self.writer.write_char(c),
693            ScalarValue::I64(v) => self.writer.write_i64(v),
694            ScalarValue::U64(v) => self.writer.write_u64(v),
695            ScalarValue::I128(v) => self.writer.write_i128(v),
696            ScalarValue::U128(v) => self.writer.write_u128(v),
697            ScalarValue::F64(v) => self.writer.write_f64(v),
698            ScalarValue::Str(s) => self.writer.write_string(&s),
699            ScalarValue::Bytes(bytes) => self.writer.write_bytes(&bytes),
700        }
701        Ok(())
702    }
703
704    fn serialize_none(&mut self) -> Result<(), Self::Error> {
705        if self.just_wrote_tag {
706            self.just_wrote_tag = false;
707            self.writer.clear_skip_before_value();
708            return Ok(());
709        }
710        self.writer.write_null();
711        Ok(())
712    }
713
714    fn write_variant_tag(&mut self, variant_name: &str) -> Result<bool, Self::Error> {
715        if self.just_wrote_tag {
716            self.writer.write_tag_chain_segment(variant_name);
717        } else {
718            self.writer.write_tag(variant_name);
719        }
720        self.just_wrote_tag = true;
721        Ok(true)
722    }
723
724    fn begin_struct_after_tag(&mut self) -> Result<(), Self::Error> {
725        self.just_wrote_tag = false;
726        self.writer.begin_struct_after_tag(false);
727        Ok(())
728    }
729
730    fn begin_seq_after_tag(&mut self) -> Result<(), Self::Error> {
731        self.just_wrote_tag = false;
732        self.writer.begin_seq_after_tag();
733        Ok(())
734    }
735
736    fn finish_variant_tag_unit_payload(&mut self) -> Result<(), Self::Error> {
737        self.just_wrote_tag = false;
738        self.writer.clear_skip_before_value();
739        Ok(())
740    }
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use facet::Facet;
747    use facet_testhelpers::test;
748
749    #[derive(Facet, Debug)]
750    struct Simple {
751        name: String,
752        value: i32,
753    }
754
755    #[derive(Facet, Debug)]
756    struct Nested {
757        inner: Simple,
758    }
759
760    #[derive(Facet, Debug)]
761    struct WithVec {
762        items: Vec<i32>,
763    }
764
765    #[derive(Facet, Debug)]
766    struct WithOptional {
767        required: String,
768        optional: Option<i32>,
769    }
770
771    #[test]
772    fn test_simple_struct() {
773        let value = Simple {
774            name: "hello".into(),
775            value: 42,
776        };
777        let result = to_string(&value).unwrap();
778        assert!(result.contains("name hello"));
779        assert!(result.contains("value 42"));
780    }
781
782    #[test]
783    fn test_compact_struct() {
784        let value = Simple {
785            name: "hello".into(),
786            value: 42,
787        };
788        let result = to_string_compact(&value).unwrap();
789        assert_eq!(result, "{name hello, value 42}");
790    }
791
792    #[test]
793    fn test_nested_struct() {
794        let value = Nested {
795            inner: Simple {
796                name: "test".into(),
797                value: 123,
798            },
799        };
800        let result = to_string(&value).unwrap();
801        assert!(result.contains("inner"));
802        // Nested struct should be inline by default
803        assert!(result.contains("{name test, value 123}"));
804    }
805
806    #[test]
807    fn test_sequence() {
808        let value = WithVec {
809            items: vec![1, 2, 3, 4, 5],
810        };
811        let result = to_string(&value).unwrap();
812        assert!(result.contains("items (1 2 3 4 5)"));
813    }
814
815    #[test]
816    fn test_quoted_string() {
817        let value = Simple {
818            name: "hello world".into(), // Has space, needs quoting
819            value: 42,
820        };
821        let result = to_string(&value).unwrap();
822        assert!(result.contains("name \"hello world\""));
823    }
824
825    #[test]
826    fn test_special_chars_need_quoting() {
827        let value = Simple {
828            name: "{braces}".into(),
829            value: 42,
830        };
831        let result = to_string(&value).unwrap();
832        assert!(result.contains("name \"{braces}\""));
833    }
834
835    #[test]
836    fn test_optional_none() {
837        let value = WithOptional {
838            required: "hello".into(),
839            optional: None,
840        };
841        let result = to_string(&value).unwrap();
842        assert!(result.contains("required hello"));
843        // optional None is serialized as @ (unit value)
844        assert!(result.contains("optional @"));
845    }
846
847    #[test]
848    fn test_optional_some() {
849        let value = WithOptional {
850            required: "hello".into(),
851            optional: Some(42),
852        };
853        let result = to_string(&value).unwrap();
854        assert!(result.contains("required hello"));
855        assert!(result.contains("optional 42"));
856    }
857
858    #[test]
859    fn test_bool_values() {
860        #[derive(Facet, Debug)]
861        struct Flags {
862            enabled: bool,
863            debug: bool,
864        }
865
866        let value = Flags {
867            enabled: true,
868            debug: false,
869        };
870        let result = to_string(&value).unwrap();
871        assert!(result.contains("enabled true"));
872        assert!(result.contains("debug false"));
873    }
874
875    #[test]
876    fn test_bare_scalar_rules() {
877        use styx_format::can_be_bare;
878
879        // These should be bare
880        assert!(can_be_bare("localhost"));
881        assert!(can_be_bare("8080"));
882        assert!(can_be_bare("hello-world"));
883        assert!(can_be_bare("https://example.com/path"));
884
885        // These must be quoted
886        assert!(!can_be_bare("")); // empty
887        assert!(!can_be_bare("hello world")); // space
888        assert!(!can_be_bare("{braces}")); // braces
889        assert!(!can_be_bare("(parens)")); // parens
890        assert!(!can_be_bare("key=value")); // equals
891        assert!(!can_be_bare("@tag")); // at sign
892        assert!(!can_be_bare("//comment")); // looks like comment
893        assert!(!can_be_bare("r#raw")); // looks like raw string
894        assert!(!can_be_bare("<<HERE")); // looks like heredoc
895    }
896
897    #[test]
898    fn test_roundtrip_simple() {
899        use crate::from_str;
900
901        #[derive(Facet, Debug, PartialEq)]
902        struct Config {
903            name: String,
904            port: u16,
905            debug: bool,
906        }
907
908        let original = Config {
909            name: "myapp".into(),
910            port: 8080,
911            debug: true,
912        };
913
914        let serialized = to_string(&original).unwrap();
915        let parsed: Config = from_str(&serialized).unwrap();
916
917        assert_eq!(original.name, parsed.name);
918        assert_eq!(original.port, parsed.port);
919        assert_eq!(original.debug, parsed.debug);
920    }
921
922    #[test]
923    fn test_roundtrip_nested() {
924        use crate::from_str;
925
926        #[derive(Facet, Debug, PartialEq)]
927        struct Inner {
928            x: i32,
929            y: i32,
930        }
931
932        #[derive(Facet, Debug, PartialEq)]
933        struct Outer {
934            name: String,
935            point: Inner,
936        }
937
938        let original = Outer {
939            name: "origin".into(),
940            point: Inner { x: 10, y: 20 },
941        };
942
943        let serialized = to_string(&original).unwrap();
944        let parsed: Outer = from_str(&serialized).unwrap();
945
946        assert_eq!(original.name, parsed.name);
947        assert_eq!(original.point.x, parsed.point.x);
948        assert_eq!(original.point.y, parsed.point.y);
949    }
950
951    #[test]
952    fn test_nested_newtype_variants_serialize_as_chained_tags() {
953        use crate::{from_str_expr, peek_to_string_expr};
954
955        #[derive(Facet, Debug, PartialEq)]
956        #[facet(rename_all = "snake_case")]
957        #[repr(u8)]
958        enum EventPattern {
959            DiscoverStart { executor: String },
960            DiscoverEnd,
961            ExecStart,
962        }
963
964        #[derive(Facet, Debug, PartialEq)]
965        #[facet(rename_all = "snake_case")]
966        #[repr(u8)]
967        enum EventAssertion {
968            MustEmit(EventPattern),
969            MustNotEmit(EventPattern),
970        }
971
972        let original = EventAssertion::MustEmit(EventPattern::DiscoverStart {
973            executor: "default".into(),
974        });
975
976        let serialized = peek_to_string_expr(Peek::new(&original)).unwrap();
977        assert_eq!(serialized, "@must_emit/@discover_start{executor default}");
978
979        let parsed: EventAssertion = from_str_expr(&serialized).unwrap();
980        assert_eq!(parsed, original);
981    }
982
983    #[test]
984    fn test_nested_unit_variants_serialize_as_chained_tags() {
985        use crate::{from_str_expr, peek_to_string_expr};
986
987        #[derive(Facet, Debug, PartialEq)]
988        #[facet(rename_all = "snake_case")]
989        #[repr(u8)]
990        enum EventPattern {
991            DiscoverEnd,
992            ExecStart,
993        }
994
995        #[derive(Facet, Debug, PartialEq)]
996        #[facet(rename_all = "snake_case")]
997        #[repr(u8)]
998        enum EventAssertion {
999            MustEmit(EventPattern),
1000            MustNotEmit(EventPattern),
1001        }
1002
1003        let original = EventAssertion::MustNotEmit(EventPattern::ExecStart);
1004        let serialized = peek_to_string_expr(Peek::new(&original)).unwrap();
1005        assert_eq!(serialized, "@must_not_emit/@exec_start");
1006
1007        let parsed: EventAssertion = from_str_expr(&serialized).unwrap();
1008        assert_eq!(parsed, original);
1009    }
1010
1011    #[test]
1012    fn test_three_nested_newtype_variants_serialize_as_chained_tags() {
1013        use crate::{from_str_expr, peek_to_string_expr};
1014
1015        #[derive(Facet, Debug, PartialEq)]
1016        #[facet(rename_all = "snake_case")]
1017        #[repr(u8)]
1018        enum Leaf {
1019            Done,
1020        }
1021
1022        #[derive(Facet, Debug, PartialEq)]
1023        #[facet(rename_all = "snake_case")]
1024        #[repr(u8)]
1025        enum Middle {
1026            Inner(Leaf),
1027        }
1028
1029        #[derive(Facet, Debug, PartialEq)]
1030        #[facet(rename_all = "snake_case")]
1031        #[repr(u8)]
1032        enum Outer {
1033            Wrapper(Middle),
1034        }
1035
1036        let original = Outer::Wrapper(Middle::Inner(Leaf::Done));
1037        let serialized = peek_to_string_expr(Peek::new(&original)).unwrap();
1038        assert_eq!(serialized, "@wrapper/@inner/@done");
1039
1040        let parsed: Outer = from_str_expr(&serialized).unwrap();
1041        assert_eq!(parsed, original);
1042    }
1043
1044    #[test]
1045    fn test_nested_scalar_newtype_variants_serialize_as_chained_tags() {
1046        use crate::{from_str_expr, peek_to_string_expr};
1047
1048        #[derive(Facet, Debug, PartialEq)]
1049        #[facet(rename_all = "snake_case")]
1050        #[repr(u8)]
1051        enum EventPattern {
1052            Message(String),
1053        }
1054
1055        #[derive(Facet, Debug, PartialEq)]
1056        #[facet(rename_all = "snake_case")]
1057        #[repr(u8)]
1058        enum EventAssertion {
1059            MustEmit(EventPattern),
1060        }
1061
1062        let original = EventAssertion::MustEmit(EventPattern::Message("hello world".into()));
1063        let serialized = peek_to_string_expr(Peek::new(&original)).unwrap();
1064        assert_eq!(serialized, r#"@must_emit/@message"hello world""#);
1065
1066        let parsed: EventAssertion = from_str_expr(&serialized).unwrap();
1067        assert_eq!(parsed, original);
1068    }
1069
1070    #[test]
1071    fn test_roundtrip_with_vec() {
1072        use crate::from_str;
1073
1074        #[derive(Facet, Debug, PartialEq)]
1075        struct Data {
1076            values: Vec<i32>,
1077        }
1078
1079        let original = Data {
1080            values: vec![1, 2, 3, 4, 5],
1081        };
1082
1083        let serialized = to_string(&original).unwrap();
1084        let parsed: Data = from_str(&serialized).unwrap();
1085
1086        assert_eq!(original.values, parsed.values);
1087    }
1088
1089    #[test]
1090    fn test_roundtrip_quoted_string() {
1091        use crate::from_str;
1092
1093        #[derive(Facet, Debug, PartialEq)]
1094        struct Message {
1095            text: String,
1096        }
1097
1098        let original = Message {
1099            text: "hello world with spaces".into(),
1100        };
1101
1102        let serialized = to_string(&original).unwrap();
1103        let parsed: Message = from_str(&serialized).unwrap();
1104
1105        assert_eq!(original.text, parsed.text);
1106    }
1107
1108    #[test]
1109    fn test_peek_to_string_expr_wraps_objects() {
1110        // Expression mode should always wrap objects in braces
1111        let value = Simple {
1112            name: "test".into(),
1113            value: 42,
1114        };
1115        let peek = Peek::new(&value);
1116        let result = peek_to_string_expr(peek).unwrap();
1117
1118        // Should have braces (unlike document mode which omits them for root)
1119        assert!(
1120            result.starts_with('{'),
1121            "expression should start with brace: {}",
1122            result
1123        );
1124        assert!(
1125            result.ends_with('}'),
1126            "expression should end with brace: {}",
1127            result
1128        );
1129        assert!(result.contains("name test"));
1130        assert!(result.contains("value 42"));
1131    }
1132
1133    #[test]
1134    fn test_peek_to_string_expr_nested() {
1135        // Nested objects should also have braces
1136        let value = Nested {
1137            inner: Simple {
1138                name: "nested".into(),
1139                value: 123,
1140            },
1141        };
1142        let peek = Peek::new(&value);
1143        let result = peek_to_string_expr(peek).unwrap();
1144
1145        assert!(result.starts_with('{'));
1146        assert!(result.contains("inner {"));
1147    }
1148
1149    #[test]
1150    fn test_peek_to_string_expr_scalar() {
1151        // Scalars should just be the value
1152        let value: i32 = 42;
1153        let peek = Peek::new(&value);
1154        let result = peek_to_string_expr(peek).unwrap();
1155        assert_eq!(result, "42");
1156    }
1157
1158    #[test]
1159    fn test_doc_metadata_field() {
1160        use crate::schema_types::Documented;
1161
1162        // A struct with documented fields
1163        #[derive(Facet, Debug)]
1164        struct Config {
1165            name: Documented<String>,
1166            port: Documented<u16>,
1167        }
1168
1169        let config = Config {
1170            name: Documented::with_doc_line("myapp".into(), "The application name"),
1171            port: Documented::with_doc_line(8080, "Port to listen on"),
1172        };
1173
1174        let serialized = to_string(&config).unwrap();
1175
1176        // Doc comments should appear before the field key
1177        assert!(serialized.contains("/// The application name\nname myapp"));
1178        assert!(serialized.contains("/// Port to listen on\nport 8080"));
1179    }
1180
1181    #[test]
1182    fn test_field_doc_comments_not_emitted_for_regular_values() {
1183        // Doc comments on Rust fields should NOT be emitted when serializing regular values.
1184        // Only Documented<T> (metadata containers) should emit doc comments.
1185        #[derive(Facet, Debug)]
1186        struct Server {
1187            /// The hostname to bind to
1188            host: String,
1189            /// The port number (1-65535)
1190            port: u16,
1191        }
1192
1193        let server = Server {
1194            host: "localhost".into(),
1195            port: 8080,
1196        };
1197
1198        let serialized = to_string(&server).unwrap();
1199
1200        // Doc comments should NOT appear - we're serializing a value, not a schema
1201        assert!(!serialized.contains("///"));
1202        assert!(serialized.contains("host localhost"));
1203        assert!(serialized.contains("port 8080"));
1204    }
1205
1206    #[test]
1207    fn test_hashmap_with_documented_keys_serialize() {
1208        use crate::schema_types::Documented;
1209        use std::collections::HashMap;
1210
1211        // A HashMap with Documented keys - doc comments are attached to keys, not values
1212        let mut map: HashMap<Documented<String>, i32> = HashMap::new();
1213        map.insert(
1214            Documented::with_doc_line("port".to_string(), "The port to listen on"),
1215            8080,
1216        );
1217        map.insert(
1218            Documented::with_doc_line("timeout".to_string(), "Timeout in seconds"),
1219            30,
1220        );
1221
1222        let serialized = to_string(&map).unwrap();
1223        tracing::debug!("Serialized HashMap:\n{}", serialized);
1224
1225        // Doc comments should appear before each key
1226        assert!(serialized.contains("/// The port to listen on\nport 8080"));
1227        assert!(serialized.contains("/// Timeout in seconds\ntimeout 30"));
1228    }
1229
1230    #[test]
1231    fn test_hashmap_with_documented_keys_roundtrip() {
1232        use crate::schema_types::Documented;
1233        use std::collections::HashMap;
1234
1235        // Parse a styx document with doc comments into HashMap<Documented<String>, i32>
1236        let input = r#"
1237/// The port to listen on
1238port 8080
1239/// Timeout in seconds
1240timeout 30
1241"#;
1242
1243        let parsed: HashMap<Documented<String>, i32> =
1244            crate::from_str(input).expect("should parse");
1245
1246        tracing::debug!("Parsed HashMap: {:?}", parsed);
1247
1248        // Check we got the right values
1249        assert_eq!(
1250            parsed.get(&Documented::new("port".to_string())),
1251            Some(&8080)
1252        );
1253        assert_eq!(
1254            parsed.get(&Documented::new("timeout".to_string())),
1255            Some(&30)
1256        );
1257
1258        // Check we got the doc comments
1259        let port_key = parsed
1260            .keys()
1261            .find(|k| k.value == "port")
1262            .expect("should have port key");
1263        assert_eq!(
1264            port_key.doc(),
1265            Some(&["The port to listen on".to_string()][..])
1266        );
1267
1268        let timeout_key = parsed
1269            .keys()
1270            .find(|k| k.value == "timeout")
1271            .expect("should have timeout key");
1272        assert_eq!(
1273            timeout_key.doc(),
1274            Some(&["Timeout in seconds".to_string()][..])
1275        );
1276    }
1277}