use super::value::OwnedValue;
use crate::yaml::simd::find_json_escape;
pub trait StreamableValue {
fn stream_json<W: core::fmt::Write>(&self, out: &mut W) -> core::fmt::Result;
fn stream_yaml<W: core::fmt::Write>(
&self,
out: &mut W,
indent_spaces: usize,
) -> core::fmt::Result;
fn is_falsy(&self) -> bool;
}
#[derive(Debug, Clone, Default)]
pub struct StreamStats {
pub count: usize,
pub last_was_falsy: bool,
}
impl StreamableValue for OwnedValue {
fn stream_json<W: core::fmt::Write>(&self, out: &mut W) -> core::fmt::Result {
stream_owned_value_json(self, out)
}
fn stream_yaml<W: core::fmt::Write>(
&self,
out: &mut W,
indent_spaces: usize,
) -> core::fmt::Result {
stream_owned_value_yaml(self, out, 0, indent_spaces)
}
fn is_falsy(&self) -> bool {
matches!(self, OwnedValue::Null | OwnedValue::Bool(false))
}
}
fn stream_owned_value_json<W: core::fmt::Write>(
value: &OwnedValue,
out: &mut W,
) -> core::fmt::Result {
match value {
OwnedValue::Null => out.write_str("null"),
OwnedValue::Bool(true) => out.write_str("true"),
OwnedValue::Bool(false) => out.write_str("false"),
OwnedValue::Int(n) => write!(out, "{}", n),
OwnedValue::Float(f) => {
if f.is_nan() || f.is_infinite() {
out.write_str("null")
} else {
write!(out, "{}", f)
}
}
OwnedValue::String(s) => stream_json_string(out, s),
OwnedValue::Array(arr) => {
out.write_char('[')?;
for (i, elem) in arr.iter().enumerate() {
if i > 0 {
out.write_char(',')?;
}
stream_owned_value_json(elem, out)?;
}
out.write_char(']')
}
OwnedValue::Object(obj) => {
out.write_char('{')?;
for (i, (key, value)) in obj.iter().enumerate() {
if i > 0 {
out.write_char(',')?;
}
stream_json_string(out, key)?;
out.write_char(':')?;
stream_owned_value_json(value, out)?;
}
out.write_char('}')
}
}
}
fn stream_json_string<W: core::fmt::Write>(out: &mut W, s: &str) -> core::fmt::Result {
out.write_char('"')?;
let bytes = s.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
let escape_pos = find_json_escape(bytes, i);
if i < escape_pos {
out.write_str(&s[i..escape_pos])?;
}
i = escape_pos;
if i < len {
let b = bytes[i];
match b {
b'"' => out.write_str("\\\"")?,
b'\\' => out.write_str("\\\\")?,
b'\n' => out.write_str("\\n")?,
b'\r' => out.write_str("\\r")?,
b'\t' => out.write_str("\\t")?,
b if b < 0x20 => {
out.write_str("\\u00")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char(HEX[(b >> 4) as usize] as char)?;
out.write_char(HEX[(b & 0xf) as usize] as char)?;
}
_ => out.write_char(b as char)?,
}
i += 1;
}
}
out.write_char('"')
}
fn stream_owned_value_yaml<W: core::fmt::Write>(
value: &OwnedValue,
out: &mut W,
current_indent: usize,
indent_spaces: usize,
) -> core::fmt::Result {
match value {
OwnedValue::Null => out.write_str("null"),
OwnedValue::Bool(true) => out.write_str("true"),
OwnedValue::Bool(false) => out.write_str("false"),
OwnedValue::Int(n) => write!(out, "{}", n),
OwnedValue::Float(f) => {
if f.is_nan() {
out.write_str(".nan")
} else if f.is_infinite() {
if *f > 0.0 {
out.write_str(".inf")
} else {
out.write_str("-.inf")
}
} else {
write!(out, "{}", f)
}
}
OwnedValue::String(s) => stream_yaml_string(out, s),
OwnedValue::Array(arr) => {
if arr.is_empty() {
out.write_str("[]")
} else if indent_spaces == 0 {
out.write_char('[')?;
for (i, elem) in arr.iter().enumerate() {
if i > 0 {
out.write_str(", ")?;
}
stream_owned_value_yaml(elem, out, 0, 0)?;
}
out.write_char(']')
} else {
for (i, elem) in arr.iter().enumerate() {
if i > 0 {
out.write_char('\n')?;
write_indent(out, current_indent)?;
}
out.write_str("- ")?;
if matches!(elem, OwnedValue::Array(_) | OwnedValue::Object(_))
&& !is_empty_container(elem)
{
out.write_char('\n')?;
write_indent(out, current_indent + indent_spaces)?;
stream_owned_value_yaml(
elem,
out,
current_indent + indent_spaces,
indent_spaces,
)?;
} else {
stream_owned_value_yaml(
elem,
out,
current_indent + indent_spaces,
indent_spaces,
)?;
}
}
Ok(())
}
}
OwnedValue::Object(obj) => {
if obj.is_empty() {
out.write_str("{}")
} else if indent_spaces == 0 {
out.write_char('{')?;
for (i, (key, val)) in obj.iter().enumerate() {
if i > 0 {
out.write_str(", ")?;
}
stream_yaml_string(out, key)?;
out.write_str(": ")?;
stream_owned_value_yaml(val, out, 0, 0)?;
}
out.write_char('}')
} else {
for (i, (key, val)) in obj.iter().enumerate() {
if i > 0 {
out.write_char('\n')?;
write_indent(out, current_indent)?;
}
stream_yaml_string(out, key)?;
out.write_str(":")?;
if matches!(val, OwnedValue::Array(_) | OwnedValue::Object(_))
&& !is_empty_container(val)
{
out.write_char('\n')?;
write_indent(out, current_indent + indent_spaces)?;
stream_owned_value_yaml(
val,
out,
current_indent + indent_spaces,
indent_spaces,
)?;
} else {
out.write_char(' ')?;
stream_owned_value_yaml(
val,
out,
current_indent + indent_spaces,
indent_spaces,
)?;
}
}
Ok(())
}
}
}
}
fn is_empty_container(value: &OwnedValue) -> bool {
match value {
OwnedValue::Array(arr) => arr.is_empty(),
OwnedValue::Object(obj) => obj.is_empty(),
_ => false,
}
}
fn write_indent<W: core::fmt::Write>(out: &mut W, spaces: usize) -> core::fmt::Result {
for _ in 0..spaces {
out.write_char(' ')?;
}
Ok(())
}
pub fn stream_yaml_string<W: core::fmt::Write>(out: &mut W, s: &str) -> core::fmt::Result {
if s.is_empty() {
return out.write_str("''");
}
if needs_yaml_quoting(s) {
stream_yaml_double_quoted(out, s)
} else {
out.write_str(s)
}
}
fn needs_yaml_quoting(s: &str) -> bool {
if s.is_empty() {
return true;
}
let bytes = s.as_bytes();
let first = bytes[0];
if matches!(
first,
b'-' | b'?'
| b':'
| b','
| b'['
| b']'
| b'{'
| b'}'
| b'#'
| b'&'
| b'*'
| b'!'
| b'|'
| b'>'
| b'\''
| b'"'
| b'%'
| b'@'
| b'`'
) {
return true;
}
if bytes[0] == b' ' || bytes[bytes.len() - 1] == b' ' {
return true;
}
let lower = s.to_lowercase();
if matches!(
lower.as_str(),
"null" | "~" | "true" | "false" | "yes" | "no" | "on" | "off" | ".inf" | "-.inf" | ".nan"
) {
return true;
}
if looks_like_number(s) {
return true;
}
for b in bytes {
if *b < 0x20 || *b == b':' || *b == b'#' {
return true;
}
}
false
}
fn looks_like_number(s: &str) -> bool {
if s.is_empty() {
return false;
}
let bytes = s.as_bytes();
let mut i = 0;
if bytes[i] == b'-' || bytes[i] == b'+' {
i += 1;
if i >= bytes.len() {
return false;
}
}
if !bytes[i].is_ascii_digit() {
return false;
}
let mut has_dot = false;
let mut has_exp = false;
while i < bytes.len() {
match bytes[i] {
b'0'..=b'9' => {}
b'.' if !has_dot && !has_exp => has_dot = true,
b'e' | b'E' if !has_exp => {
has_exp = true;
if i + 1 < bytes.len() && (bytes[i + 1] == b'-' || bytes[i + 1] == b'+') {
i += 1;
}
}
_ => return false,
}
i += 1;
}
true
}
fn stream_yaml_double_quoted<W: core::fmt::Write>(out: &mut W, s: &str) -> core::fmt::Result {
out.write_char('"')?;
for ch in s.chars() {
match ch {
'"' => out.write_str("\\\"")?,
'\\' => out.write_str("\\\\")?,
'\n' => out.write_str("\\n")?,
'\r' => out.write_str("\\r")?,
'\t' => out.write_str("\\t")?,
c if c < ' ' => {
let b = c as u8;
out.write_str("\\x")?;
const HEX: &[u8; 16] = b"0123456789abcdef";
out.write_char(HEX[(b >> 4) as usize] as char)?;
out.write_char(HEX[(b & 0xf) as usize] as char)?;
}
c => out.write_char(c)?,
}
}
out.write_char('"')
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
#[test]
fn test_stream_null() {
let mut buf = String::new();
OwnedValue::Null.stream_json(&mut buf).unwrap();
assert_eq!(buf, "null");
}
#[test]
fn test_stream_bool() {
let mut buf = String::new();
OwnedValue::Bool(true).stream_json(&mut buf).unwrap();
assert_eq!(buf, "true");
buf.clear();
OwnedValue::Bool(false).stream_json(&mut buf).unwrap();
assert_eq!(buf, "false");
}
#[test]
fn test_stream_int() {
let mut buf = String::new();
OwnedValue::Int(42).stream_json(&mut buf).unwrap();
assert_eq!(buf, "42");
buf.clear();
OwnedValue::Int(-123).stream_json(&mut buf).unwrap();
assert_eq!(buf, "-123");
}
#[test]
fn test_stream_float() {
let mut buf = String::new();
OwnedValue::Float(3.125).stream_json(&mut buf).unwrap();
assert_eq!(buf, "3.125");
}
#[test]
fn test_stream_string() {
let mut buf = String::new();
OwnedValue::String("hello".to_string())
.stream_json(&mut buf)
.unwrap();
assert_eq!(buf, "\"hello\"");
}
#[test]
fn test_stream_string_escaping() {
let mut buf = String::new();
OwnedValue::String("hello\nworld".to_string())
.stream_json(&mut buf)
.unwrap();
assert_eq!(buf, "\"hello\\nworld\"");
buf.clear();
OwnedValue::String("tab\there".to_string())
.stream_json(&mut buf)
.unwrap();
assert_eq!(buf, "\"tab\\there\"");
buf.clear();
OwnedValue::String("quote\"here".to_string())
.stream_json(&mut buf)
.unwrap();
assert_eq!(buf, "\"quote\\\"here\"");
}
#[test]
fn test_stream_array() {
let mut buf = String::new();
OwnedValue::Array(vec![
OwnedValue::Int(1),
OwnedValue::Int(2),
OwnedValue::Int(3),
])
.stream_json(&mut buf)
.unwrap();
assert_eq!(buf, "[1,2,3]");
}
#[test]
fn test_stream_object() {
let mut buf = String::new();
let mut map = IndexMap::new();
map.insert("name".to_string(), OwnedValue::String("Alice".to_string()));
map.insert("age".to_string(), OwnedValue::Int(30));
OwnedValue::Object(map).stream_json(&mut buf).unwrap();
assert_eq!(buf, "{\"name\":\"Alice\",\"age\":30}");
}
#[test]
fn test_is_falsy() {
assert!(OwnedValue::Null.is_falsy());
assert!(OwnedValue::Bool(false).is_falsy());
assert!(!OwnedValue::Bool(true).is_falsy());
assert!(!OwnedValue::Int(0).is_falsy());
assert!(!OwnedValue::String("".to_string()).is_falsy());
}
}