Skip to main content

noyalib/
fmt.rs

1//! Formatting wrappers for fine-grained control over YAML output style.
2//!
3//! These wrappers let you control the YAML output style per-value rather than
4//! globally via [`SerializerConfig`](crate::SerializerConfig). Each wrapper
5//! serializes transparently during deserialization but emits style hints during
6//! serialization.
7//!
8//! # Examples
9//!
10//! ```rust
11//! use noyalib::fmt::{FlowSeq, LitString};
12//!
13//! #[derive(serde::Serialize, serde::Deserialize)]
14//! struct Config {
15//!     tags: FlowSeq<Vec<String>>,
16//!     script: LitString,
17//! }
18//! ```
19
20// SPDX-License-Identifier: MIT OR Apache-2.0
21// Copyright (c) 2026 Noyalib. All rights reserved.
22
23use crate::prelude::*;
24use core::ops::Deref;
25
26// Magic names used as newtype struct sentinels.
27// The serializer intercepts these to apply formatting hints.
28pub(crate) const MAGIC_FLOW_SEQ: &str = "__noya_flow_seq";
29pub(crate) const MAGIC_FLOW_MAP: &str = "__noya_flow_map";
30pub(crate) const MAGIC_LIT_STR: &str = "__noya_lit_str";
31pub(crate) const MAGIC_FOLD_STR: &str = "__noya_fold_str";
32pub(crate) const MAGIC_COMMENTED: &str = "__noya_commented";
33pub(crate) const MAGIC_SPACE_AFTER: &str = "__noya_space_after";
34pub(crate) const MAGIC_ANCHOR_DEF: &str = "__noya_anchor_def";
35pub(crate) const MAGIC_ANCHOR_REF: &str = "__noya_anchor_ref";
36/// Newtype name `TaggedValue::serialize` wraps its single-entry-map wire
37/// form in. A serializer with no tag concept (`serde_json`) passes a
38/// newtype through and still sees `{"!tag": value}`; this crate's own
39/// serializer recognises the name and rebuilds `Value::Tagged`, so a
40/// genuine mapping whose only key starts with `!` is never mistaken for
41/// a tag (#377).
42pub(crate) const MAGIC_TAGGED: &str = "__noya_tagged";
43
44/// Force flow style `[a, b, c]` for a sequence value.
45///
46/// # Examples
47///
48/// ```
49/// use noyalib::{to_string, FlowSeq};
50/// #[derive(serde::Serialize)]
51/// struct Doc { v: FlowSeq<Vec<i32>> }
52/// let yaml = to_string(&Doc { v: FlowSeq(vec![1, 2]) }).unwrap();
53/// assert!(yaml.contains("["));
54/// ```
55#[derive(Clone, PartialEq, Eq, Hash)]
56pub struct FlowSeq<T>(pub T);
57
58impl<T: fmt::Debug> fmt::Debug for FlowSeq<T> {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.debug_tuple("FlowSeq").field(&self.0).finish()
61    }
62}
63
64impl<T> Deref for FlowSeq<T> {
65    type Target = T;
66    fn deref(&self) -> &T {
67        &self.0
68    }
69}
70
71impl<T> From<T> for FlowSeq<T> {
72    fn from(v: T) -> Self {
73        Self(v)
74    }
75}
76
77impl<T> FlowSeq<T> {
78    /// Unwrap into the inner value.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use noyalib::FlowSeq;
84    /// let f = FlowSeq(vec![1, 2, 3]);
85    /// assert_eq!(f.into_inner(), vec![1, 2, 3]);
86    /// ```
87    pub fn into_inner(self) -> T {
88        self.0
89    }
90}
91
92impl<T: serde_core::Serialize> serde_core::Serialize for FlowSeq<T> {
93    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94    where
95        S: serde_core::Serializer,
96    {
97        serializer.serialize_newtype_struct(MAGIC_FLOW_SEQ, &self.0)
98    }
99}
100
101impl<'de, T: serde_core::Deserialize<'de>> serde_core::Deserialize<'de> for FlowSeq<T> {
102    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103    where
104        D: serde_core::Deserializer<'de>,
105    {
106        T::deserialize(deserializer).map(FlowSeq)
107    }
108}
109
110/// Force flow style `{k: v, ...}` for a mapping value.
111///
112/// # Examples
113///
114/// ```
115/// use noyalib::{to_string, FlowMap};
116/// use std::collections::BTreeMap;
117/// #[derive(serde::Serialize)]
118/// struct Doc { m: FlowMap<BTreeMap<String, i32>> }
119/// let mut m = BTreeMap::new();
120/// let _ = m.insert("a".into(), 1);
121/// let yaml = to_string(&Doc { m: FlowMap(m) }).unwrap();
122/// assert!(yaml.contains("{"));
123/// ```
124#[derive(Clone, PartialEq, Eq, Hash)]
125pub struct FlowMap<T>(pub T);
126
127impl<T: fmt::Debug> fmt::Debug for FlowMap<T> {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.debug_tuple("FlowMap").field(&self.0).finish()
130    }
131}
132
133impl<T> Deref for FlowMap<T> {
134    type Target = T;
135    fn deref(&self) -> &T {
136        &self.0
137    }
138}
139
140impl<T> From<T> for FlowMap<T> {
141    fn from(v: T) -> Self {
142        Self(v)
143    }
144}
145
146impl<T> FlowMap<T> {
147    /// Unwrap into the inner value.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use noyalib::FlowMap;
153    /// use std::collections::BTreeMap;
154    /// let mut m = BTreeMap::new();
155    /// let _ = m.insert("k".to_string(), 1);
156    /// let f = FlowMap(m);
157    /// assert_eq!(f.into_inner().len(), 1);
158    /// ```
159    pub fn into_inner(self) -> T {
160        self.0
161    }
162}
163
164impl<T: serde_core::Serialize> serde_core::Serialize for FlowMap<T> {
165    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
166    where
167        S: serde_core::Serializer,
168    {
169        serializer.serialize_newtype_struct(MAGIC_FLOW_MAP, &self.0)
170    }
171}
172
173impl<'de, T: serde_core::Deserialize<'de>> serde_core::Deserialize<'de> for FlowMap<T> {
174    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175    where
176        D: serde_core::Deserializer<'de>,
177    {
178        T::deserialize(deserializer).map(FlowMap)
179    }
180}
181
182/// Force literal block scalar `|` style for a borrowed string.
183///
184/// # Examples
185///
186/// ```
187/// use noyalib::fmt::LitStr;
188/// let s = LitStr("line1\nline2\n");
189/// assert_eq!(s.as_str(), "line1\nline2\n");
190/// ```
191#[derive(Clone, PartialEq, Eq, Hash)]
192pub struct LitStr<'a>(pub &'a str);
193
194impl fmt::Debug for LitStr<'_> {
195    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196        f.debug_tuple("LitStr").field(&self.0).finish()
197    }
198}
199
200impl Deref for LitStr<'_> {
201    type Target = str;
202    fn deref(&self) -> &str {
203        self.0
204    }
205}
206
207impl<'a> From<&'a str> for LitStr<'a> {
208    fn from(v: &'a str) -> Self {
209        Self(v)
210    }
211}
212
213impl<'a> LitStr<'a> {
214    /// Get the inner string slice.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use noyalib::fmt::LitStr;
220    /// let s = LitStr("hello");
221    /// assert_eq!(s.as_str(), "hello");
222    /// ```
223    #[must_use]
224    pub fn as_str(&self) -> &str {
225        self.0
226    }
227
228    /// Unwrap into the inner string slice.
229    ///
230    /// # Examples
231    ///
232    /// ```
233    /// use noyalib::fmt::LitStr;
234    /// let s = LitStr("hello");
235    /// assert_eq!(s.into_inner(), "hello");
236    /// ```
237    #[must_use]
238    pub fn into_inner(self) -> &'a str {
239        self.0
240    }
241}
242
243impl serde_core::Serialize for LitStr<'_> {
244    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
245    where
246        S: serde_core::Serializer,
247    {
248        serializer.serialize_newtype_struct(MAGIC_LIT_STR, self.0)
249    }
250}
251
252/// Force literal block scalar `|` style for an owned string.
253///
254/// # Examples
255///
256/// ```
257/// use noyalib::LitString;
258/// let s = LitString("line1\nline2\n".to_string());
259/// assert_eq!(&*s, "line1\nline2\n");
260/// ```
261#[derive(Clone, PartialEq, Eq, Hash)]
262pub struct LitString(pub String);
263
264impl fmt::Debug for LitString {
265    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266        f.debug_tuple("LitString").field(&self.0).finish()
267    }
268}
269
270impl Deref for LitString {
271    type Target = str;
272    fn deref(&self) -> &str {
273        &self.0
274    }
275}
276
277impl From<String> for LitString {
278    fn from(v: String) -> Self {
279        Self(v)
280    }
281}
282
283impl From<&str> for LitString {
284    fn from(v: &str) -> Self {
285        Self(v.to_owned())
286    }
287}
288
289impl LitString {
290    /// Unwrap into the inner `String`.
291    ///
292    /// # Examples
293    ///
294    /// ```
295    /// use noyalib::LitString;
296    /// let s = LitString("hi".into());
297    /// assert_eq!(s.into_inner(), "hi");
298    /// ```
299    #[must_use]
300    pub fn into_inner(self) -> String {
301        self.0
302    }
303}
304
305impl serde_core::Serialize for LitString {
306    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
307    where
308        S: serde_core::Serializer,
309    {
310        serializer.serialize_newtype_struct(MAGIC_LIT_STR, &self.0)
311    }
312}
313
314impl<'de> serde_core::Deserialize<'de> for LitString {
315    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
316    where
317        D: serde_core::Deserializer<'de>,
318    {
319        String::deserialize(deserializer).map(LitString)
320    }
321}
322
323/// Force folded block scalar `>` style for a borrowed string.
324///
325/// # Examples
326///
327/// ```
328/// use noyalib::fmt::FoldStr;
329/// let s = FoldStr("line1\n\nline2");
330/// assert_eq!(s.as_str(), "line1\n\nline2");
331/// ```
332#[derive(Clone, PartialEq, Eq, Hash)]
333pub struct FoldStr<'a>(pub &'a str);
334
335impl fmt::Debug for FoldStr<'_> {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        f.debug_tuple("FoldStr").field(&self.0).finish()
338    }
339}
340
341impl Deref for FoldStr<'_> {
342    type Target = str;
343    fn deref(&self) -> &str {
344        self.0
345    }
346}
347
348impl<'a> From<&'a str> for FoldStr<'a> {
349    fn from(v: &'a str) -> Self {
350        Self(v)
351    }
352}
353
354impl<'a> FoldStr<'a> {
355    /// Get the inner string slice.
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// use noyalib::fmt::FoldStr;
361    /// assert_eq!(FoldStr("x").as_str(), "x");
362    /// ```
363    #[must_use]
364    pub fn as_str(&self) -> &str {
365        self.0
366    }
367
368    /// Unwrap into the inner string slice.
369    ///
370    /// # Examples
371    ///
372    /// ```
373    /// use noyalib::fmt::FoldStr;
374    /// assert_eq!(FoldStr("x").into_inner(), "x");
375    /// ```
376    #[must_use]
377    pub fn into_inner(self) -> &'a str {
378        self.0
379    }
380}
381
382impl serde_core::Serialize for FoldStr<'_> {
383    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
384    where
385        S: serde_core::Serializer,
386    {
387        serializer.serialize_newtype_struct(MAGIC_FOLD_STR, self.0)
388    }
389}
390
391/// Force folded block scalar `>` style for an owned string.
392///
393/// # Examples
394///
395/// ```
396/// use noyalib::FoldString;
397/// let s = FoldString("para one\n\npara two".to_string());
398/// assert_eq!(&*s, "para one\n\npara two");
399/// ```
400#[derive(Clone, PartialEq, Eq, Hash)]
401pub struct FoldString(pub String);
402
403impl fmt::Debug for FoldString {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        f.debug_tuple("FoldString").field(&self.0).finish()
406    }
407}
408
409impl Deref for FoldString {
410    type Target = str;
411    fn deref(&self) -> &str {
412        &self.0
413    }
414}
415
416impl From<String> for FoldString {
417    fn from(v: String) -> Self {
418        Self(v)
419    }
420}
421
422impl From<&str> for FoldString {
423    fn from(v: &str) -> Self {
424        Self(v.to_owned())
425    }
426}
427
428impl FoldString {
429    /// Unwrap into the inner `String`.
430    ///
431    /// # Examples
432    ///
433    /// ```
434    /// use noyalib::FoldString;
435    /// assert_eq!(FoldString("x".into()).into_inner(), "x");
436    /// ```
437    #[must_use]
438    pub fn into_inner(self) -> String {
439        self.0
440    }
441}
442
443impl serde_core::Serialize for FoldString {
444    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
445    where
446        S: serde_core::Serializer,
447    {
448        serializer.serialize_newtype_struct(MAGIC_FOLD_STR, &self.0)
449    }
450}
451
452impl<'de> serde_core::Deserialize<'de> for FoldString {
453    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
454    where
455        D: serde_core::Deserializer<'de>,
456    {
457        String::deserialize(deserializer).map(FoldString)
458    }
459}
460
461/// Attach an inline YAML comment `# ...` after a value.
462///
463/// The comment text should not include the `#` prefix.
464///
465/// **Note:** Comments are serialization-only metadata. When deserializing,
466/// the `comment` field is always empty because YAML comments are not
467/// part of the data model and cannot survive a roundtrip.
468///
469/// # Examples
470///
471/// ```
472/// use noyalib::{to_string, Commented};
473/// #[derive(serde::Serialize)]
474/// struct Doc { v: Commented<i32> }
475/// let yaml = to_string(&Doc { v: Commented::new(42, "meaning") }).unwrap();
476/// assert!(yaml.contains("# meaning"));
477/// ```
478#[derive(Clone, PartialEq, Eq, Hash)]
479pub struct Commented<T> {
480    /// The inner value.
481    pub value: T,
482    /// The comment text (without `#` prefix).
483    pub comment: String,
484}
485
486impl<T: fmt::Debug> fmt::Debug for Commented<T> {
487    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
488        f.debug_struct("Commented")
489            .field("value", &self.value)
490            .field("comment", &self.comment)
491            .finish()
492    }
493}
494
495impl<T> Commented<T> {
496    /// Create a new commented value.
497    ///
498    /// # Examples
499    ///
500    /// ```
501    /// use noyalib::Commented;
502    /// let c = Commented::new(42, "the answer");
503    /// assert_eq!(c.value, 42);
504    /// assert_eq!(c.comment, "the answer");
505    /// ```
506    pub fn new(value: T, comment: impl Into<String>) -> Self {
507        Self {
508            value,
509            comment: comment.into(),
510        }
511    }
512
513    /// Unwrap into the inner value.
514    ///
515    /// # Examples
516    ///
517    /// ```
518    /// use noyalib::Commented;
519    /// let c = Commented::new(7, "note");
520    /// assert_eq!(c.into_inner(), 7);
521    /// ```
522    pub fn into_inner(self) -> T {
523        self.value
524    }
525}
526
527impl<T> Deref for Commented<T> {
528    type Target = T;
529    fn deref(&self) -> &T {
530        &self.value
531    }
532}
533
534impl<T: serde_core::Serialize> serde_core::Serialize for Commented<T> {
535    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
536    where
537        S: serde_core::Serializer,
538    {
539        use serde_core::ser::SerializeTuple as _;
540
541        // Serialize as a tuple (value, comment) wrapped in the magic newtype
542        struct Inner<'a, T>(&'a T, &'a str);
543
544        impl<T: serde_core::Serialize> serde_core::Serialize for Inner<'_, T> {
545            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
546            where
547                S: serde_core::Serializer,
548            {
549                let mut tup = serializer.serialize_tuple(2)?;
550                tup.serialize_element(self.0)?;
551                tup.serialize_element(self.1)?;
552                tup.end()
553            }
554        }
555
556        serializer.serialize_newtype_struct(MAGIC_COMMENTED, &Inner(&self.value, &self.comment))
557    }
558}
559
560impl<'de, T: serde_core::Deserialize<'de>> serde_core::Deserialize<'de> for Commented<T> {
561    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
562    where
563        D: serde_core::Deserializer<'de>,
564    {
565        // Note: comments are serialization-only metadata and cannot survive a
566        // roundtrip through YAML. Deserializing always produces an empty comment.
567        T::deserialize(deserializer).map(|v| Self {
568            value: v,
569            comment: String::new(),
570        })
571    }
572}
573
574/// Emit a blank line after the value.
575///
576/// # Examples
577///
578/// ```
579/// use noyalib::SpaceAfter;
580/// let s = SpaceAfter("section".to_string());
581/// assert_eq!(s.0, "section");
582/// ```
583#[derive(Clone, PartialEq, Eq, Hash)]
584pub struct SpaceAfter<T>(pub T);
585
586impl<T: fmt::Debug> fmt::Debug for SpaceAfter<T> {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        f.debug_tuple("SpaceAfter").field(&self.0).finish()
589    }
590}
591
592impl<T> Deref for SpaceAfter<T> {
593    type Target = T;
594    fn deref(&self) -> &T {
595        &self.0
596    }
597}
598
599impl<T> From<T> for SpaceAfter<T> {
600    fn from(v: T) -> Self {
601        Self(v)
602    }
603}
604
605impl<T> SpaceAfter<T> {
606    /// Unwrap into the inner value.
607    ///
608    /// # Examples
609    ///
610    /// ```
611    /// use noyalib::SpaceAfter;
612    /// assert_eq!(SpaceAfter("x".to_string()).into_inner(), "x");
613    /// ```
614    pub fn into_inner(self) -> T {
615        self.0
616    }
617}
618
619impl<T: serde_core::Serialize> serde_core::Serialize for SpaceAfter<T> {
620    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
621    where
622        S: serde_core::Serializer,
623    {
624        serializer.serialize_newtype_struct(MAGIC_SPACE_AFTER, &self.0)
625    }
626}
627
628impl<'de, T: serde_core::Deserialize<'de>> serde_core::Deserialize<'de> for SpaceAfter<T> {
629    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
630    where
631        D: serde_core::Deserializer<'de>,
632    {
633        T::deserialize(deserializer).map(SpaceAfter)
634    }
635}