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 { indent: usize, has_fields: bool },
42 Seq { indent: usize, has_items: bool },
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
48enum LinePos {
49 Start,
51 AfterSeqMarker,
53 Inline,
55}
56
57pub struct YamlSerializer {
59 out: Vec<u8>,
60 stack: Vec<Ctx>,
61 doc_started: bool,
63 line_pos: LinePos,
65}
66
67impl YamlSerializer {
68 pub fn new() -> Self {
70 Self {
71 out: Vec::new(),
72 stack: Vec::new(),
73 doc_started: false,
74 line_pos: LinePos::Start,
75 }
76 }
77
78 pub fn finish(self) -> Vec<u8> {
80 self.out
81 }
82
83 fn ensure_doc_started(&mut self) {
85 if !self.doc_started {
86 self.out.extend_from_slice(b"---\n");
87 self.doc_started = true;
88 self.line_pos = LinePos::Start;
89 }
90 }
91
92 fn write_indent(&mut self, depth: usize) {
94 for _ in 0..depth {
95 self.out.extend_from_slice(b" ");
96 }
97 }
98
99 fn newline(&mut self) {
101 if self.line_pos != LinePos::Start {
102 self.out.push(b'\n');
103 self.line_pos = LinePos::Start;
104 }
105 }
106
107 fn write_seq_item_prefix(&mut self, seq_indent: usize) {
110 self.newline();
111 self.write_indent(seq_indent);
112 self.out.extend_from_slice(b"- ");
113 self.line_pos = LinePos::AfterSeqMarker;
114 }
115
116 fn write_field_prefix(&mut self, indent: usize) {
119 self.newline();
120 self.write_indent(indent);
121 self.line_pos = LinePos::Inline;
122 }
123
124 fn current_indent(&self) -> usize {
126 match self.stack.last() {
127 Some(Ctx::Struct { indent, .. }) => *indent,
128 Some(Ctx::Seq { indent, .. }) => *indent,
129 None => 0,
130 }
131 }
132
133 fn should_use_block_scalar(s: &str) -> bool {
136 if !s.contains('\n') {
138 return false;
139 }
140
141 if s.trim().is_empty() {
143 return false;
144 }
145
146 if s.contains('\r') {
148 return false;
149 }
150
151 true
152 }
153
154 fn write_block_scalar(&mut self, s: &str, indent: usize) {
157 let chomping = if s.ends_with('\n') {
162 if s.ends_with("\n\n") {
163 "+" } else {
165 "" }
167 } else {
168 "-" };
170
171 self.out.push(b'|');
172 self.out.extend_from_slice(chomping.as_bytes());
173
174 let content = if chomping == "+" {
177 s.trim_end_matches('\n')
178 } else if chomping == "-" {
179 s
180 } else {
181 s.trim_end_matches('\n')
182 };
183
184 for line in content.split('\n') {
185 self.out.push(b'\n');
186 self.write_indent(indent + 1);
187 self.out.extend_from_slice(line.as_bytes());
188 }
189
190 if chomping == "+" {
192 let trailing_count = s.len() - s.trim_end_matches('\n').len();
193 for _ in 1..trailing_count {
194 self.out.push(b'\n');
195 }
196 }
197
198 self.line_pos = LinePos::Inline;
199 }
200
201 fn needs_quotes(s: &str) -> bool {
203 s.is_empty()
204 || s.contains(':')
205 || s.contains('#')
206 || s.contains('\n')
207 || s.contains('\r')
208 || s.contains('"')
209 || s.contains('\'')
210 || s.starts_with(' ')
211 || s.ends_with(' ')
212 || s.starts_with('-')
213 || s.starts_with('?')
214 || s.starts_with('*')
215 || s.starts_with('&')
216 || s.starts_with('!')
217 || s.starts_with('|')
218 || s.starts_with('>')
219 || s.starts_with('%')
220 || s.starts_with('@')
221 || s.starts_with('`')
222 || s.starts_with('[')
223 || s.starts_with('{')
224 || looks_like_bool(s)
225 || looks_like_null(s)
226 || looks_like_number(s)
227 }
228
229 fn write_string(&mut self, s: &str) {
231 if Self::should_use_block_scalar(s) {
232 let indent = self.current_indent();
233 self.write_block_scalar(s, indent);
234 } else if Self::needs_quotes(s) {
235 self.out.push(b'"');
236 for c in s.chars() {
237 match c {
238 '"' => self.out.extend_from_slice(b"\\\""),
239 '\\' => self.out.extend_from_slice(b"\\\\"),
240 '\n' => self.out.extend_from_slice(b"\\n"),
241 '\r' => self.out.extend_from_slice(b"\\r"),
242 '\t' => self.out.extend_from_slice(b"\\t"),
243 c if c.is_control() => {
244 self.out
245 .extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes());
246 }
247 c => {
248 let mut buf = [0u8; 4];
249 self.out
250 .extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
251 }
252 }
253 }
254 self.out.push(b'"');
255 self.line_pos = LinePos::Inline;
256 } else {
257 self.out.extend_from_slice(s.as_bytes());
258 self.line_pos = LinePos::Inline;
259 }
260 }
261}
262
263impl Default for YamlSerializer {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl FormatSerializer for YamlSerializer {
270 type Error = YamlSerializeError;
271
272 fn begin_struct(&mut self) -> Result<(), Self::Error> {
273 self.ensure_doc_started();
274
275 let (struct_indent, seq_indent_for_prefix) = match self.stack.last() {
277 Some(Ctx::Seq { indent, .. }) => {
278 (*indent + 1, Some(*indent))
280 }
281 Some(Ctx::Struct { indent, .. }) => {
282 (*indent + 1, None)
284 }
285 None => {
286 (0, None)
288 }
289 };
290
291 if let Some(seq_indent) = seq_indent_for_prefix {
293 self.write_seq_item_prefix(seq_indent);
294 if let Some(Ctx::Seq { has_items, .. }) = self.stack.last_mut() {
296 *has_items = true;
297 }
298 }
299
300 self.stack.push(Ctx::Struct {
303 indent: struct_indent,
304 has_fields: false,
305 });
306 Ok(())
307 }
308
309 fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
310 let (indent, has_fields) = match self.stack.last() {
311 Some(Ctx::Struct { indent, has_fields }) => (*indent, *has_fields),
312 _ => {
313 return Err(YamlSerializeError::new(
314 "field_key called outside of a struct",
315 ));
316 }
317 };
318
319 if !has_fields && self.line_pos == LinePos::AfterSeqMarker {
322 } else {
325 self.write_field_prefix(indent);
327 }
328
329 self.write_string(key);
330 self.out.extend_from_slice(b": ");
331 self.line_pos = LinePos::Inline;
332
333 if let Some(Ctx::Struct { has_fields, .. }) = self.stack.last_mut() {
335 *has_fields = true;
336 }
337
338 Ok(())
339 }
340
341 fn end_struct(&mut self) -> Result<(), Self::Error> {
342 match self.stack.pop() {
343 Some(Ctx::Struct { has_fields, .. }) => {
344 if !has_fields {
346 self.out.extend_from_slice(b"{}");
347 self.line_pos = LinePos::Inline;
348 }
349 Ok(())
350 }
351 _ => Err(YamlSerializeError::new(
352 "end_struct called without matching begin_struct",
353 )),
354 }
355 }
356
357 fn begin_seq(&mut self) -> Result<(), Self::Error> {
358 self.ensure_doc_started();
359
360 let (new_seq_indent, parent_seq_indent) = match self.stack.last() {
362 Some(Ctx::Seq { indent, .. }) => {
363 (*indent + 1, Some(*indent))
365 }
366 Some(Ctx::Struct { indent, .. }) => {
367 (*indent + 1, None)
369 }
370 None => {
371 (0, None)
373 }
374 };
375
376 if let Some(parent_indent) = parent_seq_indent {
378 self.newline();
379 self.write_indent(parent_indent);
380 self.out.extend_from_slice(b"-");
381 self.line_pos = LinePos::Inline;
382 if let Some(Ctx::Seq { has_items, .. }) = self.stack.last_mut() {
384 *has_items = true;
385 }
386 }
387
388 self.stack.push(Ctx::Seq {
389 indent: new_seq_indent,
390 has_items: false,
391 });
392 Ok(())
393 }
394
395 fn end_seq(&mut self) -> Result<(), Self::Error> {
396 match self.stack.pop() {
397 Some(Ctx::Seq { has_items, .. }) => {
398 if !has_items {
400 self.out.extend_from_slice(b"[]");
401 self.line_pos = LinePos::Inline;
402 }
403 Ok(())
404 }
405 _ => Err(YamlSerializeError::new(
406 "end_seq called without matching begin_seq",
407 )),
408 }
409 }
410
411 fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
412 self.ensure_doc_started();
413
414 let seq_indent = match self.stack.last() {
416 Some(Ctx::Seq { indent, .. }) => Some(*indent),
417 _ => None,
418 };
419 if let Some(indent) = seq_indent {
420 self.write_seq_item_prefix(indent);
421 if let Some(Ctx::Seq { has_items, .. }) = self.stack.last_mut() {
423 *has_items = true;
424 }
425 }
426
427 match scalar {
428 ScalarValue::Null => self.out.extend_from_slice(b"null"),
429 ScalarValue::Bool(v) => {
430 if v {
431 self.out.extend_from_slice(b"true")
432 } else {
433 self.out.extend_from_slice(b"false")
434 }
435 }
436 ScalarValue::Char(c) => {
437 let mut buf = [0u8; 4];
438 self.write_string(c.encode_utf8(&mut buf));
439 }
440 ScalarValue::I64(v) => {
441 #[cfg(feature = "fast")]
442 self.out
443 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
444 #[cfg(not(feature = "fast"))]
445 self.out.extend_from_slice(v.to_string().as_bytes());
446 }
447 ScalarValue::U64(v) => {
448 #[cfg(feature = "fast")]
449 self.out
450 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
451 #[cfg(not(feature = "fast"))]
452 self.out.extend_from_slice(v.to_string().as_bytes());
453 }
454 ScalarValue::I128(v) => {
455 #[cfg(feature = "fast")]
456 self.out
457 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
458 #[cfg(not(feature = "fast"))]
459 self.out.extend_from_slice(v.to_string().as_bytes());
460 }
461 ScalarValue::U128(v) => {
462 #[cfg(feature = "fast")]
463 self.out
464 .extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
465 #[cfg(not(feature = "fast"))]
466 self.out.extend_from_slice(v.to_string().as_bytes());
467 }
468 ScalarValue::F64(v) => {
469 #[cfg(feature = "fast")]
470 self.out
471 .extend_from_slice(zmij::Buffer::new().format(v).as_bytes());
472 #[cfg(not(feature = "fast"))]
473 self.out.extend_from_slice(v.to_string().as_bytes());
474 }
475 ScalarValue::Str(s) | ScalarValue::StringlyTyped(s) => self.write_string(&s),
476 ScalarValue::Bytes(_) => {
477 return Err(YamlSerializeError::new(
478 "bytes serialization not supported for YAML",
479 ));
480 }
481 }
482
483 self.line_pos = LinePos::Inline;
484 Ok(())
485 }
486}
487
488fn looks_like_bool(s: &str) -> bool {
490 matches!(
491 s.to_lowercase().as_str(),
492 "true" | "false" | "yes" | "no" | "on" | "off" | "y" | "n"
493 )
494}
495
496fn looks_like_null(s: &str) -> bool {
498 matches!(s.to_lowercase().as_str(), "null" | "~" | "nil" | "none")
499}
500
501fn looks_like_number(s: &str) -> bool {
503 if s.is_empty() {
504 return false;
505 }
506 let s = s.trim();
507 s.parse::<i64>().is_ok() || s.parse::<f64>().is_ok()
508}
509
510pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<YamlSerializeError>>
538where
539 T: Facet<'facet> + ?Sized,
540{
541 let bytes = to_vec(value)?;
542 Ok(String::from_utf8(bytes).expect("YAML output should always be valid UTF-8"))
543}
544
545pub fn to_vec<'facet, T>(value: &T) -> Result<Vec<u8>, SerializeError<YamlSerializeError>>
561where
562 T: Facet<'facet> + ?Sized,
563{
564 let mut serializer = YamlSerializer::new();
565 serialize_root(&mut serializer, Peek::new(value))?;
566 let mut output = serializer.finish();
567 if !output.ends_with(b"\n") {
569 output.push(b'\n');
570 }
571 Ok(output)
572}
573
574pub fn peek_to_string<'input, 'facet>(
579 peek: Peek<'input, 'facet>,
580) -> Result<String, SerializeError<YamlSerializeError>> {
581 let mut serializer = YamlSerializer::new();
582 serialize_root(&mut serializer, peek)?;
583 let mut output = serializer.finish();
584 if !output.ends_with(b"\n") {
585 output.push(b'\n');
586 }
587 Ok(String::from_utf8(output).expect("YAML output should always be valid UTF-8"))
588}
589
590pub fn to_writer<'facet, W, T>(writer: W, value: &T) -> std::io::Result<()>
610where
611 W: std::io::Write,
612 T: Facet<'facet> + ?Sized,
613{
614 peek_to_writer(writer, Peek::new(value))
615}
616
617pub fn peek_to_writer<'input, 'facet, W>(
619 mut writer: W,
620 peek: Peek<'input, 'facet>,
621) -> std::io::Result<()>
622where
623 W: std::io::Write,
624{
625 let mut serializer = YamlSerializer::new();
626 serialize_root(&mut serializer, peek).map_err(|e| std::io::Error::other(format!("{:?}", e)))?;
627 let mut output = serializer.finish();
628 if !output.ends_with(b"\n") {
629 output.push(b'\n');
630 }
631 writer.write_all(&output)
632}