1extern crate alloc;
4
5#[cfg_attr(feature = "fast", allow(unused_imports))]
6use alloc::{
7 format,
8 string::{String, ToString},
9 vec::Vec,
10};
11use core::fmt::{self, Debug};
12
13use facet_core::Facet;
14use facet_format::{FormatSerializer, ScalarValue, SerializeError, serialize_root};
15use facet_reflect::Peek;
16
17#[derive(Debug)]
19pub struct YamlSerializeError {
20 msg: String,
21}
22
23impl fmt::Display for YamlSerializeError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 f.write_str(&self.msg)
26 }
27}
28
29impl std::error::Error for YamlSerializeError {}
30
31impl YamlSerializeError {
32 fn new(msg: impl Into<String>) -> Self {
33 Self { msg: msg.into() }
34 }
35}
36
37#[derive(Debug, Clone, Copy)]
39enum Ctx {
40 Struct { first: bool, indent: usize },
42 Seq { first: bool, indent: usize },
44}
45
46pub struct YamlSerializer {
48 out: Vec<u8>,
49 stack: Vec<Ctx>,
50 doc_started: bool,
52 inline_next: bool,
54}
55
56impl YamlSerializer {
57 pub fn new() -> Self {
59 Self {
60 out: Vec::new(),
61 stack: Vec::new(),
62 doc_started: false,
63 inline_next: false,
64 }
65 }
66
67 pub fn finish(self) -> Vec<u8> {
69 self.out
70 }
71
72 fn depth(&self) -> usize {
74 self.stack
75 .last()
76 .map(|ctx| match ctx {
77 Ctx::Struct { indent, .. } => *indent,
78 Ctx::Seq { indent, .. } => *indent,
79 })
80 .unwrap_or(0)
81 }
82
83 fn needs_quotes(s: &str) -> bool {
85 s.is_empty()
86 || s.contains(':')
87 || s.contains('#')
88 || s.contains('\n')
89 || s.contains('\r')
90 || s.contains('"')
91 || s.contains('\'')
92 || s.starts_with(' ')
93 || s.ends_with(' ')
94 || s.starts_with('-')
95 || s.starts_with('?')
96 || s.starts_with('*')
97 || s.starts_with('&')
98 || s.starts_with('!')
99 || s.starts_with('|')
100 || s.starts_with('>')
101 || s.starts_with('%')
102 || s.starts_with('@')
103 || s.starts_with('`')
104 || s.starts_with('[')
105 || s.starts_with('{')
106 || looks_like_bool(s)
107 || looks_like_null(s)
108 || looks_like_number(s)
109 }
110
111 fn write_string(&mut self, s: &str) {
113 if Self::needs_quotes(s) {
114 self.out.push(b'"');
115 for c in s.chars() {
116 match c {
117 '"' => self.out.extend_from_slice(b"\\\""),
118 '\\' => self.out.extend_from_slice(b"\\\\"),
119 '\n' => self.out.extend_from_slice(b"\\n"),
120 '\r' => self.out.extend_from_slice(b"\\r"),
121 '\t' => self.out.extend_from_slice(b"\\t"),
122 c if c.is_control() => {
123 self.out
124 .extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes());
125 }
126 c => {
127 let mut buf = [0u8; 4];
128 self.out
129 .extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
130 }
131 }
132 }
133 self.out.push(b'"');
134 } else {
135 self.out.extend_from_slice(s.as_bytes());
136 }
137 }
138
139 fn write_indent_for(&mut self, depth: usize) {
141 for _ in 0..depth {
142 self.out.extend_from_slice(b" ");
143 }
144 }
145}
146
147impl Default for YamlSerializer {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl FormatSerializer for YamlSerializer {
154 type Error = YamlSerializeError;
155
156 fn begin_struct(&mut self) -> Result<(), Self::Error> {
157 if !self.doc_started {
159 self.out.extend_from_slice(b"---\n");
160 self.doc_started = true;
161 }
162
163 let new_indent = self.depth();
164
165 if self.inline_next {
167 self.out.push(b'\n');
168 self.inline_next = false;
169 }
170
171 self.stack.push(Ctx::Struct {
172 first: true,
173 indent: new_indent,
174 });
175 Ok(())
176 }
177
178 fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
179 let (first, indent) = match self.stack.last() {
181 Some(Ctx::Struct { first, indent }) => (*first, *indent),
182 _ => {
183 return Err(YamlSerializeError::new(
184 "field_key called outside of a struct",
185 ));
186 }
187 };
188
189 if !first {
190 self.out.push(b'\n');
191 }
192
193 self.write_indent_for(indent);
195
196 self.write_string(key);
197 self.out.extend_from_slice(b": ");
198 self.inline_next = true;
199
200 if let Some(Ctx::Struct {
202 first: f,
203 indent: i,
204 }) = self.stack.last_mut()
205 {
206 *f = false;
207 *i = indent + 1;
208 }
209
210 Ok(())
211 }
212
213 fn end_struct(&mut self) -> Result<(), Self::Error> {
214 match self.stack.pop() {
215 Some(Ctx::Struct { first, .. }) => {
216 if first {
218 if self.inline_next {
219 self.inline_next = false;
220 }
221 self.out.extend_from_slice(b"{}");
222 }
223
224 if let Some(Ctx::Struct { indent, .. }) = self.stack.last_mut() {
226 *indent = indent.saturating_sub(1);
227 }
228
229 Ok(())
230 }
231 _ => Err(YamlSerializeError::new(
232 "end_struct called without matching begin_struct",
233 )),
234 }
235 }
236
237 fn begin_seq(&mut self) -> Result<(), Self::Error> {
238 if !self.doc_started {
240 self.out.extend_from_slice(b"---\n");
241 self.doc_started = true;
242 }
243
244 let new_indent = self.depth();
245
246 if self.inline_next {
248 self.out.push(b'\n');
249 self.inline_next = false;
250 }
251
252 self.stack.push(Ctx::Seq {
253 first: true,
254 indent: new_indent,
255 });
256 Ok(())
257 }
258
259 fn end_seq(&mut self) -> Result<(), Self::Error> {
260 match self.stack.pop() {
261 Some(Ctx::Seq { first, .. }) => {
262 if first {
264 if self.inline_next {
265 self.inline_next = false;
266 }
267 self.out.extend_from_slice(b"[]");
268 }
269
270 if let Some(Ctx::Struct { indent, .. }) = self.stack.last_mut() {
272 *indent = indent.saturating_sub(1);
273 }
274
275 Ok(())
276 }
277 _ => Err(YamlSerializeError::new(
278 "end_seq called without matching begin_seq",
279 )),
280 }
281 }
282
283 fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
284 if !self.doc_started {
286 self.out.extend_from_slice(b"---\n");
287 self.doc_started = true;
288 }
289
290 if let Some(Ctx::Seq { first, indent }) = self.stack.last_mut() {
292 if !*first {
293 self.out.push(b'\n');
294 }
295 *first = false;
296
297 let indent_val = *indent;
299 self.write_indent_for(indent_val);
300 self.out.extend_from_slice(b"- ");
301 }
302
303 self.inline_next = false;
304
305 match scalar {
306 ScalarValue::Null => self.out.extend_from_slice(b"null"),
307 ScalarValue::Bool(v) => {
308 if v {
309 self.out.extend_from_slice(b"true")
310 } else {
311 self.out.extend_from_slice(b"false")
312 }
313 }
314 ScalarValue::I64(v) => {
315 #[cfg(feature = "fast")]
316 self.out
317 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
318 #[cfg(not(feature = "fast"))]
319 self.out.extend_from_slice(v.to_string().as_bytes());
320 }
321 ScalarValue::U64(v) => {
322 #[cfg(feature = "fast")]
323 self.out
324 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
325 #[cfg(not(feature = "fast"))]
326 self.out.extend_from_slice(v.to_string().as_bytes());
327 }
328 ScalarValue::I128(v) => {
329 #[cfg(feature = "fast")]
330 self.out
331 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
332 #[cfg(not(feature = "fast"))]
333 self.out.extend_from_slice(v.to_string().as_bytes());
334 }
335 ScalarValue::U128(v) => {
336 #[cfg(feature = "fast")]
337 self.out
338 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
339 #[cfg(not(feature = "fast"))]
340 self.out.extend_from_slice(v.to_string().as_bytes());
341 }
342 ScalarValue::F64(v) => {
343 #[cfg(feature = "fast")]
344 self.out
345 .extend_from_slice(zmij::Buffer::new().format(v).as_bytes());
346 #[cfg(not(feature = "fast"))]
347 self.out.extend_from_slice(v.to_string().as_bytes());
348 }
349 ScalarValue::Str(s) => self.write_string(&s),
350 ScalarValue::Bytes(_) => {
351 return Err(YamlSerializeError::new(
352 "bytes serialization not supported for YAML",
353 ));
354 }
355 }
356
357 if let Some(Ctx::Struct { indent, .. }) = self.stack.last_mut() {
359 *indent = indent.saturating_sub(1);
360 }
361
362 Ok(())
363 }
364}
365
366fn looks_like_bool(s: &str) -> bool {
368 matches!(
369 s.to_lowercase().as_str(),
370 "true" | "false" | "yes" | "no" | "on" | "off" | "y" | "n"
371 )
372}
373
374fn looks_like_null(s: &str) -> bool {
376 matches!(s.to_lowercase().as_str(), "null" | "~" | "nil" | "none")
377}
378
379fn looks_like_number(s: &str) -> bool {
381 if s.is_empty() {
382 return false;
383 }
384 let s = s.trim();
385 s.parse::<i64>().is_ok() || s.parse::<f64>().is_ok()
386}
387
388pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<YamlSerializeError>>
416where
417 T: Facet<'facet> + ?Sized,
418{
419 let bytes = to_vec(value)?;
420 Ok(String::from_utf8(bytes).expect("YAML output should always be valid UTF-8"))
421}
422
423pub fn to_vec<'facet, T>(value: &T) -> Result<Vec<u8>, SerializeError<YamlSerializeError>>
439where
440 T: Facet<'facet> + ?Sized,
441{
442 let mut serializer = YamlSerializer::new();
443 serialize_root(&mut serializer, Peek::new(value))?;
444 let mut output = serializer.finish();
445 if !output.ends_with(b"\n") {
447 output.push(b'\n');
448 }
449 Ok(output)
450}
451
452pub fn peek_to_string<'input, 'facet>(
457 peek: Peek<'input, 'facet>,
458) -> Result<String, SerializeError<YamlSerializeError>> {
459 let mut serializer = YamlSerializer::new();
460 serialize_root(&mut serializer, peek)?;
461 let mut output = serializer.finish();
462 if !output.ends_with(b"\n") {
463 output.push(b'\n');
464 }
465 Ok(String::from_utf8(output).expect("YAML output should always be valid UTF-8"))
466}
467
468pub fn to_writer<'facet, W, T>(writer: W, value: &T) -> std::io::Result<()>
488where
489 W: std::io::Write,
490 T: Facet<'facet> + ?Sized,
491{
492 peek_to_writer(writer, Peek::new(value))
493}
494
495pub fn peek_to_writer<'input, 'facet, W>(
497 mut writer: W,
498 peek: Peek<'input, 'facet>,
499) -> std::io::Result<()>
500where
501 W: std::io::Write,
502{
503 let mut serializer = YamlSerializer::new();
504 serialize_root(&mut serializer, peek).map_err(|e| std::io::Error::other(format!("{:?}", e)))?;
505 let mut output = serializer.finish();
506 if !output.ends_with(b"\n") {
507 output.push(b'\n');
508 }
509 writer.write_all(&output)
510}