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