use crate::error::{Error, Result};
use crate::value::{ObjectMap, Value};
pub fn emit_canonical(value: &Value) -> Result<String> {
super::representable::check_representable(value)?;
let mut out = String::with_capacity(estimate_size(value));
match value {
Value::Object(o) => emit_object_pairs(o, 0, true, &mut out)?,
Value::Array(items) if items.is_empty() => {
out.push_str("[]\n");
}
Value::Array(items) => emit_array_root(items, &mut out)?,
_ => return Err(Error::Unrepresentable(crate::error::ReasonCode::ScalarRoot)),
}
Ok(out)
}
fn emit_object_pairs(
obj: &ObjectMap,
indent: usize,
is_root: bool,
out: &mut String,
) -> Result<()> {
for (index, (k, v)) in obj.iter().enumerate() {
emit_pair(k, v, indent, is_root && index == 0, out)?;
}
Ok(())
}
fn emit_array_root(items: &[Value], out: &mut String) -> Result<()> {
let needs_wrap = !items.is_empty() && crate::render::helpers::first_item_needs_wrap(&items[0]);
if needs_wrap {
out.push_str("[\n");
for item in items {
emit_array_item(item, 1, false, out)?;
}
out.push_str("]\n");
} else {
for (index, item) in items.iter().enumerate() {
emit_array_item(item, 0, index == 0, out)?;
}
}
Ok(())
}
fn emit_pair(
key: &str,
value: &Value,
indent: usize,
root_first_key: bool,
out: &mut String,
) -> Result<()> {
push_indent(out, indent);
crate::render::helpers::push_escaped_key_segment(key, root_first_key, out);
match value {
Value::Null => {
out.push_str(": null\n");
}
Value::Bool(b) => {
out.push_str(": ");
out.push_str(if *b { "true" } else { "false" });
out.push('\n');
}
Value::Integer(s) => {
out.push_str(": ");
out.push_str(s);
out.push('\n');
}
Value::Float(s) => {
out.push_str(": ");
out.push_str(&canonical_float(s));
out.push('\n');
}
Value::String(s) => {
emit_string_in_pair(s, indent, out)?;
}
Value::Array(items) => {
if items.is_empty() {
out.push_str(": []\n");
} else {
out.push_str(": [\n");
for item in items {
emit_array_item(item, indent + 1, false, out)?;
}
push_indent(out, indent);
out.push_str("]\n");
}
}
Value::Object(obj) => {
if obj.is_empty() {
out.push_str(": {}\n");
} else {
out.push_str(": {\n");
emit_object_pairs(obj, indent + 1, false, out)?;
push_indent(out, indent);
out.push_str("}\n");
}
}
}
Ok(())
}
fn emit_array_item(
value: &Value,
indent: usize,
is_root_array_first: bool,
out: &mut String,
) -> Result<()> {
push_indent(out, indent);
match value {
Value::Null => {
out.push_str("null\n");
}
Value::Bool(b) => {
out.push_str(if *b { "true" } else { "false" });
out.push('\n');
}
Value::Integer(s) => {
out.push_str(s);
out.push('\n');
}
Value::Float(s) => {
out.push_str(&canonical_float(s));
out.push('\n');
}
Value::String(s) => {
emit_string_as_item(s, indent, is_root_array_first, out)?;
}
Value::Array(items) => {
if items.is_empty() {
out.push_str("[]\n");
} else {
out.push_str("[\n");
for item in items {
emit_array_item(item, indent + 1, false, out)?;
}
push_indent(out, indent);
out.push_str("]\n");
}
}
Value::Object(obj) => {
if obj.is_empty() {
out.push_str("{}\n");
} else {
out.push_str("{\n");
emit_object_pairs(obj, indent + 1, false, out)?;
push_indent(out, indent);
out.push_str("}\n");
}
}
}
Ok(())
}
fn emit_string_in_pair(s: &str, indent: usize, out: &mut String) -> Result<()> {
if s.is_empty() {
out.push_str(":\n");
return Ok(());
}
if s.contains('\r') {
return Err(crate::render::helpers::cr_error());
}
if crate::render::helpers::string_needs_multiline(s) {
return emit_multiline_string(s, indent, true, out);
}
if needs_raw_marker(s) {
out.push_str(":: ");
out.push_str(s);
out.push('\n');
} else {
out.push_str(": ");
out.push_str(s);
out.push('\n');
}
Ok(())
}
fn emit_string_as_item(
s: &str,
indent: usize,
is_root_array_first: bool,
out: &mut String,
) -> Result<()> {
if s.is_empty() {
out.push_str("::\n");
return Ok(());
}
if s.contains('\r') {
return Err(crate::render::helpers::cr_error());
}
if crate::render::helpers::string_needs_multiline(s) {
return emit_multiline_string(s, indent, false, out);
}
if crate::render::helpers::item_needs_raw_marker(s)
|| (is_root_array_first
&& (crate::render::helpers::bare_item_is_pair_candidate(s)
|| s.starts_with('\u{FEFF}')))
{
out.push_str(":: ");
out.push_str(s);
out.push('\n');
} else {
out.push_str(s);
out.push('\n');
}
Ok(())
}
fn emit_multiline_string(s: &str, indent: usize, is_pair: bool, out: &mut String) -> Result<()> {
match crate::render::helpers::choose_multiline_form(s, false)? {
crate::render::helpers::MultilineForm::Verbatim => {
emit_multiline_verbatim(s, indent, is_pair, out);
}
crate::render::helpers::MultilineForm::Stripped => {
emit_multiline_stripped(s, indent, is_pair, out);
}
}
Ok(())
}
fn emit_multiline_verbatim(s: &str, indent: usize, is_pair: bool, out: &mut String) {
if is_pair {
out.push_str(": ((\n");
} else {
out.push_str("((\n");
}
out.push_str(s);
out.push('\n');
push_indent(out, indent);
out.push_str("))\n");
}
fn emit_multiline_stripped(s: &str, indent: usize, is_pair: bool, out: &mut String) {
if is_pair {
out.push_str(": (\n");
} else {
out.push_str("(\n");
}
out.push_str(s);
out.push('\n');
push_indent(out, indent);
out.push_str(")\n");
}
pub(crate) fn canonical_float(s: &str) -> std::borrow::Cow<'_, str> {
let val: f64 = match s.parse() {
Ok(v) => v,
Err(_) => return std::borrow::Cow::Borrowed(s), };
if val == 0.0 {
return std::borrow::Cow::Borrowed(s);
}
let abs = val.abs();
if !(1e-2..1e7).contains(&abs) {
let raw = format!("{:e}", val); std::borrow::Cow::Owned(normalise_scientific(&raw))
} else {
std::borrow::Cow::Borrowed(s)
}
}
fn normalise_scientific(raw: &str) -> String {
let e_pos = raw.find('e').unwrap_or(raw.len());
let mantissa = &raw[..e_pos];
let exp_part = &raw[e_pos + 1..];
let mantissa = if mantissa.contains('.') {
let trimmed = mantissa.trim_end_matches('0');
trimmed.trim_end_matches('.')
} else {
mantissa
};
let exp_str = exp_part.trim_start_matches('+');
format!("{}e{}", mantissa, exp_str)
}
fn needs_raw_marker(body: &str) -> bool {
crate::render::helpers::needs_raw_marker(body)
}
const INDENT: &str = " ";
fn push_indent(out: &mut String, level: usize) {
const SPACES: &str = " "; let mut remaining = level * INDENT.len();
if remaining == 0 {
return;
}
out.reserve(remaining);
while remaining > 0 {
let chunk = remaining.min(SPACES.len());
out.push_str(&SPACES[..chunk]);
remaining -= chunk;
}
}
fn estimate_size(value: &Value) -> usize {
match value {
Value::Null => 5,
Value::Bool(_) => 6,
Value::Integer(s) | Value::Float(s) | Value::String(s) => s.len() + 8,
Value::Array(items) => 4 + items.iter().map(estimate_size).sum::<usize>(),
Value::Object(obj) => obj
.iter()
.map(|(k, v)| k.len() + 4 + estimate_size(v))
.sum::<usize>()
.saturating_add(4),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::ObjectMap;
use compact_str::CompactString;
use indexmap::IndexMap;
use rustc_hash::FxBuildHasher;
fn obj(pairs: Vec<(&str, Value)>) -> Value {
let mut map: ObjectMap = IndexMap::with_capacity_and_hasher(pairs.len(), FxBuildHasher);
for (k, v) in pairs {
map.insert(CompactString::new(k), v);
}
Value::Object(map)
}
fn arr(items: Vec<Value>) -> Value {
Value::Array(items)
}
fn int(n: i64) -> Value {
let mut buf = itoa::Buffer::new();
Value::Integer(CompactString::new(buf.format(n)))
}
fn float(f: f64) -> Value {
let mut buf = ryu::Buffer::new();
Value::Float(CompactString::new(buf.format(f)))
}
fn s(text: &str) -> Value {
Value::String(CompactString::new(text))
}
#[test]
fn empty_object_root_produces_zero_bytes() {
let v = obj(vec![]);
assert_eq!(emit_canonical(&v).unwrap(), "");
}
#[test]
fn empty_array_root_produces_brackets() {
let v = arr(vec![]);
assert_eq!(emit_canonical(&v).unwrap(), "[]\n");
}
#[test]
fn simple_pairs() {
let v = obj(vec![
("host", s("localhost")),
("port", int(8080)),
("debug", Value::Bool(true)),
]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "host: localhost\nport: 8080\ndebug: true\n");
}
#[test]
fn null_and_false_keywords() {
let v = obj(vec![
("maintenance", Value::Null),
("enabled", Value::Bool(false)),
]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "maintenance: null\nenabled: false\n");
}
#[test]
fn float_values() {
let v = obj(vec![("ratio", float(0.5)), ("sci", float(1.5e-3))]);
let out = emit_canonical(&v).unwrap();
assert!(out.contains("ratio: 0.5\n"), "got: {out}");
assert!(
out.contains("sci: 1.5e-3\n") || out.contains("sci: 0.0015\n"),
"got: {out}"
);
}
#[test]
fn canonical_float_zero_forms_pass_through() {
assert_eq!(canonical_float("0.0"), "0.0");
assert_eq!(canonical_float("-0.0"), "-0.0");
}
#[test]
fn canonical_zero_emits_decimal_with_sign() {
let v = obj(vec![("z", float(0.0)), ("nz", float(-0.0))]);
assert_eq!(emit_canonical(&v).unwrap(), "z: 0.0\nnz: -0.0\n");
}
#[test]
fn canonical_min_positive_scale_magnitudes() {
let v = obj(vec![
("k", float(f64::MIN_POSITIVE)),
("mn", float(f64::from_bits(1))),
("mnn", float(-f64::from_bits(1))),
]);
assert_eq!(
emit_canonical(&v).unwrap(),
"k: 2.2250738585072014e-308\nmn: 5e-324\nmnn: -5e-324\n"
);
}
#[test]
fn empty_string_pair() {
let v = obj(vec![("note", s(""))]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "note:\n");
}
#[test]
fn raw_marker_for_keywords() {
let v = obj(vec![("a", s("true")), ("b", s("null")), ("c", s("false"))]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "a:: true\nb:: null\nc:: false\n");
}
#[test]
fn raw_marker_for_numbers() {
let v = obj(vec![("a", s("42")), ("b", s("0.5")), ("c", s("0xFF"))]);
let out = emit_canonical(&v).unwrap();
assert!(out.contains("a:: 42\n"));
assert!(out.contains("b:: 0.5\n"));
assert!(out.contains("c:: 0xFF\n"));
}
#[test]
fn raw_marker_for_inline_opener() {
let v = obj(vec![("a", s("{hello}"))]);
let out = emit_canonical(&v).unwrap();
assert!(out.contains("a:: {hello}\n"));
}
#[test]
fn nested_object() {
let v = obj(vec![(
"server",
obj(vec![("host", s("localhost")), ("port", int(8080))]),
)]);
let out = emit_canonical(&v).unwrap();
let expected = "server: {\n host: localhost\n port: 8080\n}\n";
assert_eq!(out, expected);
}
#[test]
fn nested_array() {
let v = obj(vec![("tags", arr(vec![s("a"), s("b")]))]);
let out = emit_canonical(&v).unwrap();
let expected = "tags: [\n a\n b\n]\n";
assert_eq!(out, expected);
}
#[test]
fn array_root_bare_items() {
let v = arr(vec![s("foo"), s("bar"), s("baz")]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "foo\nbar\nbaz\n");
}
#[test]
fn array_root_wraps_when_first_item_is_compound() {
let v = arr(vec![arr(vec![s("a"), s("b")]), arr(vec![s("c"), s("d")])]);
let out = emit_canonical(&v).unwrap();
let expected =
"[\n [\n a\n b\n ]\n [\n c\n d\n ]\n]\n";
assert_eq!(out, expected);
}
#[test]
fn array_root_does_not_wrap_for_scalars() {
let v = arr(vec![int(1), int(2), int(3)]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "1\n2\n3\n");
}
#[test]
fn cr_in_string_is_error() {
let v = obj(vec![("x", s("hello\rworld"))]);
assert!(emit_canonical(&v).is_err());
}
#[test]
fn verbatim_multiline_string() {
let v = obj(vec![("msg", s("line one\nline two"))]);
let out = emit_canonical(&v).unwrap();
assert_eq!(
out,
"msg: ((\n\
line one\n\
line two\n\
))\n"
);
}
#[test]
fn verbatim_multiline_in_array_item() {
let v = arr(vec![s("line one\nline two"), s("end")]);
let out = emit_canonical(&v).unwrap();
assert_eq!(
out,
"((\n\
line one\n\
line two\n\
))\n\
end\n"
);
}
#[test]
fn empty_string_array_item() {
let v = arr(vec![s(""), s("ok")]);
let out = emit_canonical(&v).unwrap();
assert_eq!(out, "::\nok\n");
}
#[test]
fn raw_marker_for_paren_tokens() {
let v = obj(vec![
("a", s("(")),
("b", s("((")),
("c", s("()")),
("d", s("(())")),
]);
let out = emit_canonical(&v).unwrap();
assert!(out.contains("a:: (\n"));
assert!(out.contains("b:: ((\n"));
assert!(out.contains("c:: ()\n"));
assert!(out.contains("d:: (())\n"));
}
#[test]
fn mixed_heterogeneous_array() {
let v = obj(vec![(
"mixed",
arr(vec![
s("plain_string"),
int(42),
Value::Bool(true),
Value::Null,
s("true"), obj(vec![("nested_obj", s("inside"))]),
arr(vec![s("nested_array")]),
]),
)]);
let out = emit_canonical(&v).unwrap();
let expected = "\
mixed: [
plain_string
42
true
null
:: true
{
nested_obj: inside
}
[
nested_array
]
]
";
assert_eq!(out, expected);
}
#[test]
fn integer_canonical_negative() {
let v = obj(vec![("x", int(-1)), ("y", int(-42))]);
let out = emit_canonical(&v).unwrap();
assert!(out.contains("x: -1\n"));
assert!(out.contains("y: -42\n"));
}
#[test]
fn integer_canonical_zero() {
let v = obj(vec![("z", int(0))]);
let out = emit_canonical(&v).unwrap();
assert!(out.contains("z: 0\n"));
}
#[test]
fn needs_raw_marker_integer_forms() {
assert!(needs_raw_marker("42"));
assert!(needs_raw_marker("-1"));
assert!(needs_raw_marker("+7"));
assert!(needs_raw_marker("0xFF"));
assert!(needs_raw_marker("0o755"));
assert!(needs_raw_marker("0b1111_0000"));
assert!(needs_raw_marker("1_000_000"));
assert!(!needs_raw_marker("hello"));
assert!(!needs_raw_marker("42abc"));
}
#[test]
fn needs_raw_marker_float_forms() {
assert!(needs_raw_marker("0.5"));
assert!(needs_raw_marker("1.5e-3"));
assert!(needs_raw_marker("1e9"));
assert!(!needs_raw_marker("1."));
assert!(!needs_raw_marker(".5"));
}
#[test]
fn structural_bytes_force_quoted() {
for key in ["a.b", "a:b", "a,b", "a{b", "a}b", "a[b", "a]b", "a(b"] {
let v = obj(vec![(key, s("v"))]);
let expected = format!("\"{}\": v\n", key);
assert_eq!(emit_canonical(&v).unwrap(), expected, "key: {key}");
}
}
#[test]
fn literal_backslash_stays_bare() {
let v = obj(vec![("path\\to", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "path\\\\to: v\n");
}
#[test]
fn leading_quote_forces_quoted() {
let v = obj(vec![("\"port\"", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "\"\\\"port\\\"\": v\n");
}
#[test]
fn leading_double_hash_forces_quoted() {
let v = obj(vec![("##a:b", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "\"##a:b\": v\n");
let v = obj(vec![("##tag", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "\"##tag\": v\n");
}
#[test]
fn interior_newline_and_cr_stay_bare() {
let v = obj(vec![("a\nb", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\\nb: v\n");
let v = obj(vec![("a\rb", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\\rb: v\n");
}
#[test]
fn edge_newline_and_cr_stay_bare() {
for key in ["\nlf", "lf\n", "\rcr", "cr\r"] {
let v = obj(vec![(key, s("v"))]);
let expected = format!("{}: v\n", key.replace('\n', "\\n").replace('\r', "\\r"));
assert_eq!(emit_canonical(&v).unwrap(), expected, "key: {key:?}");
}
}
#[test]
fn interior_tab_stays_bare_raw() {
let v = obj(vec![("a\tb", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\tb: v\n");
}
#[test]
fn control_bytes_use_uppercase_unicode_escape() {
let v = obj(vec![("a\u{1}b", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\\u0001b: v\n");
let v = obj(vec![("a\u{0}b", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\\u0000b: v\n");
let v = obj(vec![("a\u{7F}b", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\\u007Fb: v\n");
}
#[test]
fn edge_whitespace_forces_quoted() {
for key in ["a ", " a", " "] {
let v = obj(vec![(key, s("v"))]);
let expected = format!("\"{}\": v\n", key);
assert_eq!(emit_canonical(&v).unwrap(), expected, "key: {key:?}");
}
let v = obj(vec![("\tx", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "\"\tx\": v\n");
}
#[test]
fn bom_root_first_key_quoted_elsewhere_bare() {
let v = obj(vec![("\u{FEFF}host", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "\"\u{FEFF}host\": v\n");
let v = obj(vec![("ok", s("v")), ("\u{FEFF}host", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "ok: v\n\u{FEFF}host: v\n");
let v = obj(vec![("outer", obj(vec![("\u{FEFF}host", s("v"))]))]);
assert_eq!(
emit_canonical(&v).unwrap(),
"outer: {\n \u{FEFF}host: v\n}\n"
);
let v = obj(vec![("a\u{FEFF}host", s("v"))]);
assert_eq!(emit_canonical(&v).unwrap(), "a\u{FEFF}host: v\n");
}
#[test]
fn root_first_key_with_structural_byte_still_quoted_among_pairs() {
let v = obj(vec![("a.b", s("v")), ("c", s("w"))]);
assert_eq!(emit_canonical(&v).unwrap(), "\"a.b\": v\nc: w\n");
}
#[test]
fn array_root_first_item_pair_candidate_takes_raw_marker() {
let v = arr(vec![s("\"tis the season\": fa")]);
let text = emit_canonical(&v).unwrap();
assert_eq!(text, ":: \"tis the season\": fa\n");
let back = crate::parse(&text).unwrap();
assert_eq!(back, v);
}
#[test]
fn array_root_first_item_unterminated_quote_stays_bare() {
let v = arr(vec![s("\"tis the season: fa")]);
assert_eq!(emit_canonical(&v).unwrap(), "\"tis the season: fa\n");
let v = arr(vec![s("'tis the season: fa")]);
assert_eq!(emit_canonical(&v).unwrap(), "'tis the season: fa\n");
}
#[test]
fn array_root_first_item_glued_colon_stays_bare() {
assert_eq!(emit_canonical(&arr(vec![s("a:b")])).unwrap(), "a:b\n");
assert_eq!(emit_canonical(&arr(vec![s("a::b")])).unwrap(), ":: a::b\n");
assert_eq!(emit_canonical(&arr(vec![s("a: b")])).unwrap(), ":: a: b\n");
}
#[test]
fn array_root_second_item_not_guarded() {
let v = arr(vec![s("head"), s("a: b")]);
assert_eq!(emit_canonical(&v).unwrap(), "head\na: b\n");
}
#[test]
fn nested_array_first_item_not_guarded() {
let v = obj(vec![("arr", arr(vec![s("a: b")]))]);
assert_eq!(emit_canonical(&v).unwrap(), "arr: [\n a: b\n]\n");
}
#[test]
fn array_root_first_item_bom_takes_raw_marker() {
assert_eq!(
emit_canonical(&arr(vec![s("\u{FEFF}host")])).unwrap(),
":: \u{FEFF}host\n"
);
let v = arr(vec![s("x"), s("\u{FEFF}host")]);
assert_eq!(emit_canonical(&v).unwrap(), "x\n\u{FEFF}host\n");
let v = obj(vec![("arr", arr(vec![s("\u{FEFF}host")]))]);
assert_eq!(emit_canonical(&v).unwrap(), "arr: [\n \u{FEFF}host\n]\n");
}
#[test]
fn array_root_first_item_plain_scalar_unaffected() {
let v = arr(vec![s("plain"), s("a: b")]);
assert_eq!(emit_canonical(&v).unwrap(), "plain\na: b\n");
}
#[test]
fn array_root_wrapped_form_untouched_by_first_item_guard() {
let v = arr(vec![obj(vec![("k", s("v"))])]);
assert_eq!(
emit_canonical(&v).unwrap(),
"[\n {\n k: v\n }\n]\n"
);
}
}