serde_saphyr/wrappers.rs
1use serde::de::{Deserialize, Deserializer};
2
3/// Force a sequence to be emitted in flow style: `[a, b, c]`.
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct FlowSeq<T>(pub T);
6
7/// Force a mapping to be emitted in flow style: `{k1: v1, k2: v2}`.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct FlowMap<T>(pub T);
10
11/// Add an empty line after the wrapped value when serializing.
12///
13/// This wrapper is transparent during deserialization and can be nested with
14/// other wrappers like `Commented`, `FlowSeq`, etc.
15/// ```rust
16/// # #[cfg(feature = "serialize")]
17/// # {
18/// use serde::Serialize;
19/// use serde_saphyr::SpaceAfter;
20///
21/// #[derive(Serialize)]
22/// struct Config {
23/// first: SpaceAfter<i32>,
24/// second: i32,
25/// }
26///
27/// let cfg = Config { first: SpaceAfter(1), second: 2 };
28/// let yaml = serde_saphyr::to_string(&cfg).unwrap();
29/// // The output will have an empty line after "first: 1"
30/// # }
31/// ```
32/// **Important:** Avoid using this wrapper with `LitStr`/`LitString` as it may add the empty
33/// line to the string content. For `FoldStr`/`FoldString` and other YAML values
34/// (e.g. `key: value`, quoted scalars), the extra empty line is cosmetic.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct SpaceAfter<T>(pub T);
37
38/// Attach an inline YAML comment to a value when serializing.
39///
40/// This wrapper lets you annotate a scalar with an inline YAML comment that is
41/// emitted after the value when using block style. The typical form is:
42/// `value # comment`. This is the most useful when deserializing the anchor
43/// reference (so a human reader can see what a referenced value represents).
44///
45/// Behavior
46/// - Block style (default): the comment appears after the scalar on the same line.
47/// - Flow style (inside `[ ... ]` or `{ ... }`): comments are suppressed to keep
48/// the flow representation compact and unambiguous.
49/// - Complex values (sequences/maps/structs): the comment is ignored; only the
50/// inner value is serialized to preserve indentation and layout.
51/// - Newlines in comments are sanitized to spaces so the comment remains on a
52/// single line (e.g., "a\nb" becomes "a b").
53/// - Deserialization of `Commented<T>` ignores comments: it behaves like `T` and
54/// produces an empty comment string.
55///
56/// Examples
57///
58/// Basic scalar with a comment in block style:
59/// ```rust
60/// # #[cfg(feature = "serialize")]
61/// # {
62/// use serde::Serialize;
63///
64/// // Re-exported from the crate root
65/// use serde_saphyr::Commented;
66///
67/// let out = serde_saphyr::to_string(&Commented(42, "answer".to_string())).unwrap();
68/// assert_eq!(out, "42 # answer\n");
69/// # }
70/// ```
71///
72/// As a mapping value, still inline:
73/// ```rust
74/// # #[cfg(feature = "serialize")]
75/// # {
76/// use serde::Serialize;
77/// use serde_saphyr::Commented;
78///
79/// #[derive(Serialize)]
80/// struct S { xn: Commented<i32> }
81///
82/// let s = S { xn: Commented(5, "send five starships first".into()) };
83/// let out = serde_saphyr::to_string(&s).unwrap();
84/// assert_eq!(out, "xn: 5 # send five starships first\n");
85/// # }
86/// ```
87///
88/// *Important*: Comments are suppressed in flow contexts (no `#` appears), and
89/// ignored for complex inner values. Value with `Commented` wrapper will be
90/// deserialized correctly as well, but deserializing comments is currently not
91/// supported.
92/// ```
93#[derive(Clone, Debug, PartialEq, Eq)]
94pub struct Commented<T>(pub T, pub String);
95
96impl<'de, T: Deserialize<'de>> Deserialize<'de> for FlowSeq<T> {
97 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
98 T::deserialize(deserializer).map(FlowSeq)
99 }
100}
101
102impl<'de, T: Deserialize<'de>> Deserialize<'de> for FlowMap<T> {
103 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
104 T::deserialize(deserializer).map(FlowMap)
105 }
106}
107
108impl<'de, T: Deserialize<'de>> Deserialize<'de> for Commented<T> {
109 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
110 T::deserialize(deserializer).map(|v| Commented(v, String::new()))
111 }
112}
113
114impl<'de, T: Deserialize<'de>> Deserialize<'de> for SpaceAfter<T> {
115 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
116 T::deserialize(deserializer).map(SpaceAfter)
117 }
118}
119
120#[cfg(all(test, feature = "deserialize"))]
121mod tests {
122 use serde::Deserialize;
123
124 use crate::{Commented, FlowMap, FlowSeq, SpaceAfter};
125
126 #[derive(Debug, Deserialize, PartialEq)]
127 struct WrappersDoc {
128 seq: FlowSeq<Vec<u32>>,
129 map: FlowMap<std::collections::BTreeMap<String, u32>>,
130 after: SpaceAfter<String>,
131 commented: Commented<bool>,
132 }
133
134 #[test]
135 fn wrappers_remain_deserializable_without_serialize() {
136 let value: WrappersDoc =
137 crate::from_str("seq: [1, 2]\nmap: {a: 1}\nafter: hello\ncommented: true\n").unwrap();
138
139 assert_eq!(value.seq, FlowSeq(vec![1, 2]));
140 assert_eq!(value.after, SpaceAfter("hello".to_string()));
141 assert_eq!(value.commented, Commented(true, String::new()));
142 assert_eq!(value.map.0.get("a"), Some(&1));
143 }
144}