serde_saphyr/wrappers.rs
1use serde_core::de::{self, Deserialize, Deserializer, Visitor};
2use std::fmt;
3use std::marker::PhantomData;
4
5/// Force a sequence to be emitted in flow style: `[a, b, c]`.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub struct FlowSeq<T>(pub T);
8
9/// Force a mapping to be emitted in flow style: `{k1: v1, k2: v2}`.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct FlowMap<T>(pub T);
12
13/// Force a string value to be emitted in double-quoted style.
14///
15/// This wrapper is transparent during deserialization: the inner value is
16/// deserialized normally and placed into `DoubleQuoted<T>`. `DoubleQuoted<T>` implements
17/// Serde traits only for string-like `T` values.
18///
19/// ```rust
20/// # #[cfg(feature = "serialize")]
21/// # {
22/// use serde::Serialize;
23/// use serde_saphyr::DoubleQuoted;
24///
25/// #[derive(Serialize)]
26/// struct Config {
27/// value: DoubleQuoted<String>,
28/// }
29///
30/// let cfg = Config { value: DoubleQuoted("plain text".to_string()) };
31/// let yaml = serde_saphyr::to_string(&cfg).unwrap();
32/// assert_eq!(yaml, "value: \"plain text\"\n");
33/// # }
34/// ```
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct DoubleQuoted<T>(pub T);
37
38/// Force a string value to be emitted in a single-quoted style. This provides additional
39/// safety constraints, as serializer rejects control characters and other values that require
40/// double-quoted string escaping.
41///
42/// This wrapper is transparent during deserialization: the inner value is
43/// deserialized normally and placed into `SingleQuoted<T>`. `SingleQuoted<T>` implements
44/// Serde traits only for string-like `T` values.
45///
46/// Serialization fails if the value cannot be represented
47/// safely in YAML single-quoted style.
48///
49/// ```rust
50/// # #[cfg(feature = "serialize")]
51/// # {
52/// use serde::Serialize;
53/// use serde_saphyr::SingleQuoted;
54///
55/// #[derive(Serialize)]
56/// struct Config {
57/// value: SingleQuoted<String>,
58/// }
59///
60/// let cfg = Config { value: SingleQuoted("plain text".to_string()) };
61/// let yaml = serde_saphyr::to_string(&cfg).unwrap();
62/// assert_eq!(yaml, "value: 'plain text'\n");
63/// # }
64/// ```
65#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct SingleQuoted<T>(pub T);
67
68/// Add an empty line after the wrapped value when serializing.
69///
70/// This wrapper is transparent during deserialization and can be nested with
71/// other wrappers like `Commented`, `FlowSeq`, etc.
72/// ```rust
73/// # #[cfg(feature = "serialize")]
74/// # {
75/// use serde::Serialize;
76/// use serde_saphyr::SpaceAfter;
77///
78/// #[derive(Serialize)]
79/// struct Config {
80/// first: SpaceAfter<i32>,
81/// second: i32,
82/// }
83///
84/// let cfg = Config { first: SpaceAfter(1), second: 2 };
85/// let yaml = serde_saphyr::to_string(&cfg).unwrap();
86/// // The output will have an empty line after "first: 1"
87/// # }
88/// ```
89/// **Important:** Avoid using this wrapper with `LitStr`/`LitString` as it may add the empty
90/// line to the string content. For `FoldStr`/`FoldString` and other YAML values
91/// (e.g. `key: value`, quoted scalars), the extra empty line is cosmetic.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct SpaceAfter<T>(pub T);
94
95/// Serialize `None` as YAML tilde (`~`) while otherwise behaving like `Option<T>`.
96///
97/// `Some(value)` is serialized transparently as `value`. `None` is serialized
98/// as `~` instead of the serializer's regular null spelling. Deserialization
99/// delegates to `Option<T>`, so `~`, `null`, and empty YAML values all become
100/// `NullableTilde(None)`.
101///
102/// ```rust
103/// # #[cfg(feature = "serialize")]
104/// # {
105/// use serde::Serialize;
106/// use serde_saphyr::NullableTilde;
107///
108/// #[derive(Serialize)]
109/// struct Config {
110/// maybe: NullableTilde<String>,
111/// }
112///
113/// let cfg = Config { maybe: NullableTilde(None) };
114/// let yaml = serde_saphyr::to_string(&cfg).unwrap();
115/// assert_eq!(yaml, "maybe: ~\n");
116/// # }
117/// ```
118#[derive(Clone, Debug, PartialEq, Eq)]
119pub struct NullableTilde<T>(pub Option<T>);
120
121/// Attach an inline YAML comment to a value when serializing.
122///
123/// This wrapper lets you annotate a scalar with an inline YAML comment that is
124/// emitted after the value when using block style. The typical form is:
125/// `value # comment`. This is the most useful when deserializing the anchor
126/// reference (so a human reader can see what a referenced value represents).
127///
128/// Comment is also captured into its field when deserializing YAML.
129///
130/// Behavior
131/// - Block style (default): the comment appears after the scalar on the same line.
132/// - Flow style (inside `[ ... ]` or `{ ... }`): comments are suppressed to keep
133/// the flow representation compact and unambiguous.
134/// - Complex values (sequences/maps/structs): the comment is ignored; only the
135/// inner value is serialized to preserve indentation and layout.
136/// - Newlines in comments are sanitized to spaces so the comment remains on a
137/// single line (e.g., "a\nb" becomes "a b").
138/// - Deserialization of `Commented<T>` captures nearby source comments when the
139/// `serde-saphyr` deserializer can provide them. Other deserializers treat it
140/// transparently and produce an empty comment string.
141/// - Comment capture is use-site oriented for replayed YAML. Comments from an
142/// anchor definition are not copied through aliases or merge keys; a field
143/// materialized by `<<: *defaults` does not inherit comments that were written
144/// above the field inside `&defaults`.
145/// - For container values, comments attached to the parent value itself are
146/// captured only by `Commented<Container>` and are not inherited by the first
147/// child field or element. A comment inside the container, directly above a
148/// child key or element, is captured by that child.
149/// - When an alias to a container is used as a nested value, leading comments
150/// above the alias follow the same inside-container rule. For example,
151/// `root:\n # comment\n *defaults` leaves the comment available to the
152/// expanded container's first child rather than capturing it on the alias use.
153///
154/// Examples
155///
156/// Basic scalar with a comment in block style:
157/// ```rust
158/// # #[cfg(feature = "serialize")]
159/// # {
160/// use serde::Serialize;
161///
162/// // Re-exported from the crate root
163/// use serde_saphyr::Commented;
164///
165/// let out = serde_saphyr::to_string(&Commented(42, "answer".to_string())).unwrap();
166/// assert_eq!(out, "42 # answer\n");
167/// # }
168/// ```
169///
170/// As a mapping value, still inline:
171/// ```rust
172/// # #[cfg(feature = "serialize")]
173/// # {
174/// use serde::Serialize;
175/// use serde_saphyr::Commented;
176///
177/// #[derive(Serialize)]
178/// struct S { xn: Commented<i32> }
179///
180/// let s = S { xn: Commented(5, "send five starships first".into()) };
181/// let out = serde_saphyr::to_string(&s).unwrap();
182/// assert_eq!(out, "xn: 5 # send five starships first\n");
183/// # }
184/// ```
185///
186/// *Important*: Comments are suppressed in flow contexts (no `#` appears), and
187/// ignored for complex inner values during serialization. During deserialization,
188/// parent-side comments on a container such as `root: # comment` are captured by
189/// `Commented<Container>` only; comments inside the container, directly above the
190/// first child key or element, remain available to that child. The same applies
191/// to leading comments above a nested alias whose target is a container.
192#[derive(Clone, Debug, PartialEq, Eq)]
193pub struct Commented<T>(pub T, pub String);
194
195#[cfg(feature = "garde")]
196impl<T: garde::Validate> garde::Validate for Commented<T> {
197 type Context = T::Context;
198
199 fn validate_into(
200 &self,
201 ctx: &Self::Context,
202 parent: &mut dyn FnMut() -> garde::Path,
203 report: &mut garde::Report,
204 ) {
205 self.0.validate_into(ctx, parent, report);
206 }
207}
208
209#[cfg(feature = "validator")]
210impl<T: validator::Validate> validator::Validate for Commented<T> {
211 fn validate(&self) -> Result<(), validator::ValidationErrors> {
212 self.0.validate()
213 }
214}
215
216#[cfg(feature = "validator")]
217impl<'v_a, T: validator::ValidateArgs<'v_a>> validator::ValidateArgs<'v_a> for Commented<T> {
218 type Args = T::Args;
219
220 fn validate_with_args(&self, args: Self::Args) -> Result<(), validator::ValidationErrors> {
221 self.0.validate_with_args(args)
222 }
223}
224
225impl<'de, T: Deserialize<'de>> Deserialize<'de> for FlowSeq<T> {
226 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
227 T::deserialize(deserializer).map(FlowSeq)
228 }
229}
230
231impl<'de, T: Deserialize<'de>> Deserialize<'de> for FlowMap<T> {
232 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
233 T::deserialize(deserializer).map(FlowMap)
234 }
235}
236
237impl<'de, T> Deserialize<'de> for DoubleQuoted<T>
238where
239 T: Deserialize<'de> + AsRef<str>,
240{
241 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
242 T::deserialize(deserializer).map(DoubleQuoted)
243 }
244}
245
246impl<'de, T> Deserialize<'de> for SingleQuoted<T>
247where
248 T: Deserialize<'de> + AsRef<str>,
249{
250 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
251 T::deserialize(deserializer).map(SingleQuoted)
252 }
253}
254
255impl<'de, T: Deserialize<'de>> Deserialize<'de> for Commented<T> {
256 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
257 struct CommentedVisitor<T>(PhantomData<T>);
258
259 impl<'de, T: Deserialize<'de>> Visitor<'de> for CommentedVisitor<T> {
260 type Value = Commented<T>;
261
262 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263 formatter.write_str("a commented YAML value")
264 }
265
266 fn visit_newtype_struct<D>(
267 self,
268 deserializer: D,
269 ) -> std::result::Result<Self::Value, D::Error>
270 where
271 D: Deserializer<'de>,
272 {
273 T::deserialize(deserializer).map(|value| Commented(value, String::new()))
274 }
275
276 fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Self::Value, A::Error>
277 where
278 A: de::SeqAccess<'de>,
279 {
280 let value = seq
281 .next_element()?
282 .ok_or_else(|| de::Error::invalid_length(0, &self))?;
283 let comment = seq.next_element()?.unwrap_or_default();
284 Ok(Commented(value, comment))
285 }
286 }
287
288 deserializer.deserialize_newtype_struct("__yaml_commented", CommentedVisitor(PhantomData))
289 }
290}
291
292impl<'de, T: Deserialize<'de>> Deserialize<'de> for SpaceAfter<T> {
293 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
294 T::deserialize(deserializer).map(SpaceAfter)
295 }
296}
297
298impl<'de, T: Deserialize<'de>> Deserialize<'de> for NullableTilde<T> {
299 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
300 Option::<T>::deserialize(deserializer).map(NullableTilde)
301 }
302}
303
304#[cfg(all(test, feature = "deserialize"))]
305mod tests {
306 use serde::Deserialize;
307
308 use crate::{
309 Commented, DoubleQuoted, FlowMap, FlowSeq, NullableTilde, SingleQuoted, SpaceAfter,
310 };
311
312 #[derive(Debug, Deserialize, PartialEq)]
313 struct WrappersDoc {
314 seq: FlowSeq<Vec<u32>>,
315 map: FlowMap<std::collections::BTreeMap<String, u32>>,
316 after: SpaceAfter<String>,
317 nullable_tilde_none: NullableTilde<String>,
318 nullable_tilde_some: NullableTilde<String>,
319 commented: Commented<bool>,
320 double_quoted: DoubleQuoted<String>,
321 single_quoted: SingleQuoted<String>,
322 }
323
324 #[test]
325 fn wrappers_remain_deserializable_without_serialize() {
326 let value: WrappersDoc = crate::from_str(
327 "seq: [1, 2]\nmap: {a: 1}\nafter: hello\nnullable_tilde_none: ~\nnullable_tilde_some: value\ncommented: true\ndouble_quoted: value\nsingle_quoted: value\n",
328 )
329 .unwrap();
330
331 assert_eq!(value.seq, FlowSeq(vec![1, 2]));
332 assert_eq!(value.after, SpaceAfter("hello".to_string()));
333 assert_eq!(value.nullable_tilde_none, NullableTilde(None));
334 assert_eq!(
335 value.nullable_tilde_some,
336 NullableTilde(Some("value".to_string()))
337 );
338 assert_eq!(value.commented, Commented(true, String::new()));
339 assert_eq!(value.double_quoted, DoubleQuoted("value".to_string()));
340 assert_eq!(value.single_quoted, SingleQuoted("value".to_string()));
341 assert_eq!(value.map.0.get("a"), Some(&1));
342 }
343}