extern crate alloc;
#[cfg_attr(feature = "fast", allow(unused_imports))]
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use core::fmt::{self, Debug};
use facet_core::Facet;
use facet_format::{FormatSerializer, ScalarValue, SerializeError, serialize_root};
use facet_reflect::Peek;
#[derive(Debug)]
pub struct YamlSerializeError {
msg: String,
}
impl fmt::Display for YamlSerializeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.msg)
}
}
impl std::error::Error for YamlSerializeError {}
impl YamlSerializeError {
fn new(msg: impl Into<String>) -> Self {
Self { msg: msg.into() }
}
}
#[derive(Debug, Clone, Copy)]
enum Ctx {
Struct { indent: usize, has_fields: bool },
Seq { indent: usize, has_items: bool },
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum LinePos {
Start,
AfterSeqMarker,
Inline,
}
pub struct YamlSerializer {
out: Vec<u8>,
stack: Vec<Ctx>,
doc_started: bool,
line_pos: LinePos,
}
impl YamlSerializer {
pub const fn new() -> Self {
Self {
out: Vec::new(),
stack: Vec::new(),
doc_started: false,
line_pos: LinePos::Start,
}
}
pub fn finish(self) -> Vec<u8> {
self.out
}
fn ensure_doc_started(&mut self) {
if !self.doc_started {
self.out.extend_from_slice(b"---\n");
self.doc_started = true;
self.line_pos = LinePos::Start;
}
}
fn write_indent(&mut self, depth: usize) {
for _ in 0..depth {
self.out.extend_from_slice(b" ");
}
}
fn newline(&mut self) {
if self.line_pos != LinePos::Start {
self.out.push(b'\n');
self.line_pos = LinePos::Start;
}
}
fn write_seq_item_prefix(&mut self, seq_indent: usize) {
self.newline();
self.write_indent(seq_indent);
self.out.extend_from_slice(b"- ");
self.line_pos = LinePos::AfterSeqMarker;
}
fn write_field_prefix(&mut self, indent: usize) {
self.newline();
self.write_indent(indent);
self.line_pos = LinePos::Inline;
}
fn current_indent(&self) -> usize {
match self.stack.last() {
Some(Ctx::Struct { indent, .. }) => *indent,
Some(Ctx::Seq { indent, .. }) => *indent,
None => 0,
}
}
fn should_use_block_scalar(s: &str) -> bool {
if !s.contains('\n') {
return false;
}
if s.trim().is_empty() {
return false;
}
if s.contains('\r') {
return false;
}
true
}
fn write_block_scalar(&mut self, s: &str, indent: usize) {
let chomping = if s.ends_with('\n') {
if s.ends_with("\n\n") {
"+" } else {
"" }
} else {
"-" };
self.out.push(b'|');
self.out.extend_from_slice(chomping.as_bytes());
let content = if chomping == "+" {
s.trim_end_matches('\n')
} else if chomping == "-" {
s
} else {
s.trim_end_matches('\n')
};
for line in content.split('\n') {
self.out.push(b'\n');
self.write_indent(indent + 1);
self.out.extend_from_slice(line.as_bytes());
}
if chomping == "+" {
let trailing_count = s.len() - s.trim_end_matches('\n').len();
for _ in 1..trailing_count {
self.out.push(b'\n');
}
}
self.line_pos = LinePos::Inline;
}
fn needs_quotes(s: &str) -> bool {
s.is_empty()
|| s.contains(':')
|| s.contains('#')
|| s.contains('\n')
|| s.contains('\r')
|| s.contains('"')
|| s.contains('\'')
|| s.starts_with(' ')
|| s.ends_with(' ')
|| s.starts_with('-')
|| s.starts_with('?')
|| s.starts_with('*')
|| s.starts_with('&')
|| s.starts_with('!')
|| s.starts_with('|')
|| s.starts_with('>')
|| s.starts_with('%')
|| s.starts_with('@')
|| s.starts_with('`')
|| s.starts_with('[')
|| s.starts_with('{')
|| looks_like_bool(s)
|| looks_like_null(s)
|| looks_like_number(s)
}
fn write_string(&mut self, s: &str) {
if Self::should_use_block_scalar(s) {
let indent = self.current_indent();
self.write_block_scalar(s, indent);
} else if Self::needs_quotes(s) {
self.out.push(b'"');
for c in s.chars() {
match c {
'"' => self.out.extend_from_slice(b"\\\""),
'\\' => self.out.extend_from_slice(b"\\\\"),
'\n' => self.out.extend_from_slice(b"\\n"),
'\r' => self.out.extend_from_slice(b"\\r"),
'\t' => self.out.extend_from_slice(b"\\t"),
c if c.is_control() => {
self.out
.extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes());
}
c => {
let mut buf = [0u8; 4];
self.out
.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
}
}
self.out.push(b'"');
self.line_pos = LinePos::Inline;
} else {
self.out.extend_from_slice(s.as_bytes());
self.line_pos = LinePos::Inline;
}
}
}
impl Default for YamlSerializer {
fn default() -> Self {
Self::new()
}
}
impl FormatSerializer for YamlSerializer {
type Error = YamlSerializeError;
fn begin_struct(&mut self) -> Result<(), Self::Error> {
self.ensure_doc_started();
let (struct_indent, seq_indent_for_prefix) = match self.stack.last() {
Some(Ctx::Seq { indent, .. }) => {
(*indent + 1, Some(*indent))
}
Some(Ctx::Struct { indent, .. }) => {
(*indent + 1, None)
}
None => {
(0, None)
}
};
if let Some(seq_indent) = seq_indent_for_prefix {
self.write_seq_item_prefix(seq_indent);
if let Some(Ctx::Seq { has_items, .. }) = self.stack.last_mut() {
*has_items = true;
}
}
self.stack.push(Ctx::Struct {
indent: struct_indent,
has_fields: false,
});
Ok(())
}
fn field_key(&mut self, key: &str) -> Result<(), Self::Error> {
let (indent, has_fields) = match self.stack.last() {
Some(Ctx::Struct { indent, has_fields }) => (*indent, *has_fields),
_ => {
return Err(YamlSerializeError::new(
"field_key called outside of a struct",
));
}
};
if !has_fields && self.line_pos == LinePos::AfterSeqMarker {
} else {
self.write_field_prefix(indent);
}
self.write_string(key);
self.out.extend_from_slice(b": ");
self.line_pos = LinePos::Inline;
if let Some(Ctx::Struct { has_fields, .. }) = self.stack.last_mut() {
*has_fields = true;
}
Ok(())
}
fn end_struct(&mut self) -> Result<(), Self::Error> {
match self.stack.pop() {
Some(Ctx::Struct { has_fields, .. }) => {
if !has_fields {
self.out.extend_from_slice(b"{}");
self.line_pos = LinePos::Inline;
}
Ok(())
}
_ => Err(YamlSerializeError::new(
"end_struct called without matching begin_struct",
)),
}
}
fn begin_seq(&mut self) -> Result<(), Self::Error> {
self.ensure_doc_started();
let (new_seq_indent, parent_seq_indent) = match self.stack.last() {
Some(Ctx::Seq { indent, .. }) => {
(*indent + 1, Some(*indent))
}
Some(Ctx::Struct { indent, .. }) => {
(*indent + 1, None)
}
None => {
(0, None)
}
};
if let Some(parent_indent) = parent_seq_indent {
self.newline();
self.write_indent(parent_indent);
self.out.extend_from_slice(b"-");
self.line_pos = LinePos::Inline;
if let Some(Ctx::Seq { has_items, .. }) = self.stack.last_mut() {
*has_items = true;
}
}
self.stack.push(Ctx::Seq {
indent: new_seq_indent,
has_items: false,
});
Ok(())
}
fn end_seq(&mut self) -> Result<(), Self::Error> {
match self.stack.pop() {
Some(Ctx::Seq { has_items, .. }) => {
if !has_items {
self.out.extend_from_slice(b"[]");
self.line_pos = LinePos::Inline;
}
Ok(())
}
_ => Err(YamlSerializeError::new(
"end_seq called without matching begin_seq",
)),
}
}
fn scalar(&mut self, scalar: ScalarValue<'_>) -> Result<(), Self::Error> {
self.ensure_doc_started();
let seq_indent = match self.stack.last() {
Some(Ctx::Seq { indent, .. }) => Some(*indent),
_ => None,
};
if let Some(indent) = seq_indent {
self.write_seq_item_prefix(indent);
if let Some(Ctx::Seq { has_items, .. }) = self.stack.last_mut() {
*has_items = true;
}
}
match scalar {
ScalarValue::Null | ScalarValue::Unit => self.out.extend_from_slice(b"null"),
ScalarValue::Bool(v) => {
if v {
self.out.extend_from_slice(b"true")
} else {
self.out.extend_from_slice(b"false")
}
}
ScalarValue::Char(c) => {
let mut buf = [0u8; 4];
self.write_string(c.encode_utf8(&mut buf));
}
ScalarValue::I64(v) => {
#[cfg(feature = "fast")]
self.out
.extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
#[cfg(not(feature = "fast"))]
self.out.extend_from_slice(v.to_string().as_bytes());
}
ScalarValue::U64(v) => {
#[cfg(feature = "fast")]
self.out
.extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
#[cfg(not(feature = "fast"))]
self.out.extend_from_slice(v.to_string().as_bytes());
}
ScalarValue::I128(v) => {
#[cfg(feature = "fast")]
self.out
.extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
#[cfg(not(feature = "fast"))]
self.out.extend_from_slice(v.to_string().as_bytes());
}
ScalarValue::U128(v) => {
#[cfg(feature = "fast")]
self.out
.extend_from_slice(itoa::Buffer::new().format(v).as_bytes());
#[cfg(not(feature = "fast"))]
self.out.extend_from_slice(v.to_string().as_bytes());
}
ScalarValue::F64(v) => {
#[cfg(feature = "fast")]
self.out
.extend_from_slice(zmij::Buffer::new().format(v).as_bytes());
#[cfg(not(feature = "fast"))]
self.out.extend_from_slice(v.to_string().as_bytes());
}
ScalarValue::Str(s) => self.write_string(&s),
ScalarValue::Bytes(_) => {
return Err(YamlSerializeError::new(
"bytes serialization not supported for YAML",
));
}
}
self.line_pos = LinePos::Inline;
Ok(())
}
}
fn looks_like_bool(s: &str) -> bool {
matches!(
s.to_lowercase().as_str(),
"true" | "false" | "yes" | "no" | "on" | "off" | "y" | "n"
)
}
fn looks_like_null(s: &str) -> bool {
matches!(s.to_lowercase().as_str(), "null" | "~" | "nil" | "none")
}
fn looks_like_number(s: &str) -> bool {
if s.is_empty() {
return false;
}
let s = s.trim();
s.parse::<i64>().is_ok() || s.parse::<f64>().is_ok()
}
pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError<YamlSerializeError>>
where
T: Facet<'facet> + ?Sized,
{
let bytes = to_vec(value)?;
Ok(String::from_utf8(bytes).expect("YAML output should always be valid UTF-8"))
}
pub fn to_vec<'facet, T>(value: &T) -> Result<Vec<u8>, SerializeError<YamlSerializeError>>
where
T: Facet<'facet> + ?Sized,
{
let mut serializer = YamlSerializer::new();
serialize_root(&mut serializer, Peek::new(value))?;
let mut output = serializer.finish();
if !output.ends_with(b"\n") {
output.push(b'\n');
}
Ok(output)
}
pub fn peek_to_string<'input, 'facet>(
peek: Peek<'input, 'facet>,
) -> Result<String, SerializeError<YamlSerializeError>> {
let mut serializer = YamlSerializer::new();
serialize_root(&mut serializer, peek)?;
let mut output = serializer.finish();
if !output.ends_with(b"\n") {
output.push(b'\n');
}
Ok(String::from_utf8(output).expect("YAML output should always be valid UTF-8"))
}
pub fn to_writer<'facet, W, T>(writer: W, value: &T) -> std::io::Result<()>
where
W: std::io::Write,
T: Facet<'facet> + ?Sized,
{
peek_to_writer(writer, Peek::new(value))
}
pub fn peek_to_writer<'input, 'facet, W>(
mut writer: W,
peek: Peek<'input, 'facet>,
) -> std::io::Result<()>
where
W: std::io::Write,
{
let mut serializer = YamlSerializer::new();
serialize_root(&mut serializer, peek).map_err(|e| std::io::Error::other(format!("{:?}", e)))?;
let mut output = serializer.finish();
if !output.ends_with(b"\n") {
output.push(b'\n');
}
writer.write_all(&output)
}