use std::collections::HashSet;
use std::fmt::Write as _;
use crate::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WriteOptions {
pub exclude_values: bool,
pub unordered_arrays: bool,
pub unordered_sets: bool,
pub unordered_objects: bool,
}
pub(crate) fn write(value: &Value, options: &WriteOptions, out: &mut String) {
match value {
Value::Null => out.push_str("Null"),
Value::Bool(true) => out.push_str("bool:true"),
Value::Bool(false) => out.push_str("bool:false"),
Value::Number(number) => write_number(*number, out),
Value::String(text) => write_string(text, out),
Value::Array(items) => write_array(items, options, out),
Value::Set(items) => {
out.push_str("set:");
write_set(items, options, out);
}
Value::Map(entries) => {
out.push_str("map:");
write_map(entries, options, out);
}
Value::Object(entries) => {
out.push_str("object:");
write_object(entries, options, out);
}
}
}
fn write_number(number: f64, out: &mut String) {
out.push_str("number:");
out.push_str(&number_to_string(number));
}
fn number_to_string(number: f64) -> String {
if number.is_nan() {
return "NaN".to_string();
}
if number == f64::INFINITY {
return "Infinity".to_string();
}
if number == f64::NEG_INFINITY {
return "-Infinity".to_string();
}
if number == 0.0 {
return "0".to_string();
}
let sign = if number < 0.0 { "-" } else { "" };
let scientific = format!("{:e}", number.abs());
let (mantissa, exponent) = scientific.split_once('e').expect("scientific form");
let exponent: i32 = exponent.parse().expect("exponent");
let mut digits: String = mantissa.chars().filter(|c| *c != '.').collect();
while digits.len() > 1 && digits.ends_with('0') {
digits.pop();
}
let digits = digits.as_str();
let count = digits.len() as i32;
let point = exponent + 1;
if count <= point && point <= 21 {
let mut text = String::from(digits);
text.extend(std::iter::repeat_n('0', (point - count) as usize));
format!("{sign}{text}")
} else if 0 < point && point <= 21 {
let split = point as usize;
format!("{sign}{}.{}", &digits[..split], &digits[split..])
} else if -6 < point && point <= 0 {
let mut text = String::from("0.");
text.extend(std::iter::repeat_n('0', (-point) as usize));
text.push_str(digits);
format!("{sign}{text}")
} else if count == 1 {
format!(
"{sign}{digits}e{}{}",
exponent_sign(point - 1),
(point - 1).abs()
)
} else {
format!(
"{sign}{}.{}e{}{}",
&digits[..1],
&digits[1..],
exponent_sign(point - 1),
(point - 1).abs()
)
}
}
fn exponent_sign(exponent: i32) -> char {
if exponent < 0 { '-' } else { '+' }
}
fn write_string(text: &str, out: &mut String) {
let length: usize = text.chars().map(|c| c.len_utf16()).sum();
let _ = write!(out, "string:{length}:");
out.push_str(text);
}
fn write_array(items: &[Value], options: &WriteOptions, out: &mut String) {
let _ = write!(out, "array:{}:", items.len());
if !options.unordered_arrays || items.len() <= 1 {
for item in items {
write(item, options, out);
}
return;
}
let mut sorted: Vec<String> = items.iter().map(|item| canonical(item, options)).collect();
sorted.sort_by(|a, b| utf16_cmp(a, b));
let _ = write!(out, "array:{}:", sorted.len());
for entry in sorted {
write_string(&entry, out);
}
}
fn utf16_cmp(a: &str, b: &str) -> std::cmp::Ordering {
a.encode_utf16().cmp(b.encode_utf16())
}
fn write_unordered(entries: Vec<String>, sorted: bool, out: &mut String) {
let count = entries.len();
let _ = write!(out, "array:{count}:");
if !sorted || count <= 1 {
for entry in entries {
out.push_str(&entry);
}
return;
}
let mut entries = entries;
entries.sort_by(|a, b| utf16_cmp(a, b));
let _ = write!(out, "array:{count}:");
for entry in entries {
write_string(&entry, out);
}
}
fn write_set(items: &[Value], options: &WriteOptions, out: &mut String) {
let mut seen = HashSet::new();
let entries = items
.iter()
.filter_map(|item| {
if item.is_scalar() {
let written = canonical(item, options);
if !seen.insert(written.clone()) {
return None;
}
Some(written)
} else {
Some(canonical(item, options))
}
})
.collect();
write_unordered(entries, options.unordered_sets, out);
}
fn write_map(entries: &[(String, Value)], options: &WriteOptions, out: &mut String) {
let serialized = serialized_entries(entries, options);
write_unordered(serialized, options.unordered_sets, out);
}
fn write_object(entries: &[(String, Value)], options: &WriteOptions, out: &mut String) {
let mut keys: Vec<&str> = entries.iter().map(|(key, _)| key.as_str()).collect();
if options.unordered_objects {
keys.sort_by(|a, b| utf16_cmp(a, b));
}
let mut kept: Vec<&str> = Vec::with_capacity(keys.len());
for key in keys {
if !kept.contains(&key) {
kept.push(key);
}
}
let _ = write!(out, "{}:", kept.len());
for key in kept {
write_string(key, out);
out.push(':');
if !options.exclude_values {
let value = entries
.iter()
.rev()
.find(|(candidate, _)| candidate == key)
.map(|(_, value)| value)
.expect("key came from the entries");
write(value, options, out);
}
out.push(',');
}
}
fn serialized_entries(entries: &[(String, Value)], options: &WriteOptions) -> Vec<String> {
let mut kept: Vec<(&String, &Value)> = Vec::with_capacity(entries.len());
for (key, value) in entries {
match kept.iter_mut().find(|(existing, _)| *existing == key) {
Some(existing) => existing.1 = value,
None => kept.push((key, value)),
}
}
kept.iter()
.map(|(key, value)| {
let mut entry = String::from("array:2:");
write_string(key, &mut entry);
write(value, options, &mut entry);
entry
})
.collect()
}
pub(crate) fn canonical(value: &Value, options: &WriteOptions) -> String {
let mut out = String::new();
write(value, options, &mut out);
out
}
#[cfg(test)]
mod tests {
use super::*;
const DEFAULT: WriteOptions = WriteOptions {
exclude_values: false,
unordered_arrays: false,
unordered_sets: true,
unordered_objects: true,
};
#[test]
fn a_null_is_the_word() {
assert_eq!(canonical(&Value::Null, &DEFAULT), "Null");
}
#[test]
fn booleans_are_labelled() {
assert_eq!(canonical(&Value::Bool(true), &DEFAULT), "bool:true");
assert_eq!(canonical(&Value::Bool(false), &DEFAULT), "bool:false");
}
#[test]
fn integers_and_equal_floats_are_the_same() {
assert_eq!(canonical(&Value::Number(1.0), &DEFAULT), "number:1");
assert_eq!(canonical(&Value::Number(100.0), &DEFAULT), "number:100");
assert_eq!(canonical(&Value::Number(-1.0), &DEFAULT), "number:-1");
}
#[test]
fn negative_zero_is_written_as_zero() {
assert_eq!(canonical(&Value::Number(-0.0), &DEFAULT), "number:0");
}
#[test]
fn the_special_numbers_have_names() {
assert_eq!(canonical(&Value::Number(f64::NAN), &DEFAULT), "number:NaN");
assert_eq!(
canonical(&Value::Number(f64::INFINITY), &DEFAULT),
"number:Infinity"
);
assert_eq!(
canonical(&Value::Number(f64::NEG_INFINITY), &DEFAULT),
"number:-Infinity"
);
}
#[test]
fn a_string_carries_its_utf16_length() {
assert_eq!(
canonical(&Value::String(String::new()), &DEFAULT),
"string:0:"
);
assert_eq!(
canonical(&Value::String("hello".into()), &DEFAULT),
"string:5:hello"
);
assert_eq!(
canonical(&Value::String("café".into()), &DEFAULT),
"string:4:café"
);
assert_eq!(
canonical(&Value::String("䏿–‡".into()), &DEFAULT),
"string:2:䏿–‡"
);
assert_eq!(
canonical(&Value::String("😀".into()), &DEFAULT),
"string:2:😀"
);
}
#[test]
fn an_array_keeps_its_order() {
assert_eq!(canonical(&Value::Array(vec![]), &DEFAULT), "array:0:");
assert_eq!(
canonical(
&Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]),
&DEFAULT
),
"array:2:number:1number:2"
);
assert_eq!(
canonical(
&Value::Array(vec![Value::Number(2.0), Value::Number(1.0)]),
&DEFAULT
),
"array:2:number:2number:1"
);
}
#[test]
fn a_map_is_written_in_key_order() {
let forward = Value::Map(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::Number(2.0)),
]);
let backward = Value::Map(vec![
("b".into(), Value::Number(2.0)),
("a".into(), Value::Number(1.0)),
]);
assert_eq!(
canonical(&forward, &DEFAULT),
canonical(&backward, &DEFAULT)
);
}
#[test]
fn a_set_ignores_order_but_an_array_does_not() {
let one = Value::Number(1.0);
let two = Value::Number(2.0);
let set_forward = Value::Set(vec![one.clone(), two.clone()]);
let set_backward = Value::Set(vec![two.clone(), one.clone()]);
assert_eq!(
canonical(&set_forward, &DEFAULT),
canonical(&set_backward, &DEFAULT)
);
let array_forward = Value::Array(vec![one.clone(), two.clone()]);
let array_backward = Value::Array(vec![two, one]);
assert_ne!(
canonical(&array_forward, &DEFAULT),
canonical(&array_backward, &DEFAULT)
);
}
#[test]
fn a_set_drops_repeated_scalars_but_not_repeated_arrays() {
assert_eq!(
canonical(
&Value::Set(vec![Value::Number(1.0), Value::Number(1.0)]),
&DEFAULT
),
"set:array:1:number:1"
);
assert_eq!(
canonical(
&Value::Set(vec![Value::Number(f64::NAN), Value::Number(f64::NAN)]),
&DEFAULT
),
"set:array:1:number:NaN"
);
let array = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
let two_arrays = Value::Set(vec![array.clone(), array]);
assert!(canonical(&two_arrays, &DEFAULT).starts_with("set:array:2:"));
}
#[test]
fn entries_are_ordered_by_utf16_code_unit() {
let emoji_first = Value::Set(vec![
Value::String("\u{ffff}z".into()),
Value::String("\u{1f600}".into()),
]);
let emoji_last = Value::Set(vec![
Value::String("\u{1f600}".into()),
Value::String("\u{ffff}z".into()),
]);
assert_eq!(
canonical(&emoji_first, &DEFAULT),
canonical(&emoji_last, &DEFAULT)
);
let written = canonical(&emoji_first, &DEFAULT);
let emoji_at = written.find('\u{1f600}').expect("emoji present");
let max_at = written.find('\u{ffff}').expect("U+FFFF present");
assert!(
emoji_at < max_at,
"emoji should sort before U+FFFF: {written}"
);
}
#[test]
fn a_repeated_map_key_keeps_the_last_value() {
let repeated = Value::Map(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::Number(9.0)),
("a".into(), Value::Number(2.0)),
]);
let resolved = Value::Map(vec![
("a".into(), Value::Number(2.0)),
("b".into(), Value::Number(9.0)),
]);
assert_eq!(
canonical(&repeated, &DEFAULT),
canonical(&resolved, &DEFAULT)
);
}
#[test]
fn a_map_key_cannot_be_confused_with_a_value() {
let first = Value::Map(vec![("ab".into(), Value::Number(1.0))]);
let second = Value::Map(vec![("a".into(), Value::String("b1".into()))]);
assert_ne!(canonical(&first, &DEFAULT), canonical(&second, &DEFAULT));
}
#[test]
fn an_object_is_not_a_map() {
let object = Value::Object(vec![("a".into(), Value::Number(1.0))]);
let map = Value::Map(vec![("a".into(), Value::Number(1.0))]);
assert_eq!(
canonical(&object, &DEFAULT),
"object:1:string:1:a:number:1,"
);
assert_eq!(
canonical(&map, &DEFAULT),
"map:array:1:array:2:string:1:anumber:1"
);
assert_ne!(canonical(&object, &DEFAULT), canonical(&map, &DEFAULT));
}
#[test]
fn an_object_is_written_in_key_order() {
let forward = Value::Object(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::Number(2.0)),
]);
let backward = Value::Object(vec![
("b".into(), Value::Number(2.0)),
("a".into(), Value::Number(1.0)),
]);
assert_eq!(
canonical(&forward, &DEFAULT),
canonical(&backward, &DEFAULT)
);
}
#[test]
fn an_object_keeps_the_last_value_for_a_repeated_key() {
let repeated = Value::Object(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::Number(9.0)),
("a".into(), Value::Number(2.0)),
]);
let resolved = Value::Object(vec![
("a".into(), Value::Number(2.0)),
("b".into(), Value::Number(9.0)),
]);
assert_eq!(
canonical(&repeated, &DEFAULT),
canonical(&resolved, &DEFAULT)
);
}
#[test]
fn exclude_values_writes_only_the_keys_of_an_object() {
let options = WriteOptions {
exclude_values: true,
..DEFAULT
};
let object = Value::Object(vec![
("a".into(), Value::Number(1.0)),
("b".into(), Value::String("x".into())),
]);
assert_eq!(
canonical(&object, &options),
"object:2:string:1:a:,string:1:b:,"
);
let map = Value::Map(vec![("a".into(), Value::Number(1.0))]);
assert_eq!(
canonical(&map, &options),
"map:array:1:array:2:string:1:anumber:1"
);
}
#[test]
fn unordered_arrays_sorts_the_entries() {
let options = WriteOptions {
unordered_arrays: true,
..DEFAULT
};
let forward = Value::Array(vec![Value::Number(2.0), Value::Number(1.0)]);
let backward = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
assert_ne!(
canonical(&forward, &DEFAULT),
canonical(&backward, &DEFAULT)
);
assert_eq!(
canonical(&forward, &options),
canonical(&backward, &options)
);
assert_eq!(
canonical(&forward, &options),
"array:2:array:2:string:8:number:1string:8:number:2"
);
}
#[test]
fn unordered_sets_off_keeps_the_insertion_order() {
let options = WriteOptions {
unordered_sets: false,
..DEFAULT
};
let forward = Value::Set(vec![Value::Number(1.0), Value::Number(2.0)]);
let backward = Value::Set(vec![Value::Number(2.0), Value::Number(1.0)]);
assert_eq!(
canonical(&forward, &DEFAULT),
canonical(&backward, &DEFAULT)
);
assert_ne!(
canonical(&forward, &options),
canonical(&backward, &options)
);
assert_eq!(
canonical(&forward, &options),
"set:array:2:number:1number:2"
);
}
}