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