rojo/src/json.rs
Joe 5f91a0777a Implement new oneshot Vfs for CLI commands and enhance JSON5 serialization
- Added `new_oneshot` method to the Vfs struct, providing a backend suitable for one-time operations without file watching.
- Updated CLI commands (`fmt_project` and `syncback`) to utilize the new oneshot Vfs, improving error handling during file operations.
- Enhanced JSON5 serialization by introducing a custom serializer that directly builds a tree structure, improving performance and maintaining sorted keys.
- Improved error logging in JSON serialization to provide more context on failures.
2026-01-31 13:06:37 -06:00

4800 lines
157 KiB
Rust

//! Utilities for parsing JSON with comments (JSONC) and deserializing to Rust types.
//!
//! This module provides convenient wrappers around `jsonc_parser` and `serde_json`
//! to reduce boilerplate and improve ergonomics when working with JSONC files.
use anyhow::Context as _;
use serde::{de::DeserializeOwned, Serialize};
/// Parse JSONC text into a `serde_json::Value`.
///
/// This handles the common pattern of calling `jsonc_parser::parse_to_serde_value`
/// and unwrapping the `Option` with a clear error message.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
#[allow(dead_code)]
pub fn parse_value(text: &str) -> anyhow::Result<serde_json::Value> {
json5::from_str(text).context("Failed to parse JSON5")
}
/// Parse JSONC text into a `serde_json::Value` with a custom context message.
///
/// This is useful when you want to provide a specific error message that includes
/// additional information like the file path.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
pub fn parse_value_with_context(
text: &str,
context: impl Fn() -> String,
) -> anyhow::Result<serde_json::Value> {
json5::from_str(text).with_context(|| format!("{}: JSON5 parse error", context()))
}
/// Parse JSONC text and deserialize it into a specific type.
///
/// This combines parsing JSONC and deserializing into a single operation,
/// eliminating the need to manually chain `parse_to_serde_value` and `from_value`.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_str<T: DeserializeOwned>(text: &str) -> anyhow::Result<T> {
json5::from_str(text).context("Failed to deserialize JSON5")
}
/// Parse JSONC text and deserialize it into a specific type with a custom context message.
///
/// This is useful when you want to provide a specific error message that includes
/// additional information like the file path.
///
/// # Errors
///
/// Returns an error if:
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_str_with_context<T: DeserializeOwned>(
text: &str,
context: impl Fn() -> String,
) -> anyhow::Result<T> {
json5::from_str(text).with_context(|| format!("{}: JSON5 parse error", context()))
}
/// Parse JSONC bytes into a `serde_json::Value` with a custom context message.
///
/// This handles UTF-8 conversion and JSONC parsing in one step.
///
/// # Errors
///
/// Returns an error if:
/// - The bytes are not valid UTF-8
/// - The text is not valid JSONC
/// - The text contains no JSON value
pub fn parse_value_from_slice_with_context(
slice: &[u8],
context: impl Fn() -> String,
) -> anyhow::Result<serde_json::Value> {
let text = std::str::from_utf8(slice)
.with_context(|| format!("{}: File is not valid UTF-8", context()))?;
parse_value_with_context(text, context)
}
/// Parse JSONC bytes and deserialize it into a specific type.
///
/// This handles UTF-8 conversion, JSONC parsing, and deserialization in one step.
///
/// # Errors
///
/// Returns an error if:
/// - The bytes are not valid UTF-8
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_slice<T: DeserializeOwned>(slice: &[u8]) -> anyhow::Result<T> {
let text = std::str::from_utf8(slice).context("File is not valid UTF-8")?;
from_str(text)
}
/// Parse JSONC bytes and deserialize it into a specific type with a custom context message.
///
/// This handles UTF-8 conversion, JSONC parsing, and deserialization in one step.
///
/// # Errors
///
/// Returns an error if:
/// - The bytes are not valid UTF-8
/// - The text is not valid JSONC
/// - The text contains no JSON value
/// - The value cannot be deserialized into type `T`
pub fn from_slice_with_context<T: DeserializeOwned>(
slice: &[u8],
context: impl Fn() -> String,
) -> anyhow::Result<T> {
let text = std::str::from_utf8(slice)
.with_context(|| format!("{}: File is not valid UTF-8", context()))?;
from_str_with_context(text, context)
}
/// A JSON5 value that uses BTreeMap for sorted keys and supports NaN/Infinity.
/// Uses String for numbers to preserve exact representation (including scientific notation).
#[derive(Debug, Clone, PartialEq)]
enum Json5Value {
Null,
Bool(bool),
Number(String), // Store as string to preserve exact representation
String(String),
Array(Vec<Json5Value>),
Object(std::collections::BTreeMap<String, Json5Value>),
}
impl Json5Value {
/// Write this value to a string with proper JSON5 formatting and indentation.
fn write_to(&self, output: &mut String, indent: usize) {
let indent_str = " ".repeat(indent);
let inner_indent = " ".repeat(indent + 1);
match self {
Json5Value::Null => output.push_str("null"),
Json5Value::Bool(b) => output.push_str(if *b { "true" } else { "false" }),
Json5Value::Number(s) => output.push_str(s),
Json5Value::String(s) => {
write_escaped_string(output, s);
}
Json5Value::Array(arr) => {
if arr.is_empty() {
output.push_str("[]");
} else {
output.push_str("[\n");
for (i, item) in arr.iter().enumerate() {
output.push_str(&inner_indent);
item.write_to(output, indent + 1);
if i < arr.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(&indent_str);
output.push(']');
}
}
Json5Value::Object(map) => {
if map.is_empty() {
output.push_str("{}");
} else {
output.push_str("{\n");
let entries: Vec<_> = map.iter().collect();
for (i, (key, value)) in entries.iter().enumerate() {
output.push_str(&inner_indent);
// Use unquoted keys if valid identifier, otherwise quote with escaping
if is_valid_identifier(key) {
output.push_str(key);
} else {
write_escaped_string(output, key);
}
output.push_str(": ");
value.write_to(output, indent + 1);
if i < entries.len() - 1 {
output.push(',');
}
output.push('\n');
}
output.push_str(&indent_str);
output.push('}');
}
}
}
}
}
/// Write a string to output with proper JSON5 escaping (double-quoted).
///
/// Per JSON5 spec Section 5.1 (https://spec.json5.org/#escapes):
/// - Standard escapes: \" \\ \b \f \n \r \t \v
/// - Control characters as \uXXXX
///
/// Per Section 5.2, generators SHOULD escape U+2028 and U+2029.
fn write_escaped_string(output: &mut String, s: &str) {
output.push('"');
for c in s.chars() {
match c {
// Standard JSON5 escape sequences (Table 1 in spec)
'"' => output.push_str("\\\""),
'\\' => output.push_str("\\\\"),
'\u{0008}' => output.push_str("\\b"), // Backspace
'\u{000C}' => output.push_str("\\f"), // Form feed
'\n' => output.push_str("\\n"), // Line feed (U+000A)
'\r' => output.push_str("\\r"), // Carriage return (U+000D)
'\t' => output.push_str("\\t"), // Horizontal tab (U+0009)
'\u{000B}' => output.push_str("\\v"), // Vertical tab
// Per Section 5.2: generators SHOULD escape U+2028 and U+2029
'\u{2028}' => output.push_str("\\u2028"), // Line separator
'\u{2029}' => output.push_str("\\u2029"), // Paragraph separator
// Other control characters as \uXXXX
c if c.is_control() => {
output.push_str(&format!("\\u{:04x}", c as u32));
}
c => output.push(c),
}
}
output.push('"');
}
/// Check if a string is a valid unquoted JSON5 identifier
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' && first != '$' {
return false;
}
chars.all(|c| c.is_alphanumeric() || c == '_' || c == '$')
}
/// Error type for compact JSON5 serialization.
#[derive(Debug)]
struct CompactJson5Error(String);
impl std::fmt::Display for CompactJson5Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for CompactJson5Error {}
impl serde::ser::Error for CompactJson5Error {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
CompactJson5Error(msg.to_string())
}
}
/// Serialize a value to a compact JSON5 string (for HTTP transport).
///
/// Supports NaN/Infinity. No whitespace between elements.
///
/// # Errors
///
/// Returns an error if the value cannot be serialized to JSON5.
#[allow(dead_code)]
pub fn to_string_compact<T: Serialize>(value: &T) -> anyhow::Result<String> {
let mut output = String::new();
let mut serializer = CompactJson5Serializer {
output: &mut output,
};
value
.serialize(&mut serializer)
.map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(output)
}
struct CompactJson5Serializer<'a> {
output: &'a mut String,
}
impl<'a> serde::Serializer for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
type SerializeSeq = Self;
type SerializeTuple = Self;
type SerializeTupleStruct = Self;
type SerializeTupleVariant = Self;
type SerializeMap = Self;
type SerializeStruct = Self;
type SerializeStructVariant = Self;
fn serialize_bool(self, v: bool) -> Result<(), Self::Error> {
self.output.push_str(if v { "true" } else { "false" });
Ok(())
}
fn serialize_i8(self, v: i8) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_i16(self, v: i16) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_i32(self, v: i32) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_i64(self, v: i64) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_u8(self, v: u8) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_u16(self, v: u16) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_u32(self, v: u32) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_u64(self, v: u64) -> Result<(), Self::Error> {
self.output.push_str(&v.to_string());
Ok(())
}
fn serialize_f32(self, v: f32) -> Result<(), Self::Error> {
self.serialize_f64(v as f64)
}
fn serialize_f64(self, v: f64) -> Result<(), Self::Error> {
if v.is_nan() {
self.output.push_str("NaN");
} else if v.is_infinite() {
if v.is_sign_positive() {
self.output.push_str("Infinity");
} else {
self.output.push_str("-Infinity");
}
} else {
// Use scientific notation for very large or very small numbers
let abs = v.abs();
if abs != 0.0 && !(1e-6..1e15).contains(&abs) {
self.output.push_str(&format!("{:e}", v));
} else {
self.output.push_str(&v.to_string());
}
}
Ok(())
}
fn serialize_char(self, v: char) -> Result<(), Self::Error> {
self.serialize_str(&v.to_string())
}
fn serialize_str(self, v: &str) -> Result<(), Self::Error> {
self.output.push('"');
for c in v.chars() {
match c {
'"' => self.output.push_str("\\\""),
'\\' => self.output.push_str("\\\\"),
'\n' => self.output.push_str("\\n"),
'\r' => self.output.push_str("\\r"),
'\t' => self.output.push_str("\\t"),
c if c < ' ' => self.output.push_str(&format!("\\u{:04x}", c as u32)),
c => self.output.push(c),
}
}
self.output.push('"');
Ok(())
}
fn serialize_bytes(self, v: &[u8]) -> Result<(), Self::Error> {
use serde::ser::SerializeSeq;
let mut seq = self.serialize_seq(Some(v.len()))?;
for b in v {
seq.serialize_element(b)?;
}
seq.end()
}
fn serialize_none(self) -> Result<(), Self::Error> {
self.serialize_unit()
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_unit(self) -> Result<(), Self::Error> {
self.output.push_str("null");
Ok(())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<(), Self::Error> {
self.serialize_unit()
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<(), Self::Error> {
self.serialize_str(variant)
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
value: &T,
) -> Result<(), Self::Error> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<(), Self::Error> {
self.output.push('{');
self.serialize_str(variant)?;
self.output.push(':');
value.serialize(&mut *self)?;
self.output.push('}');
Ok(())
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
self.output.push('[');
Ok(self)
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
self.output.push('{');
self.serialize_str(variant)?;
self.output.push_str(":[");
Ok(self)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
self.output.push('{');
Ok(self)
}
fn serialize_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
self.serialize_map(Some(len))
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
self.output.push('{');
self.serialize_str(variant)?;
self.output.push_str(":{");
Ok(self)
}
}
impl<'a> serde::ser::SerializeSeq for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
if !self.output.ends_with('[') {
self.output.push(',');
}
value.serialize(&mut **self)
}
fn end(self) -> Result<(), Self::Error> {
self.output.push(']');
Ok(())
}
}
impl<'a> serde::ser::SerializeTuple for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::end(self)
}
}
impl<'a> serde::ser::SerializeTupleStruct for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::end(self)
}
}
impl<'a> serde::ser::SerializeTupleVariant for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<(), Self::Error> {
self.output.push_str("]}");
Ok(())
}
}
impl<'a> serde::ser::SerializeMap for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
if !self.output.ends_with('{') {
self.output.push(',');
}
key.serialize(&mut **self)
}
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.output.push(':');
value.serialize(&mut **self)
}
fn end(self) -> Result<(), Self::Error> {
self.output.push('}');
Ok(())
}
}
impl<'a> serde::ser::SerializeStruct for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
serde::ser::SerializeMap::serialize_key(self, key)?;
serde::ser::SerializeMap::serialize_value(self, value)
}
fn end(self) -> Result<(), Self::Error> {
serde::ser::SerializeMap::end(self)
}
}
impl<'a> serde::ser::SerializeStructVariant for &mut CompactJson5Serializer<'a> {
type Ok = ();
type Error = CompactJson5Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
serde::ser::SerializeStruct::serialize_field(self, key, value)
}
fn end(self) -> Result<(), Self::Error> {
self.output.push_str("}}");
Ok(())
}
}
/// Serialize a value to a JSON5 byte vector with sorted keys.
///
/// Supports NaN/Infinity and sorts object keys alphabetically.
/// Uses a custom serializer that builds a tree directly (no parsing needed).
///
/// # Errors
///
/// Returns an error if the value cannot be serialized.
pub fn to_vec_pretty_sorted<T: Serialize>(value: &T) -> anyhow::Result<Vec<u8>> {
// Serialize directly to Json5Value tree (with BTreeMap for sorted keys)
let tree = value
.serialize(Json5ValueSerializer)
.map_err(|e| anyhow::anyhow!("Failed to serialize: {}", e))?;
// Write the tree to string with pretty formatting
let mut output = String::new();
tree.write_to(&mut output, 0);
output.push('\n');
Ok(output.into_bytes())
}
/// A serde Serializer that builds a Json5Value tree directly.
/// This avoids the need to parse - we serialize directly to the intermediate representation.
struct Json5ValueSerializer;
impl serde::Serializer for Json5ValueSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
type SerializeSeq = Json5SeqSerializer;
type SerializeTuple = Json5SeqSerializer;
type SerializeTupleStruct = Json5SeqSerializer;
type SerializeTupleVariant = Json5TupleVariantSerializer;
type SerializeMap = Json5MapSerializer;
type SerializeStruct = Json5MapSerializer;
type SerializeStructVariant = Json5StructVariantSerializer;
fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Bool(v))
}
fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Number(v.to_string()))
}
fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> {
self.serialize_f64(v as f64)
}
fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> {
let s = if v.is_nan() {
"NaN".to_string()
} else if v.is_infinite() {
if v.is_sign_positive() {
"Infinity".to_string()
} else {
"-Infinity".to_string()
}
} else {
// Use scientific notation for very large or very small numbers
// to ensure they can be parsed back by json5
let abs = v.abs();
if abs != 0.0 && !(1e-6..1e15).contains(&abs) {
format!("{:e}", v)
} else {
v.to_string()
}
};
Ok(Json5Value::Number(s))
}
fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::String(v.to_string()))
}
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::String(v.to_string()))
}
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
let arr: Vec<Json5Value> = v
.iter()
.map(|b| Json5Value::Number(b.to_string()))
.collect();
Ok(Json5Value::Array(arr))
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Null)
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
value.serialize(self)
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Null)
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Null)
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::String(variant.to_string()))
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error> {
let inner = value.serialize(Json5ValueSerializer)?;
let mut map = std::collections::BTreeMap::new();
map.insert(variant.to_string(), inner);
Ok(Json5Value::Object(map))
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Ok(Json5SeqSerializer {
items: Vec::with_capacity(len.unwrap_or(0)),
})
}
fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
self.serialize_seq(Some(len))
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Ok(Json5TupleVariantSerializer {
variant: variant.to_string(),
items: Vec::with_capacity(len),
})
}
fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Ok(Json5MapSerializer {
map: std::collections::BTreeMap::new(),
next_key: None,
_capacity: len.unwrap_or(0),
})
}
fn serialize_struct(
self,
_name: &'static str,
len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
self.serialize_map(Some(len))
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Ok(Json5StructVariantSerializer {
variant: variant.to_string(),
map: std::collections::BTreeMap::new(),
_capacity: len,
})
}
}
#[derive(Debug)]
struct Json5SerError(String);
impl std::fmt::Display for Json5SerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for Json5SerError {}
impl serde::ser::Error for Json5SerError {
fn custom<T: std::fmt::Display>(msg: T) -> Self {
Json5SerError(msg.to_string())
}
}
struct Json5SeqSerializer {
items: Vec<Json5Value>,
}
impl serde::ser::SerializeSeq for Json5SeqSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.items.push(value.serialize(Json5ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Array(self.items))
}
}
impl serde::ser::SerializeTuple for Json5SeqSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
serde::ser::SerializeSeq::end(self)
}
}
impl serde::ser::SerializeTupleStruct for Json5SeqSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
serde::ser::SerializeSeq::serialize_element(self, value)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
serde::ser::SerializeSeq::end(self)
}
}
struct Json5TupleVariantSerializer {
variant: String,
items: Vec<Json5Value>,
}
impl serde::ser::SerializeTupleVariant for Json5TupleVariantSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_field<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.items.push(value.serialize(Json5ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
let mut map = std::collections::BTreeMap::new();
map.insert(self.variant, Json5Value::Array(self.items));
Ok(Json5Value::Object(map))
}
}
struct Json5MapSerializer {
map: std::collections::BTreeMap<String, Json5Value>,
next_key: Option<String>,
_capacity: usize,
}
impl serde::ser::SerializeMap for Json5MapSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_key<T: ?Sized + Serialize>(&mut self, key: &T) -> Result<(), Self::Error> {
// Serialize key to Json5Value, then extract string
let key_value = key.serialize(Json5ValueSerializer)?;
let key_str = match key_value {
Json5Value::String(s) => s,
Json5Value::Number(n) => n,
_ => {
return Err(Json5SerError(
"Map key must be string or number".to_string(),
))
}
};
self.next_key = Some(key_str);
Ok(())
}
fn serialize_value<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
let key = self.next_key.take().ok_or_else(|| {
Json5SerError("serialize_value called without serialize_key".to_string())
})?;
self.map.insert(key, value.serialize(Json5ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Object(self.map))
}
}
impl serde::ser::SerializeStruct for Json5MapSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
self.map
.insert(key.to_string(), value.serialize(Json5ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(Json5Value::Object(self.map))
}
}
struct Json5StructVariantSerializer {
variant: String,
map: std::collections::BTreeMap<String, Json5Value>,
_capacity: usize,
}
impl serde::ser::SerializeStructVariant for Json5StructVariantSerializer {
type Ok = Json5Value;
type Error = Json5SerError;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
self.map
.insert(key.to_string(), value.serialize(Json5ValueSerializer)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
let mut outer = std::collections::BTreeMap::new();
outer.insert(self.variant, Json5Value::Object(self.map));
Ok(Json5Value::Object(outer))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
#[test]
fn test_parse_value() {
let value = parse_value(r#"{"foo": "bar"}"#).unwrap();
assert_eq!(value["foo"], "bar");
}
#[test]
fn test_parse_value_with_comments() {
let value = parse_value(
r#"{
// This is a comment
"foo": "bar" // Inline comment
}"#,
)
.unwrap();
assert_eq!(value["foo"], "bar");
}
#[test]
fn test_parse_value_with_trailing_comma() {
let value = parse_value(
r#"{
"foo": "bar",
"baz": 123,
}"#,
)
.unwrap();
assert_eq!(value["foo"], "bar");
assert_eq!(value["baz"], 123);
}
#[test]
fn test_parse_value_empty() {
// Empty string should fail to parse
assert!(parse_value("").is_err());
}
#[test]
fn test_parse_value_invalid() {
// Invalid JSON should fail to parse
assert!(parse_value("{invalid}").is_err());
}
#[test]
fn test_parse_value_with_context() {
let err = parse_value_with_context("{invalid}", || "test.json".to_string()).unwrap_err();
assert!(err.to_string().contains("test.json"));
assert!(err.to_string().contains("parse"));
}
#[derive(Debug, Deserialize, PartialEq)]
struct TestStruct {
foo: String,
bar: i32,
}
#[test]
fn test_from_str() {
let result: TestStruct = from_str(r#"{"foo": "hello", "bar": 42}"#).unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_str_with_comments() {
let result: TestStruct = from_str(
r#"{
// Comment
"foo": "hello",
"bar": 42, // Trailing comma is fine
}"#,
)
.unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_str_invalid_type() {
let err = from_str::<TestStruct>(r#"{"foo": "hello"}"#).unwrap_err();
assert!(err.to_string().contains("deserialize"));
}
#[test]
fn test_from_str_with_context() {
let err = from_str_with_context::<TestStruct>(r#"{"foo": "hello"}"#, || {
"config.json".to_string()
})
.unwrap_err();
assert!(err.to_string().contains("config.json"));
assert!(err.to_string().contains("JSON5 parse error"));
}
#[test]
fn test_parse_value_from_slice_with_context() {
let err = parse_value_from_slice_with_context(b"{invalid}", || "test.json".to_string())
.unwrap_err();
assert!(err.to_string().contains("test.json"));
assert!(err.to_string().contains("parse"));
}
#[test]
fn test_parse_value_from_slice_with_context_invalid_utf8() {
let err = parse_value_from_slice_with_context(&[0xFF, 0xFF], || "test.json".to_string())
.unwrap_err();
assert!(err.to_string().contains("test.json"));
assert!(err.to_string().contains("UTF-8"));
}
#[test]
fn test_from_slice() {
let result: TestStruct = from_slice(br#"{"foo": "hello", "bar": 42}"#).unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_slice_with_comments() {
let result: TestStruct = from_slice(
br#"{
// Comment
"foo": "hello",
"bar": 42, // Trailing comma is fine
}"#,
)
.unwrap();
assert_eq!(
result,
TestStruct {
foo: "hello".to_string(),
bar: 42
}
);
}
#[test]
fn test_from_slice_invalid_utf8() {
let err = from_slice::<TestStruct>(&[0xFF, 0xFF]).unwrap_err();
assert!(err.to_string().contains("UTF-8"));
}
#[test]
fn test_from_slice_with_context() {
let err = from_slice_with_context::<TestStruct>(br#"{"foo": "hello"}"#, || {
"config.json".to_string()
})
.unwrap_err();
assert!(err.to_string().contains("config.json"));
assert!(err.to_string().contains("JSON5 parse error"));
}
#[test]
fn test_from_slice_with_context_invalid_utf8() {
let err =
from_slice_with_context::<TestStruct>(&[0xFF, 0xFF], || "config.json".to_string())
.unwrap_err();
assert!(err.to_string().contains("config.json"));
assert!(err.to_string().contains("UTF-8"));
}
#[test]
fn test_to_vec_pretty_json5_format() {
use indexmap::IndexMap;
let mut map: IndexMap<String, i32> = IndexMap::new();
map.insert("zebra".to_string(), 1);
map.insert("apple".to_string(), 2);
map.insert("mango".to_string(), 3);
let result = to_vec_pretty_sorted(&map).unwrap();
let output = String::from_utf8(result).unwrap();
// JSON5 output should contain all keys (may be unquoted)
assert!(output.contains("zebra"));
assert!(output.contains("apple"));
assert!(output.contains("mango"));
}
#[test]
fn test_serialize_special_floats() {
use serde::Serialize;
#[derive(Serialize)]
struct SpecialFloats {
infinity: f64,
neg_infinity: f64,
nan: f64,
normal: f64,
}
let data = SpecialFloats {
infinity: f64::INFINITY,
neg_infinity: f64::NEG_INFINITY,
nan: f64::NAN,
normal: 42.5,
};
let output = String::from_utf8(to_vec_pretty_sorted(&data).unwrap()).unwrap();
// JSON5 should output special float values correctly
assert!(output.contains("Infinity"));
assert!(output.contains("-Infinity"));
assert!(output.contains("NaN"));
assert!(output.contains("42.5"));
}
// =========================================================================
// JSON5 NaN/Infinity tests - critical for Roblox sync/syncback
// =========================================================================
//
// Note: serde_json::Value cannot represent NaN/Infinity (as_f64 returns None).
// However, direct deserialization into f64 fields works correctly.
// This is fine for Rojo since properties are deserialized into typed structs.
#[derive(Debug, Deserialize, PartialEq)]
struct Vector3 {
x: f64,
y: f64,
z: f64,
}
#[test]
fn test_deserialize_infinity() {
#[derive(Debug, Deserialize)]
struct TestFloat {
x: f64,
}
let result: TestFloat = from_str(r#"{ "x": Infinity }"#).unwrap();
assert!(result.x.is_infinite() && result.x.is_sign_positive());
}
#[test]
fn test_deserialize_negative_infinity() {
#[derive(Debug, Deserialize)]
struct TestFloat {
x: f64,
}
let result: TestFloat = from_str(r#"{ "x": -Infinity }"#).unwrap();
assert!(result.x.is_infinite() && result.x.is_sign_negative());
}
#[test]
fn test_deserialize_nan() {
#[derive(Debug, Deserialize)]
struct TestFloat {
x: f64,
}
let result: TestFloat = from_str(r#"{ "x": NaN }"#).unwrap();
assert!(result.x.is_nan());
}
#[test]
fn test_deserialize_vector_with_infinity() {
let result: Vector3 = from_str(r#"{ "x": Infinity, "y": -Infinity, "z": 0 }"#).unwrap();
assert!(result.x.is_infinite() && result.x.is_sign_positive());
assert!(result.y.is_infinite() && result.y.is_sign_negative());
assert_eq!(result.z, 0.0);
}
#[test]
fn test_deserialize_vector_with_nan() {
let result: Vector3 = from_str(r#"{ "x": NaN, "y": 1.5, "z": NaN }"#).unwrap();
assert!(result.x.is_nan());
assert_eq!(result.y, 1.5);
assert!(result.z.is_nan());
}
#[derive(Debug, Deserialize)]
struct CFrameLike {
position: Vector3,
orientation: [f64; 9],
}
#[test]
fn test_deserialize_cframe_with_special_values() {
// Simulates a CFrame with NaN/Infinity in position and rotation matrix
let result: CFrameLike = from_str(
r#"{
"position": { "x": Infinity, "y": NaN, "z": -Infinity },
"orientation": [1, 0, 0, 0, NaN, 0, 0, 0, Infinity]
}"#,
)
.unwrap();
assert!(result.position.x.is_infinite() && result.position.x.is_sign_positive());
assert!(result.position.y.is_nan());
assert!(result.position.z.is_infinite() && result.position.z.is_sign_negative());
assert_eq!(result.orientation[0], 1.0);
assert!(result.orientation[4].is_nan());
assert!(result.orientation[8].is_infinite());
}
#[derive(Debug, Deserialize)]
struct TypedProperties {
#[serde(rename = "Size")]
size: Vector3,
#[serde(rename = "Transparency")]
transparency: f64,
}
#[derive(Debug, Deserialize)]
struct RojoPropertyTyped {
#[serde(rename = "$className")]
class_name: String,
#[serde(rename = "$properties")]
properties: TypedProperties,
}
#[test]
fn test_deserialize_rojo_style_properties_with_special_floats() {
// Simulates how Rojo project files might contain properties with NaN/Infinity
let result: RojoPropertyTyped = from_str(
r#"{
"$className": "Part",
"$properties": {
"Size": { "x": Infinity, "y": NaN, "z": -Infinity },
"Transparency": NaN
}
}"#,
)
.unwrap();
assert_eq!(result.class_name, "Part");
assert!(result.properties.size.x.is_infinite());
assert!(result.properties.size.y.is_nan());
assert!(result.properties.size.z.is_infinite());
assert!(result.properties.transparency.is_nan());
}
#[test]
fn test_json5_with_comments_and_special_floats() {
// Combines JSON5 features: comments, trailing commas, and special float values
let result: Vector3 = from_str(
r#"{
// Position with infinite X
"x": Infinity,
"y": -Infinity, // Negative infinity
"z": NaN, // Not a number
}"#,
)
.unwrap();
assert!(result.x.is_infinite() && result.x.is_sign_positive());
assert!(result.y.is_infinite() && result.y.is_sign_negative());
assert!(result.z.is_nan());
}
#[test]
fn test_from_slice_with_special_floats() {
let result: Vector3 =
from_slice(br#"{ "x": Infinity, "y": NaN, "z": -Infinity }"#).unwrap();
assert!(result.x.is_infinite());
assert!(result.y.is_nan());
assert!(result.z.is_infinite());
}
#[test]
fn test_deserialize_array_of_special_floats() {
#[derive(Debug, Deserialize)]
struct ArrayHolder {
values: Vec<f64>,
}
let result: ArrayHolder =
from_str(r#"{ "values": [1.0, Infinity, -Infinity, NaN, 0.0] }"#).unwrap();
assert_eq!(result.values[0], 1.0);
assert!(result.values[1].is_infinite() && result.values[1].is_sign_positive());
assert!(result.values[2].is_infinite() && result.values[2].is_sign_negative());
assert!(result.values[3].is_nan());
assert_eq!(result.values[4], 0.0);
}
#[test]
fn test_deserialize_nested_special_floats() {
// Simulates nested structures like CFrame matrices
#[derive(Debug, Deserialize)]
struct Transform {
position: [f64; 3],
rotation: [[f64; 3]; 3],
}
let result: Transform = from_str(
r#"{
"position": [Infinity, NaN, -Infinity],
"rotation": [
[1, 0, NaN],
[0, Infinity, 0],
[-Infinity, 0, 1]
]
}"#,
)
.unwrap();
assert!(result.position[0].is_infinite());
assert!(result.position[1].is_nan());
assert!(result.position[2].is_infinite());
assert!(result.rotation[0][2].is_nan());
assert!(result.rotation[1][1].is_infinite());
assert!(result.rotation[2][0].is_infinite());
}
#[test]
fn test_mixed_json5_features_with_special_floats() {
// All JSON5 features together: unquoted keys would work too but we use quoted for Rojo
#[derive(Debug, Deserialize)]
struct MixedTest {
normal: f64,
infinity: f64,
negative_infinity: f64,
nan_value: f64,
}
let result: MixedTest = from_str(
r#"{
// Regular number
"normal": 42.5,
"infinity": Infinity, // Positive infinity
"negative_infinity": -Infinity,
"nan_value": NaN, // Not a number
}"#, // trailing comma
)
.unwrap();
assert_eq!(result.normal, 42.5);
assert!(result.infinity.is_infinite() && result.infinity.is_sign_positive());
assert!(
result.negative_infinity.is_infinite() && result.negative_infinity.is_sign_negative()
);
assert!(result.nan_value.is_nan());
}
// =========================================================================
// Syncback roundtrip tests - serialize then deserialize
// =========================================================================
#[test]
fn test_syncback_roundtrip_special_floats() {
use serde::Serialize;
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct SyncbackData {
normal: f64,
infinity: f64,
neg_infinity: f64,
}
let original = SyncbackData {
normal: 42.5,
infinity: f64::INFINITY,
neg_infinity: f64::NEG_INFINITY,
};
// Serialize with json5
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Verify JSON5 output contains special values
assert!(json_str.contains("Infinity"));
assert!(json_str.contains("-Infinity"));
// Deserialize back
let deserialized: SyncbackData = from_str(&json_str).unwrap();
assert_eq!(deserialized.normal, original.normal);
assert!(deserialized.infinity.is_infinite() && deserialized.infinity.is_sign_positive());
assert!(
deserialized.neg_infinity.is_infinite() && deserialized.neg_infinity.is_sign_negative()
);
}
#[test]
fn test_syncback_roundtrip_nan() {
use serde::Serialize;
#[derive(Debug, Serialize, Deserialize)]
struct NanData {
value: f64,
}
let original = NanData { value: f64::NAN };
// Serialize with json5
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Verify JSON5 output contains NaN
assert!(json_str.contains("NaN"));
// Deserialize back
let deserialized: NanData = from_str(&json_str).unwrap();
assert!(deserialized.value.is_nan());
}
#[test]
fn test_syncback_cframe_like_roundtrip() {
use serde::Serialize;
#[derive(Debug, Serialize, Deserialize)]
struct CFrameData {
position: [f64; 3],
orientation: [[f64; 3]; 3],
}
let original = CFrameData {
position: [f64::INFINITY, f64::NAN, f64::NEG_INFINITY],
orientation: [
[1.0, 0.0, f64::NAN],
[0.0, f64::INFINITY, 0.0],
[f64::NEG_INFINITY, 0.0, 1.0],
],
};
// Serialize
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Deserialize
let deserialized: CFrameData = from_str(&json_str).unwrap();
// Verify position
assert!(
deserialized.position[0].is_infinite() && deserialized.position[0].is_sign_positive()
);
assert!(deserialized.position[1].is_nan());
assert!(
deserialized.position[2].is_infinite() && deserialized.position[2].is_sign_negative()
);
// Verify orientation
assert_eq!(deserialized.orientation[0][0], 1.0);
assert!(deserialized.orientation[0][2].is_nan());
assert!(deserialized.orientation[1][1].is_infinite());
assert!(
deserialized.orientation[2][0].is_infinite()
&& deserialized.orientation[2][0].is_sign_negative()
);
}
#[test]
fn test_syncback_rojo_meta_like_roundtrip() {
use serde::Serialize;
use std::collections::BTreeMap;
#[derive(Debug, Serialize, Deserialize)]
struct MetaFile {
#[serde(rename = "className")]
class_name: String,
properties: BTreeMap<String, f64>,
}
let mut properties = BTreeMap::new();
properties.insert("Transparency".to_string(), f64::NAN);
properties.insert("Health".to_string(), f64::INFINITY);
properties.insert("WalkSpeed".to_string(), 16.0);
let original = MetaFile {
class_name: "Humanoid".to_string(),
properties,
};
// Serialize
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Verify output format
assert!(json_str.contains("Humanoid"));
assert!(json_str.contains("NaN"));
assert!(json_str.contains("Infinity"));
// Deserialize
let deserialized: MetaFile = from_str(&json_str).unwrap();
assert_eq!(deserialized.class_name, "Humanoid");
assert!(deserialized
.properties
.get("Transparency")
.unwrap()
.is_nan());
assert!(deserialized.properties.get("Health").unwrap().is_infinite());
assert_eq!(*deserialized.properties.get("WalkSpeed").unwrap(), 16.0);
}
// =========================================================================
// Sorted JSON5 serializer tests - comprehensive coverage
// =========================================================================
mod serializer_tests {
use super::*;
// =====================================================================
// Large/Small Number Handling (the main bug we fixed)
// =====================================================================
#[test]
fn test_roundtrip_large_number() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let original = Data { value: 1e47 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Should use scientific notation for large numbers
assert!(
json_str.contains("1e47") || json_str.contains("1e+47"),
"Large number should be in scientific notation: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_roundtrip_very_small_number() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let original = Data { value: 1e-100 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Should use scientific notation for very small numbers
assert!(
json_str.contains("e-"),
"Very small number should be in scientific notation: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_roundtrip_negative_large_number() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let original = Data { value: -1e47 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_roundtrip_negative_small_number() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let original = Data { value: -1e-50 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
// Test the exact boundaries where we switch to scientific notation
#[test]
fn test_boundary_at_1e15() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
below: f64,
at: f64,
above: f64,
}
let original = Data {
below: 9.99e14,
at: 1e15,
above: 1.01e15,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// At and above 1e15 should use scientific notation
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.below, original.below);
assert_eq!(deserialized.at, original.at);
assert_eq!(deserialized.above, original.above);
}
#[test]
fn test_boundary_at_1e_minus_6() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
above: f64,
at: f64,
below: f64,
}
let original = Data {
above: 1.01e-6,
at: 1e-6,
below: 9.9e-7,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Below 1e-6 should use scientific notation
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.above, original.above);
assert_eq!(deserialized.at, original.at);
assert_eq!(deserialized.below, original.below);
}
#[test]
fn test_zero_not_scientific() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
num: f64,
}
let original = Data { num: 0.0 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Zero should not use scientific notation (check the value part only)
// The output should contain "num: 0" not "num: 0e0" or similar
assert!(
json_str.contains(": 0") && !json_str.contains("0e"),
"Zero should not be in scientific notation: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.num, 0.0);
}
#[test]
fn test_normal_numbers_not_scientific() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
a: f64,
b: f64,
c: f64,
}
let original = Data {
a: 42.5,
b: 1000.0,
c: 0.001,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Normal numbers should not use scientific notation
assert!(
!json_str.contains("e"),
"Normal numbers should not use scientific notation: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.a, original.a);
assert_eq!(deserialized.b, original.b);
assert_eq!(deserialized.c, original.c);
}
// =====================================================================
// Special Float Values (NaN, Infinity)
// =====================================================================
#[test]
fn test_nan_roundtrip() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
value: f64,
}
let original = Data { value: f64::NAN };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(json_str.contains("NaN"), "Should contain NaN: {}", json_str);
let deserialized: Data = from_str(&json_str).unwrap();
assert!(deserialized.value.is_nan());
}
#[test]
fn test_infinity_roundtrip() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
value: f64,
}
let original = Data {
value: f64::INFINITY,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(
json_str.contains("Infinity"),
"Should contain Infinity: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert!(deserialized.value.is_infinite() && deserialized.value.is_sign_positive());
}
#[test]
fn test_negative_infinity_roundtrip() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
value: f64,
}
let original = Data {
value: f64::NEG_INFINITY,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(
json_str.contains("-Infinity"),
"Should contain -Infinity: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert!(deserialized.value.is_infinite() && deserialized.value.is_sign_negative());
}
#[test]
fn test_mixed_special_floats() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
nan: f64,
pos_inf: f64,
neg_inf: f64,
normal: f64,
large: f64,
}
let original = Data {
nan: f64::NAN,
pos_inf: f64::INFINITY,
neg_inf: f64::NEG_INFINITY,
normal: 42.5,
large: 1e47,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert!(deserialized.nan.is_nan());
assert!(deserialized.pos_inf.is_infinite() && deserialized.pos_inf.is_sign_positive());
assert!(deserialized.neg_inf.is_infinite() && deserialized.neg_inf.is_sign_negative());
assert_eq!(deserialized.normal, 42.5);
assert_eq!(deserialized.large, 1e47);
}
// =====================================================================
// Integer Types
// =====================================================================
#[test]
fn test_integer_types() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
i8_val: i8,
i16_val: i16,
i32_val: i32,
i64_val: i64,
u8_val: u8,
u16_val: u16,
u32_val: u32,
u64_val: u64,
}
let original = Data {
i8_val: -128,
i16_val: -32768,
i32_val: -2147483648,
i64_val: -9223372036854775808,
u8_val: 255,
u16_val: 65535,
u32_val: 4294967295,
u64_val: 18446744073709551615,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
// =====================================================================
// Sorted Keys
// =====================================================================
#[test]
fn test_output_sorted_keys() {
#[derive(Debug, Serialize)]
struct Data {
zebra: i32,
apple: i32,
mango: i32,
}
let data = Data {
zebra: 3,
apple: 1,
mango: 2,
};
let serialized = to_vec_pretty_sorted(&data).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Keys should be sorted alphabetically
let apple_pos = json_str.find("apple").unwrap();
let mango_pos = json_str.find("mango").unwrap();
let zebra_pos = json_str.find("zebra").unwrap();
assert!(apple_pos < mango_pos);
assert!(mango_pos < zebra_pos);
}
#[test]
fn test_nested_objects_sorted() {
#[derive(Debug, Serialize)]
struct Inner {
z: i32,
a: i32,
}
#[derive(Debug, Serialize)]
struct Outer {
z_inner: Inner,
a_inner: Inner,
}
let data = Outer {
z_inner: Inner { z: 1, a: 2 },
a_inner: Inner { z: 3, a: 4 },
};
let serialized = to_vec_pretty_sorted(&data).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Outer keys should be sorted
let a_inner_pos = json_str.find("a_inner").unwrap();
let z_inner_pos = json_str.find("z_inner").unwrap();
assert!(a_inner_pos < z_inner_pos);
// Inner keys should also be sorted (check first occurrence of 'a:' vs 'z:')
// This is a bit tricky since 'a' appears in key names too
assert!(json_str.contains("a_inner"));
assert!(json_str.contains("z_inner"));
}
#[test]
fn test_map_keys_sorted() {
let mut map = std::collections::HashMap::new();
map.insert("zebra", 3);
map.insert("apple", 1);
map.insert("mango", 2);
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let apple_pos = json_str.find("apple").unwrap();
let mango_pos = json_str.find("mango").unwrap();
let zebra_pos = json_str.find("zebra").unwrap();
assert!(apple_pos < mango_pos);
assert!(mango_pos < zebra_pos);
}
// =====================================================================
// Pretty Formatting
// =====================================================================
#[test]
fn test_output_pretty_format() {
#[derive(Debug, Serialize)]
struct Data {
key: String,
}
let data = Data {
key: "value".to_string(),
};
let serialized = to_vec_pretty_sorted(&data).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Should have newlines and indentation
assert!(json_str.contains('\n'));
assert!(json_str.contains(" ")); // indentation
}
#[test]
fn test_nested_indentation() {
#[derive(Debug, Serialize)]
struct Inner {
value: i32,
}
#[derive(Debug, Serialize)]
struct Outer {
inner: Inner,
}
let data = Outer {
inner: Inner { value: 42 },
};
let serialized = to_vec_pretty_sorted(&data).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Should have multiple levels of indentation
assert!(json_str.contains(" ")); // 4 spaces for nested
}
// =====================================================================
// String Escaping
// =====================================================================
#[test]
fn test_write_to_escapes_special_chars() {
let value = Json5Value::String("hello\nworld\t\"test\"".to_string());
let mut output = String::new();
value.write_to(&mut output, 0);
assert_eq!(output, r#""hello\nworld\t\"test\"""#);
}
#[test]
fn test_string_escaping_backslash() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
path: String,
}
let original = Data {
path: r"C:\Users\test".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(
json_str.contains(r"\\"),
"Backslash should be escaped: {}",
json_str
);
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.path, original.path);
}
#[test]
fn test_string_escaping_control_chars() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
text: String,
}
let original = Data {
text: "line1\nline2\rline3\ttab".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(json_str.contains(r"\n"));
assert!(json_str.contains(r"\r"));
assert!(json_str.contains(r"\t"));
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.text, original.text);
}
#[test]
fn test_unicode_strings() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
emoji: String,
chinese: String,
arabic: String,
}
let original = Data {
emoji: "Hello 👋 World 🌍".to_string(),
chinese: "你好世界".to_string(),
arabic: "مرحبا بالعالم".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
// =====================================================================
// Identifier Quoting
// =====================================================================
#[test]
fn test_valid_identifier_unquoted() {
assert!(is_valid_identifier("foo"));
assert!(is_valid_identifier("_bar"));
assert!(is_valid_identifier("$baz"));
assert!(is_valid_identifier("foo123"));
assert!(is_valid_identifier("_"));
assert!(is_valid_identifier("$"));
assert!(is_valid_identifier("camelCase"));
assert!(is_valid_identifier("snake_case"));
}
#[test]
fn test_invalid_identifier_quoted() {
assert!(!is_valid_identifier("")); // empty
assert!(!is_valid_identifier("123")); // starts with digit
assert!(!is_valid_identifier("foo-bar")); // contains dash
assert!(!is_valid_identifier("foo bar")); // contains space
assert!(!is_valid_identifier("foo.bar")); // contains dot
assert!(!is_valid_identifier("1foo")); // starts with digit
assert!(!is_valid_identifier("foo@bar")); // contains @
}
#[test]
fn test_keys_with_special_chars_quoted() {
let mut map = std::collections::HashMap::new();
map.insert("normal_key", 1);
map.insert("key-with-dash", 2);
map.insert("key with space", 3);
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Normal key should be unquoted
assert!(
json_str.contains("normal_key:"),
"Normal key should be unquoted: {}",
json_str
);
// Special keys should be quoted
assert!(
json_str.contains("\"key-with-dash\""),
"Dashed key should be quoted: {}",
json_str
);
assert!(
json_str.contains("\"key with space\""),
"Spaced key should be quoted: {}",
json_str
);
}
#[test]
fn test_keys_with_escaped_chars_roundtrip() {
// Test keys that contain characters requiring escaping
let mut map = std::collections::HashMap::new();
map.insert("key\"with\"quotes".to_string(), 1);
map.insert("key\\with\\backslash".to_string(), 2);
map.insert("key\nwith\nnewline".to_string(), 3);
map.insert("key\twith\ttab".to_string(), 4);
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Verify escaping is present
assert!(
json_str.contains(r#"\""#),
"Quotes in key should be escaped: {}",
json_str
);
assert!(
json_str.contains(r"\\"),
"Backslash in key should be escaped: {}",
json_str
);
assert!(
json_str.contains(r"\n"),
"Newline in key should be escaped: {}",
json_str
);
assert!(
json_str.contains(r"\t"),
"Tab in key should be escaped: {}",
json_str
);
// Verify roundtrip works
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized.get("key\"with\"quotes"), Some(&1));
assert_eq!(deserialized.get("key\\with\\backslash"), Some(&2));
assert_eq!(deserialized.get("key\nwith\nnewline"), Some(&3));
assert_eq!(deserialized.get("key\twith\ttab"), Some(&4));
}
#[test]
fn test_keys_with_control_chars_roundtrip() {
// Test keys with control characters (should be escaped as \uXXXX)
let mut map = std::collections::HashMap::new();
map.insert("key\x00null".to_string(), 1); // NUL character
map.insert("key\x07bell".to_string(), 2); // BEL character
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Control characters should be escaped as \uXXXX
assert!(
json_str.contains(r"\u0000") || json_str.contains(r"\u0007"),
"Control characters in key should be escaped: {}",
json_str
);
// Verify roundtrip works
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized.get("key\x00null"), Some(&1));
assert_eq!(deserialized.get("key\x07bell"), Some(&2));
}
#[test]
fn test_write_escaped_string_function() {
// Direct test of the escaping function
let mut output = String::new();
write_escaped_string(&mut output, r#"hello"world"#);
assert_eq!(output, r#""hello\"world""#);
let mut output = String::new();
write_escaped_string(&mut output, "line1\nline2");
assert_eq!(output, r#""line1\nline2""#);
let mut output = String::new();
write_escaped_string(&mut output, r"path\to\file");
assert_eq!(output, r#""path\\to\\file""#);
}
// =====================================================================
// Arrays and Empty Collections
// =====================================================================
#[test]
fn test_array_roundtrip() {
let original: Vec<i32> = vec![1, 2, 3, 4, 5];
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Vec<i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_empty_array() {
let original: Vec<i32> = vec![];
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(json_str.contains("[]"));
let deserialized: Vec<i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_empty_object() {
let original: std::collections::HashMap<String, i32> = std::collections::HashMap::new();
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(json_str.contains("{}"));
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_nested_arrays() {
let original: Vec<Vec<i32>> = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Vec<Vec<i32>> = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
// =====================================================================
// Optional Values
// =====================================================================
#[test]
fn test_option_some() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: Option<i32>,
}
let original = Data { value: Some(42) };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_option_none() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: Option<i32>,
}
let original = Data { value: None };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(json_str.contains("null"));
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
// =====================================================================
// Bool and Null
// =====================================================================
#[test]
fn test_bool_values() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
t: bool,
f: bool,
}
let original = Data { t: true, f: false };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert!(json_str.contains("true"));
assert!(json_str.contains("false"));
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_null_value() {
let original: Option<i32> = None;
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
assert_eq!(json_str.trim(), "null");
}
// =====================================================================
// Complex/Real-World Structures
// =====================================================================
#[test]
fn test_roundtrip_complex_model() {
#[derive(Debug, Serialize, Deserialize)]
struct Model {
#[serde(rename = "className")]
class_name: String,
properties: std::collections::BTreeMap<String, f64>,
}
let mut properties = std::collections::BTreeMap::new();
properties.insert("Value".to_string(), 1e47);
properties.insert("Normal".to_string(), 42.5);
properties.insert("Tiny".to_string(), 1e-50);
let original = Model {
class_name: "NumberValue".to_string(),
properties,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Model = from_str(&json_str).unwrap();
assert_eq!(deserialized.class_name, "NumberValue");
assert_eq!(*deserialized.properties.get("Value").unwrap(), 1e47);
assert_eq!(*deserialized.properties.get("Normal").unwrap(), 42.5);
assert_eq!(*deserialized.properties.get("Tiny").unwrap(), 1e-50);
}
#[test]
fn test_roblox_model_like_structure() {
// Mimics the actual structure that caused the original bug
#[derive(Debug, Serialize, Deserialize)]
struct JsonModel {
#[serde(rename = "className")]
class_name: String,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
properties: std::collections::BTreeMap<String, serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
children: Vec<JsonModel>,
}
let mut properties = std::collections::BTreeMap::new();
properties.insert("Value".to_string(), serde_json::json!(1e47));
let model = JsonModel {
class_name: "NumberValue".to_string(),
properties,
children: vec![],
};
let serialized = to_vec_pretty_sorted(&model).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Should be parseable
let _: JsonModel = from_str(&json_str).unwrap();
}
#[test]
fn test_deeply_nested_structure() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Level3 {
value: f64,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Level2 {
level3: Level3,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Level1 {
level2: Level2,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Root {
level1: Level1,
}
let original = Root {
level1: Level1 {
level2: Level2 {
level3: Level3 { value: 1e47 },
},
},
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Root = from_str(&json_str).unwrap();
assert_eq!(deserialized.level1.level2.level3.value, 1e47);
}
// =====================================================================
// Enum Variants
// =====================================================================
#[test]
fn test_enum_unit_variant() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
enum Status {
Active,
Inactive,
}
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
status: Status,
}
let original = Data {
status: Status::Active,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
#[test]
fn test_enum_newtype_variant() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
enum Value {
Number(f64),
Text(String),
}
let original = Value::Number(1e47);
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Value = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
// =====================================================================
// Char and Bytes
// =====================================================================
#[test]
fn test_char_serialization() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
ch: char,
}
let original = Data { ch: 'X' };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized, original);
}
// =====================================================================
// Regression Tests
// =====================================================================
#[test]
fn test_regression_numbervalue_1e47() {
// This is the exact case that caused the original bug
#[derive(Debug, Serialize, Deserialize)]
struct NumberValue {
#[serde(rename = "className")]
class_name: String,
properties: std::collections::BTreeMap<String, f64>,
}
let mut properties = std::collections::BTreeMap::new();
properties.insert("Value".to_string(), 1e47);
let original = NumberValue {
class_name: "NumberValue".to_string(),
properties,
};
// This should not panic or error
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// And should be parseable
let deserialized: NumberValue = from_str(&json_str).unwrap();
assert_eq!(deserialized.properties["Value"], 1e47);
}
#[test]
fn test_f64_max_min() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
max: f64,
min: f64,
}
let original = Data {
max: f64::MAX,
min: f64::MIN,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.max, f64::MAX);
assert_eq!(deserialized.min, f64::MIN);
}
#[test]
fn test_f64_epsilon_and_min_positive() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
epsilon: f64,
min_positive: f64,
}
let original = Data {
epsilon: f64::EPSILON,
min_positive: f64::MIN_POSITIVE,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.epsilon, f64::EPSILON);
assert_eq!(deserialized.min_positive, f64::MIN_POSITIVE);
}
}
// =========================================================================
// JSON5-specific parsing edge cases (using json5 crate)
// These test that we can correctly parse JSON5 input formats
// =========================================================================
mod json5_parsing_edge_cases {
use super::*;
// =====================================================================
// Number Parsing Edge Cases
// =====================================================================
#[test]
fn test_parse_hexadecimal() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
let result: Data = from_str(r#"{ value: 0xFF }"#).unwrap();
assert_eq!(result.value, 255);
let result: Data = from_str(r#"{ value: 0xDEADBEEF }"#).unwrap();
assert_eq!(result.value, 0xDEADBEEF);
}
#[test]
fn test_parse_negative_hexadecimal() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
let result: Data = from_str(r#"{ value: -0xFF }"#).unwrap();
assert_eq!(result.value, -255);
}
#[test]
fn test_parse_leading_decimal_point() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: .5 }"#).unwrap();
assert_eq!(result.value, 0.5);
let result: Data = from_str(r#"{ value: .123 }"#).unwrap();
assert_eq!(result.value, 0.123);
}
#[test]
fn test_parse_trailing_decimal_point() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: 5. }"#).unwrap();
assert_eq!(result.value, 5.0);
let result: Data = from_str(r#"{ value: 123. }"#).unwrap();
assert_eq!(result.value, 123.0);
}
#[test]
fn test_parse_positive_sign() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: +5 }"#).unwrap();
assert_eq!(result.value, 5.0);
let result: Data = from_str(r#"{ value: +Infinity }"#).unwrap();
assert!(result.value.is_infinite() && result.value.is_sign_positive());
}
#[test]
fn test_parse_negative_zero() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: -0 }"#).unwrap();
assert_eq!(result.value, 0.0);
// Note: -0.0 == 0.0 in Rust, but we can check the sign bit
assert!(result.value.is_sign_negative() || result.value == 0.0);
}
#[test]
fn test_parse_exponent_variations() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: f64,
}
// Uppercase E
let result: Data = from_str(r#"{ value: 1E10 }"#).unwrap();
assert_eq!(result.value, 1e10);
// Positive exponent with +
let result: Data = from_str(r#"{ value: 1e+10 }"#).unwrap();
assert_eq!(result.value, 1e10);
// Negative exponent
let result: Data = from_str(r#"{ value: 1e-10 }"#).unwrap();
assert_eq!(result.value, 1e-10);
// Zero exponent
let result: Data = from_str(r#"{ value: 5e0 }"#).unwrap();
assert_eq!(result.value, 5.0);
}
// =====================================================================
// String Parsing Edge Cases
// =====================================================================
#[test]
fn test_parse_single_quoted_strings() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: 'hello world' }"#).unwrap();
assert_eq!(result.value, "hello world");
}
#[test]
fn test_parse_single_quoted_with_double_quote() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: 'say "hello"' }"#).unwrap();
assert_eq!(result.value, r#"say "hello""#);
}
#[test]
fn test_parse_escaped_single_quote() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: 'it\'s working' }"#).unwrap();
assert_eq!(result.value, "it's working");
}
#[test]
fn test_parse_multi_line_string() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
// JSON5 allows escaped newlines in strings
let result: Data = from_str("{ value: 'line1\\\nline2' }").unwrap();
assert_eq!(result.value, "line1line2");
}
#[test]
fn test_parse_hex_escape_sequences() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\x48\x65\x6C\x6C\x6F" }"#).unwrap();
assert_eq!(result.value, "Hello");
}
#[test]
fn test_parse_unicode_escape_sequences() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\u0048\u0065\u006C\u006C\u006F" }"#).unwrap();
assert_eq!(result.value, "Hello");
// Emoji
let result: Data = from_str(r#"{ value: "\uD83D\uDE00" }"#).unwrap();
assert_eq!(result.value, "😀");
}
// =====================================================================
// Object Parsing Edge Cases
// =====================================================================
#[test]
fn test_parse_unquoted_keys() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
hello: String,
world: i32,
}
let result: Data = from_str(r#"{ hello: "test", world: 42 }"#).unwrap();
assert_eq!(result.hello, "test");
assert_eq!(result.world, 42);
}
#[test]
fn test_parse_single_quoted_keys() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
#[serde(rename = "my-key")]
my_key: String,
}
let result: Data = from_str(r#"{ 'my-key': "value" }"#).unwrap();
assert_eq!(result.my_key, "value");
}
#[test]
fn test_parse_reserved_words_as_keys() {
// JSON5 allows reserved words as unquoted keys
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
r#while: i32,
r#for: i32,
r#if: i32,
}
let result: Data = from_str(r#"{ while: 1, for: 2, if: 3 }"#).unwrap();
assert_eq!(result.r#while, 1);
assert_eq!(result.r#for, 2);
assert_eq!(result.r#if, 3);
}
#[test]
fn test_parse_trailing_comma_object() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
a: i32,
b: i32,
}
let result: Data = from_str(r#"{ a: 1, b: 2, }"#).unwrap();
assert_eq!(result.a, 1);
assert_eq!(result.b, 2);
}
#[test]
fn test_parse_nested_trailing_commas() {
#[derive(Debug, Deserialize, PartialEq)]
struct Inner {
x: i32,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Outer {
inner: Inner,
value: i32,
}
let result: Outer = from_str(r#"{ inner: { x: 1, }, value: 2, }"#).unwrap();
assert_eq!(result.inner.x, 1);
assert_eq!(result.value, 2);
}
// =====================================================================
// Array Parsing Edge Cases
// =====================================================================
#[test]
fn test_parse_trailing_comma_array() {
let result: Vec<i32> = from_str(r#"[1, 2, 3,]"#).unwrap();
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_parse_nested_arrays_with_trailing_commas() {
let result: Vec<Vec<i32>> = from_str(r#"[[1, 2,], [3, 4,],]"#).unwrap();
assert_eq!(result, vec![vec![1, 2], vec![3, 4]]);
}
// =====================================================================
// Comment Parsing Edge Cases
// =====================================================================
#[test]
fn test_parse_single_line_comment() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str(
r#"{
// This is a comment
value: 42
}"#,
)
.unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_parse_multi_line_comment() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str(
r#"{
/* This is a
multi-line comment */
value: 42
}"#,
)
.unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_parse_comment_after_value() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
a: i32,
b: i32,
}
let result: Data = from_str(
r#"{
a: 1, // comment after value
b: 2 /* another comment */
}"#,
)
.unwrap();
assert_eq!(result.a, 1);
assert_eq!(result.b, 2);
}
#[test]
fn test_parse_comment_in_array() {
let result: Vec<i32> = from_str(
r#"[
1, // first
2, /* second */
3
]"#,
)
.unwrap();
assert_eq!(result, vec![1, 2, 3]);
}
// =====================================================================
// Whitespace Edge Cases
// =====================================================================
#[test]
fn test_parse_various_whitespace() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
// Tab, newline, carriage return
let result: Data = from_str("{\t\n\rvalue\t:\n42\r}").unwrap();
assert_eq!(result.value, 42);
}
// =====================================================================
// Mixed JSON5 Features
// =====================================================================
#[test]
fn test_parse_complex_json5() {
#[derive(Debug, Deserialize)]
struct Config {
name: String,
#[serde(rename = "maxValue")]
max_value: f64,
enabled: bool,
items: Vec<f64>,
}
let result: Config = from_str(
r#"{
// Configuration file
name: 'test-config', // single quotes
maxValue: Infinity, // special float
enabled: true,
items: [
.5, // leading decimal
1., // trailing decimal
0xFF, // hex
1e10, // scientific
NaN, // NaN
], // trailing comma
}"#,
)
.unwrap();
assert_eq!(result.name, "test-config");
assert!(result.max_value.is_infinite());
assert!(result.enabled);
assert_eq!(result.items[0], 0.5);
assert_eq!(result.items[1], 1.0);
assert_eq!(result.items[2], 255.0);
assert_eq!(result.items[3], 1e10);
assert!(result.items[4].is_nan());
}
// =====================================================================
// Negative Zero Serialization Roundtrip
// =====================================================================
#[test]
fn test_negative_zero_roundtrip() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
value: f64,
}
let original = Data { value: -0.0 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
// -0.0 == 0.0 in IEEE 754, but the sign bit should be preserved
assert_eq!(deserialized.value, 0.0);
}
// =====================================================================
// Subnormal Numbers
// =====================================================================
#[test]
fn test_subnormal_numbers() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
tiny: f64,
}
// Subnormal (denormalized) number
let original = Data { tiny: 5e-324 };
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.tiny, original.tiny);
}
}
// =========================================================================
// JSON5 Spec Conformance Tests (https://spec.json5.org/)
// =========================================================================
//
// Comprehensive tests based on the official JSON5 specification to ensure
// our serializer produces valid JSON5 and can parse all valid JSON5 input.
mod json5_spec_conformance {
use super::*;
// =====================================================================
// Section 5.1: Escape Sequences (Table 1)
// https://spec.json5.org/#escapes
// =====================================================================
#[test]
fn test_escape_quotation_mark() {
// \" - Quotation mark (U+0022)
let mut output = String::new();
write_escaped_string(&mut output, "say \"hello\"");
assert_eq!(output, r#""say \"hello\"""#);
}
#[test]
fn test_escape_reverse_solidus() {
// \\ - Reverse solidus (U+005C)
let mut output = String::new();
write_escaped_string(&mut output, r"path\to\file");
assert_eq!(output, r#""path\\to\\file""#);
}
#[test]
fn test_escape_backspace() {
// \b - Backspace (U+0008)
let mut output = String::new();
write_escaped_string(&mut output, "hello\u{0008}world");
assert_eq!(output, r#""hello\bworld""#);
}
#[test]
fn test_escape_form_feed() {
// \f - Form feed (U+000C)
let mut output = String::new();
write_escaped_string(&mut output, "page\u{000C}break");
assert_eq!(output, r#""page\fbreak""#);
}
#[test]
fn test_escape_line_feed() {
// \n - Line feed (U+000A)
let mut output = String::new();
write_escaped_string(&mut output, "line1\nline2");
assert_eq!(output, r#""line1\nline2""#);
}
#[test]
fn test_escape_carriage_return() {
// \r - Carriage return (U+000D)
let mut output = String::new();
write_escaped_string(&mut output, "line1\rline2");
assert_eq!(output, r#""line1\rline2""#);
}
#[test]
fn test_escape_horizontal_tab() {
// \t - Horizontal tab (U+0009)
let mut output = String::new();
write_escaped_string(&mut output, "col1\tcol2");
assert_eq!(output, r#""col1\tcol2""#);
}
#[test]
fn test_escape_vertical_tab() {
// \v - Vertical tab (U+000B)
let mut output = String::new();
write_escaped_string(&mut output, "hello\u{000B}world");
assert_eq!(output, r#""hello\vworld""#);
}
#[test]
fn test_escape_null_character() {
// \0 - Null (U+0000) - we use \u0000 for safety
let mut output = String::new();
write_escaped_string(&mut output, "null\u{0000}char");
assert!(
output.contains("\\u0000") || output.contains("\\0"),
"Null should be escaped: {}",
output
);
}
#[test]
fn test_all_standard_escapes_combined() {
// Test all standard escapes in one string
let input = "\"\\\u{0008}\u{000C}\n\r\t\u{000B}";
let mut output = String::new();
write_escaped_string(&mut output, input);
assert!(output.contains("\\\""), "Quote not escaped");
assert!(output.contains("\\\\"), "Backslash not escaped");
assert!(output.contains("\\b"), "Backspace not escaped");
assert!(output.contains("\\f"), "Form feed not escaped");
assert!(output.contains("\\n"), "Line feed not escaped");
assert!(output.contains("\\r"), "Carriage return not escaped");
assert!(output.contains("\\t"), "Tab not escaped");
assert!(output.contains("\\v"), "Vertical tab not escaped");
}
// =====================================================================
// Section 5.2: Paragraph and Line Separators
// "JSON5 generators should escape these code points in strings."
// =====================================================================
#[test]
fn test_escape_line_separator_u2028() {
// U+2028 - Line separator (generators SHOULD escape)
let mut output = String::new();
write_escaped_string(&mut output, "line\u{2028}separator");
assert!(
output.contains("\\u2028"),
"Line separator U+2028 should be escaped: {}",
output
);
}
#[test]
fn test_escape_paragraph_separator_u2029() {
// U+2029 - Paragraph separator (generators SHOULD escape)
let mut output = String::new();
write_escaped_string(&mut output, "paragraph\u{2029}separator");
assert!(
output.contains("\\u2029"),
"Paragraph separator U+2029 should be escaped: {}",
output
);
}
#[test]
fn test_parse_unescaped_line_separator() {
// JSON5 parsers should accept unescaped U+2028
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
// Construct JSON5 with literal U+2028
let json5 = "{ value: \"line\u{2028}separator\" }";
let result: Data = from_str(json5).unwrap();
assert_eq!(result.value, "line\u{2028}separator");
}
#[test]
fn test_parse_unescaped_paragraph_separator() {
// JSON5 parsers should accept unescaped U+2029
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let json5 = "{ value: \"paragraph\u{2029}separator\" }";
let result: Data = from_str(json5).unwrap();
assert_eq!(result.value, "paragraph\u{2029}separator");
}
// =====================================================================
// Section 5.1: Hex Escapes (\xHH)
// =====================================================================
#[test]
fn test_parse_hex_escape_lowercase() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\x48\x65\x6c\x6c\x6f" }"#).unwrap();
assert_eq!(result.value, "Hello");
}
#[test]
fn test_parse_hex_escape_uppercase() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\x48\x45\x4C\x4C\x4F" }"#).unwrap();
assert_eq!(result.value, "HELLO");
}
// =====================================================================
// Section 5.1: Unicode Escapes (\uHHHH)
// =====================================================================
#[test]
fn test_parse_unicode_escape_bmp() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
// Basic Multilingual Plane characters
let result: Data = from_str(r#"{ value: "\u0048\u0065\u006C\u006C\u006F" }"#).unwrap();
assert_eq!(result.value, "Hello");
}
#[test]
fn test_parse_unicode_escape_surrogate_pair() {
// Characters outside BMP use surrogate pairs
// 🎼 (U+1F3BC) = \uD83C\uDFBC
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\uD83C\uDFBC" }"#).unwrap();
assert_eq!(result.value, "🎼");
}
#[test]
fn test_parse_unicode_escape_emoji() {
// 😀 (U+1F600) = \uD83D\uDE00
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\uD83D\uDE00" }"#).unwrap();
assert_eq!(result.value, "😀");
}
// =====================================================================
// Section 6: Numbers
// https://spec.json5.org/#numbers
// =====================================================================
#[test]
fn test_number_hexadecimal_lowercase() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
let result: Data = from_str(r#"{ value: 0xdecaf }"#).unwrap();
assert_eq!(result.value, 0xdecaf);
}
#[test]
fn test_number_hexadecimal_uppercase() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
let result: Data = from_str(r#"{ value: 0XDECAF }"#).unwrap();
assert_eq!(result.value, 0xDECAF);
}
#[test]
fn test_number_hexadecimal_mixed_case() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
let result: Data = from_str(r#"{ value: 0xDeAdBeEf }"#).unwrap();
assert_eq!(result.value, 0xDEADBEEF);
}
#[test]
fn test_number_negative_hexadecimal() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
let result: Data = from_str(r#"{ value: -0xC0FFEE }"#).unwrap();
assert_eq!(result.value, -0xC0FFEE);
}
#[test]
fn test_number_leading_decimal_point() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: .8675309 }"#).unwrap();
assert!((result.value - 0.8675309).abs() < 1e-10);
}
#[test]
fn test_number_trailing_decimal_point() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: 8675309. }"#).unwrap();
assert_eq!(result.value, 8675309.0);
}
#[test]
fn test_number_positive_infinity() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: Infinity }"#).unwrap();
assert!(result.value.is_infinite() && result.value.is_sign_positive());
}
#[test]
fn test_number_positive_infinity_with_plus() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: +Infinity }"#).unwrap();
assert!(result.value.is_infinite() && result.value.is_sign_positive());
}
#[test]
fn test_number_negative_infinity() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: -Infinity }"#).unwrap();
assert!(result.value.is_infinite() && result.value.is_sign_negative());
}
#[test]
fn test_number_nan() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: NaN }"#).unwrap();
assert!(result.value.is_nan());
}
#[test]
fn test_number_positive_sign() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str(r#"{ value: +42 }"#).unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_number_exponent_uppercase_e() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: 1E10 }"#).unwrap();
assert_eq!(result.value, 1e10);
}
#[test]
fn test_number_exponent_with_positive_sign() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
let result: Data = from_str(r#"{ value: 1e+10 }"#).unwrap();
assert_eq!(result.value, 1e10);
}
// =====================================================================
// Section 3: Objects
// https://spec.json5.org/#objects
// =====================================================================
#[test]
fn test_object_unquoted_identifier_keys() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
width: i32,
height: i32,
}
let result: Data = from_str(r#"{ width: 1920, height: 1080 }"#).unwrap();
assert_eq!(result.width, 1920);
assert_eq!(result.height, 1080);
}
#[test]
fn test_object_single_trailing_comma() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
width: i32,
height: i32,
}
let result: Data = from_str(r#"{ width: 1920, height: 1080, }"#).unwrap();
assert_eq!(result.width, 1920);
assert_eq!(result.height, 1080);
}
#[test]
fn test_object_single_quoted_key() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
#[serde(rename = "aspect-ratio")]
aspect_ratio: String,
}
let result: Data = from_str(r#"{ 'aspect-ratio': '16:9' }"#).unwrap();
assert_eq!(result.aspect_ratio, "16:9");
}
#[test]
fn test_object_double_quoted_key() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
#[serde(rename = "aspect-ratio")]
aspect_ratio: String,
}
let result: Data = from_str(r#"{ "aspect-ratio": "16:9" }"#).unwrap();
assert_eq!(result.aspect_ratio, "16:9");
}
#[test]
fn test_object_nested() {
#[derive(Debug, Deserialize, PartialEq)]
struct Inner {
width: i32,
height: i32,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Outer {
image: Inner,
}
let result: Outer = from_str(
r#"{
image: {
width: 1920,
height: 1080,
}
}"#,
)
.unwrap();
assert_eq!(result.image.width, 1920);
assert_eq!(result.image.height, 1080);
}
// =====================================================================
// Section 4: Arrays
// https://spec.json5.org/#arrays
// =====================================================================
#[test]
fn test_array_trailing_comma() {
let result: Vec<i32> = from_str(r#"[1, 2, 3,]"#).unwrap();
assert_eq!(result, vec![1, 2, 3]);
}
#[test]
fn test_array_mixed_types() {
// "There is no requirement that the values in an array be of the same type."
// Use serde_json::Value which can hold any type
let result: Vec<serde_json::Value> = from_str(r#"[1, true, 'three',]"#).unwrap();
assert_eq!(result.len(), 3);
assert_eq!(result[0], 1);
assert_eq!(result[1], true);
assert_eq!(result[2], "three");
}
#[test]
fn test_array_nested_with_trailing_commas() {
let result: Vec<Vec<i32>> = from_str(r#"[[1, 2,], [3, 4,],]"#).unwrap();
assert_eq!(result, vec![vec![1, 2], vec![3, 4]]);
}
// =====================================================================
// Section 5: Strings
// https://spec.json5.org/#strings
// =====================================================================
#[test]
fn test_string_single_quoted() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: 'single quoted' }"#).unwrap();
assert_eq!(result.value, "single quoted");
}
#[test]
fn test_string_single_quoted_with_double_quotes() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: 'I can use "double quotes" here' }"#).unwrap();
assert_eq!(result.value, r#"I can use "double quotes" here"#);
}
#[test]
fn test_string_multiline_escaped() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
// Line continuation - backslash followed by newline is removed
let result: Data = from_str("{ value: 'Look, Mom! \\\nNo newlines!' }").unwrap();
assert_eq!(result.value, "Look, Mom! No newlines!");
}
// =====================================================================
// Section 7: Comments
// https://spec.json5.org/#comments
// =====================================================================
#[test]
fn test_comment_single_line() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str(
r#"{
// This is a single line comment.
value: 42
}"#,
)
.unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_comment_multi_line() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str(
r#"{
/* This is a multi-
line comment. */
value: 42
}"#,
)
.unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_comment_inline() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
a: i32,
b: i32,
}
let result: Data = from_str(
r#"{
a: 1, // inline comment
b: 2 /* another */
}"#,
)
.unwrap();
assert_eq!(result.a, 1);
assert_eq!(result.b, 2);
}
// =====================================================================
// Section 8: White Space
// https://spec.json5.org/#white-space
// =====================================================================
#[test]
fn test_whitespace_horizontal_tab() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str("{\tvalue:\t42\t}").unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_whitespace_vertical_tab() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str("{\u{000B}value:\u{000B}42\u{000B}}").unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_whitespace_form_feed() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
let result: Data = from_str("{\u{000C}value:\u{000C}42\u{000C}}").unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_whitespace_non_breaking_space() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
// U+00A0 Non-breaking space
let result: Data = from_str("{\u{00A0}value:\u{00A0}42\u{00A0}}").unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_whitespace_bom() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
// U+FEFF Byte order mark
let result: Data = from_str("\u{FEFF}{ value: 42 }").unwrap();
assert_eq!(result.value, 42);
}
// =====================================================================
// Section 1.2: Short Example from Spec
// Complete example that uses many JSON5 features
// =====================================================================
#[test]
fn test_spec_short_example() {
// This is the example from the JSON5 spec Section 1.2
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Example {
unquoted: String,
single_quotes: String,
line_breaks: String,
hexadecimal: i64,
leading_decimal_point: f64,
and_trailing: f64,
positive_sign: i32,
trailing_comma: String,
and_in: Vec<String>,
#[serde(rename = "backwardsCompatible")]
backwards_compatible: String,
}
let result: Example = from_str(
r#"{
// comments
unquoted: 'and you can quote me on that',
singleQuotes: 'I can use "double quotes" here',
lineBreaks: "Look, Mom! \
No \\n's!",
hexadecimal: 0xdecaf,
leadingDecimalPoint: .8675309, andTrailing: 8675309.,
positiveSign: +1,
trailingComma: 'in objects', andIn: ['arrays',],
"backwardsCompatible": "with JSON",
}"#,
)
.unwrap();
assert_eq!(result.unquoted, "and you can quote me on that");
assert_eq!(result.single_quotes, r#"I can use "double quotes" here"#);
assert_eq!(result.line_breaks, "Look, Mom! No \\n's!");
assert_eq!(result.hexadecimal, 0xdecaf);
assert!((result.leading_decimal_point - 0.8675309).abs() < 1e-10);
assert_eq!(result.and_trailing, 8675309.0);
assert_eq!(result.positive_sign, 1);
assert_eq!(result.trailing_comma, "in objects");
assert_eq!(result.and_in, vec!["arrays"]);
assert_eq!(result.backwards_compatible, "with JSON");
}
// =====================================================================
// Roundtrip Tests - Serialization then Parsing
// =====================================================================
#[test]
fn test_roundtrip_all_escapes() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: String,
}
// String with all escapable characters
let original = Data {
value: "\"\\\u{0008}\u{000C}\n\r\t\u{000B}\u{2028}\u{2029}".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_roundtrip_special_numbers() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
infinity: f64,
neg_infinity: f64,
nan: f64,
large: f64,
small: f64,
}
let original = Data {
infinity: f64::INFINITY,
neg_infinity: f64::NEG_INFINITY,
nan: f64::NAN,
large: 1e47,
small: 1e-47,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert!(
deserialized.infinity.is_infinite() && deserialized.infinity.is_sign_positive()
);
assert!(
deserialized.neg_infinity.is_infinite()
&& deserialized.neg_infinity.is_sign_negative()
);
assert!(deserialized.nan.is_nan());
assert_eq!(deserialized.large, 1e47);
assert_eq!(deserialized.small, 1e-47);
}
#[test]
fn test_roundtrip_keys_with_all_escapes() {
// Test that keys are properly escaped and can be parsed back
let mut map = std::collections::HashMap::new();
map.insert("key\"quote".to_string(), 1);
map.insert("key\\backslash".to_string(), 2);
map.insert("key\nline".to_string(), 3);
map.insert("key\ttab".to_string(), 4);
map.insert("key\u{0008}backspace".to_string(), 5);
map.insert("key\u{000C}formfeed".to_string(), 6);
map.insert("key\u{000B}vtab".to_string(), 7);
map.insert("key\u{2028}lsep".to_string(), 8);
map.insert("key\u{2029}psep".to_string(), 9);
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized.get("key\"quote"), Some(&1));
assert_eq!(deserialized.get("key\\backslash"), Some(&2));
assert_eq!(deserialized.get("key\nline"), Some(&3));
assert_eq!(deserialized.get("key\ttab"), Some(&4));
assert_eq!(deserialized.get("key\u{0008}backspace"), Some(&5));
assert_eq!(deserialized.get("key\u{000C}formfeed"), Some(&6));
assert_eq!(deserialized.get("key\u{000B}vtab"), Some(&7));
assert_eq!(deserialized.get("key\u{2028}lsep"), Some(&8));
assert_eq!(deserialized.get("key\u{2029}psep"), Some(&9));
}
#[test]
fn test_generator_produces_valid_json5() {
// Section 11: "A JSON5 generator produces JSON5 text. The resulting
// text must strictly conform to the JSON5 grammar."
#[derive(Debug, Serialize)]
struct Complex {
string: String,
number: f64,
boolean: bool,
null_val: Option<i32>,
array: Vec<i32>,
nested: Nested,
}
#[derive(Debug, Serialize)]
struct Nested {
value: f64,
}
let data = Complex {
string: "hello\nworld\t\"test\"\u{2028}\u{2029}".to_string(),
number: 1e47,
boolean: true,
null_val: None,
array: vec![1, 2, 3],
nested: Nested {
value: f64::INFINITY,
},
};
let serialized = to_vec_pretty_sorted(&data).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// If it parses, it's valid JSON5
let _: serde_json::Value = from_str(&json_str).unwrap();
}
}
// =========================================================================
// DISGUSTING EDGE CASE TESTS
// =========================================================================
//
// These tests push the JSON5 spec to its absolute limits. They are designed
// to break parsers and serializers that don't strictly follow the spec.
// Based on https://spec.json5.org/
mod json5_torture_tests {
use super::*;
// =====================================================================
// String Escape Torture
// =====================================================================
#[test]
fn test_every_escape_sequence_in_one_string() {
// Every escape from Table 1 plus hex and unicode escapes
let json5 = r#"{
value: "\"\\\b\f\n\r\t\v\0\x41\u0042"
}"#;
#[derive(Debug, Deserialize)]
struct Data {
value: String,
}
let result: Data = from_str(json5).unwrap();
// " \ backspace formfeed newline carriage-return tab vertical-tab null A B
assert_eq!(result.value, "\"\\\u{0008}\u{000C}\n\r\t\u{000B}\u{0000}AB");
}
#[test]
fn test_string_with_all_control_characters() {
// Roundtrip all ASCII control characters (U+0000 to U+001F)
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: String,
}
let mut control_chars = String::new();
for i in 0u8..32 {
control_chars.push(i as char);
}
// Add DEL (U+007F) too
control_chars.push('\u{007F}');
let original = Data {
value: control_chars,
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_surrogate_pair_boundaries() {
// Test the exact boundaries of surrogate pairs
// High surrogates: U+D800 to U+DBFF
// Low surrogates: U+DC00 to U+DFFF
// First valid surrogate pair encodes U+10000
#[derive(Debug, Deserialize)]
struct Data {
value: String,
}
// 𐀀 (U+10000) = \uD800\uDC00
let result: Data = from_str(r#"{ value: "\uD800\uDC00" }"#).unwrap();
assert_eq!(result.value, "\u{10000}");
// 🀀 (U+1F000) = \uD83C\uDC00
let result: Data = from_str(r#"{ value: "\uD83C\uDC00" }"#).unwrap();
assert_eq!(result.value, "🀀");
// Last valid BMP character before surrogates: U+D7FF
let result: Data = from_str(r#"{ value: "\uD7FF" }"#).unwrap();
assert_eq!(result.value, "\u{D7FF}");
// First character after surrogates: U+E000
let result: Data = from_str(r#"{ value: "\uE000" }"#).unwrap();
assert_eq!(result.value, "\u{E000}");
}
#[test]
fn test_maximum_unicode_code_point() {
// U+10FFFF is the maximum valid Unicode code point
// Encoded as surrogate pair: \uDBFF\uDFFF
#[derive(Debug, Deserialize)]
struct Data {
value: String,
}
let result: Data = from_str(r#"{ value: "\uDBFF\uDFFF" }"#).unwrap();
assert_eq!(result.value, "\u{10FFFF}");
}
#[test]
fn test_string_line_continuation_all_terminators() {
// Per Table 2: line continuation can use LF, CR, CRLF, U+2028, U+2029
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
// LF continuation
let result: Data = from_str("{ value: 'hello\\\nworld' }").unwrap();
assert_eq!(result.value, "helloworld");
// CR continuation
let result: Data = from_str("{ value: 'hello\\\rworld' }").unwrap();
assert_eq!(result.value, "helloworld");
// CRLF continuation
let result: Data = from_str("{ value: 'hello\\\r\nworld' }").unwrap();
assert_eq!(result.value, "helloworld");
// U+2028 line separator continuation
let result: Data = from_str("{ value: 'hello\\\u{2028}world' }").unwrap();
assert_eq!(result.value, "helloworld");
// U+2029 paragraph separator continuation
let result: Data = from_str("{ value: 'hello\\\u{2029}world' }").unwrap();
assert_eq!(result.value, "helloworld");
}
#[test]
fn test_identity_escapes() {
// Per spec Section 5.1: "If any other character follows a reverse solidus,
// except for the decimal digits 1 through 9, that character will be
// included in the string, but the reverse solidus will not."
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: String,
}
// \A\C\/\D\C should become AC/DC
let result: Data = from_str(r#"{ value: '\A\C\/\D\C' }"#).unwrap();
assert_eq!(result.value, "AC/DC");
// Random escaped letters
let result: Data = from_str(r#"{ value: '\z\y\x41' }"#).unwrap();
assert_eq!(result.value, "zyA"); // \x41 is hex escape for 'A'
}
// =====================================================================
// Number Torture
// =====================================================================
#[test]
fn test_all_special_number_forms() {
#[derive(Debug, Deserialize)]
struct Data {
a: f64,
b: f64,
c: f64,
d: f64,
e: f64,
f: f64,
g: f64,
h: f64,
i: f64,
j: f64,
}
let result: Data = from_str(
r#"{
a: Infinity,
b: -Infinity,
c: +Infinity,
d: NaN,
e: +NaN,
f: -NaN,
g: .5,
h: 5.,
i: +42,
j: 0xDEADBEEF
}"#,
)
.unwrap();
assert!(result.a.is_infinite() && result.a.is_sign_positive());
assert!(result.b.is_infinite() && result.b.is_sign_negative());
assert!(result.c.is_infinite() && result.c.is_sign_positive());
assert!(result.d.is_nan());
assert!(result.e.is_nan());
assert!(result.f.is_nan());
assert_eq!(result.g, 0.5);
assert_eq!(result.h, 5.0);
assert_eq!(result.i, 42.0);
assert_eq!(result.j, 0xDEADBEEF_u64 as f64);
}
#[test]
fn test_hexadecimal_edge_cases() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i64,
}
// Lowercase x
let result: Data = from_str(r#"{ value: 0x0 }"#).unwrap();
assert_eq!(result.value, 0);
// Uppercase X
let result: Data = from_str(r#"{ value: 0X0 }"#).unwrap();
assert_eq!(result.value, 0);
// All hex digits lowercase
let result: Data = from_str(r#"{ value: 0xabcdef }"#).unwrap();
assert_eq!(result.value, 0xABCDEF);
// All hex digits uppercase
let result: Data = from_str(r#"{ value: 0xABCDEF }"#).unwrap();
assert_eq!(result.value, 0xABCDEF);
// Positive hex
let result: Data = from_str(r#"{ value: +0xFF }"#).unwrap();
assert_eq!(result.value, 255);
// Negative hex
let result: Data = from_str(r#"{ value: -0xFF }"#).unwrap();
assert_eq!(result.value, -255);
}
#[test]
fn test_exponent_edge_cases() {
#[derive(Debug, Deserialize)]
struct Data {
value: f64,
}
// Uppercase E
let result: Data = from_str(r#"{ value: 1E10 }"#).unwrap();
assert_eq!(result.value, 1e10);
// Lowercase e
let result: Data = from_str(r#"{ value: 1e10 }"#).unwrap();
assert_eq!(result.value, 1e10);
// Explicit positive exponent
let result: Data = from_str(r#"{ value: 1e+10 }"#).unwrap();
assert_eq!(result.value, 1e10);
// Negative exponent
let result: Data = from_str(r#"{ value: 1e-10 }"#).unwrap();
assert_eq!(result.value, 1e-10);
// Zero exponent
let result: Data = from_str(r#"{ value: 1e0 }"#).unwrap();
assert_eq!(result.value, 1.0);
// Leading decimal with exponent
let result: Data = from_str(r#"{ value: .5e10 }"#).unwrap();
assert_eq!(result.value, 0.5e10);
// Trailing decimal with exponent
let result: Data = from_str(r#"{ value: 5.e10 }"#).unwrap();
assert_eq!(result.value, 5e10);
}
// =====================================================================
// Identifier/Key Torture
// =====================================================================
#[test]
fn test_identifier_edge_cases() {
// ECMAScript 5.1 IdentifierName rules
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
#[serde(rename = "$")]
dollar: i32,
#[serde(rename = "_")]
underscore: i32,
#[serde(rename = "$_")]
dollar_underscore: i32,
#[serde(rename = "_$")]
underscore_dollar: i32,
#[serde(rename = "$$$$")]
many_dollars: i32,
#[serde(rename = "____")]
many_underscores: i32,
#[serde(rename = "a1234567890")]
letter_digits: i32,
}
let result: Data = from_str(
r#"{
$: 1,
_: 2,
$_: 3,
_$: 4,
$$$$: 5,
____: 6,
a1234567890: 7
}"#,
)
.unwrap();
assert_eq!(result.dollar, 1);
assert_eq!(result.underscore, 2);
assert_eq!(result.dollar_underscore, 3);
assert_eq!(result.underscore_dollar, 4);
assert_eq!(result.many_dollars, 5);
assert_eq!(result.many_underscores, 6);
assert_eq!(result.letter_digits, 7);
}
#[test]
fn test_reserved_words_as_keys() {
// All ES5 reserved words should work as unquoted keys
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Reserved {
r#break: i32,
r#case: i32,
r#catch: i32,
r#continue: i32,
r#debugger: i32,
r#default: i32,
r#delete: i32,
r#do: i32,
r#else: i32,
r#finally: i32,
r#for: i32,
r#function: i32,
r#if: i32,
r#in: i32,
r#instanceof: i32,
r#new: i32,
r#return: i32,
r#switch: i32,
this: i32,
r#throw: i32,
r#try: i32,
r#typeof: i32,
var: i32,
void: i32,
r#while: i32,
with: i32,
class: i32,
r#const: i32,
r#enum: i32,
export: i32,
extends: i32,
import: i32,
#[serde(rename = "super")]
super_: i32,
null: i32,
r#true: i32,
r#false: i32,
}
let json5 = r#"{
break: 1, case: 2, catch: 3, continue: 4, debugger: 5,
default: 6, delete: 7, do: 8, else: 9, finally: 10,
for: 11, function: 12, if: 13, in: 14, instanceof: 15,
new: 16, return: 17, switch: 18, this: 19, throw: 20,
try: 21, typeof: 22, var: 23, void: 24, while: 25,
with: 26, class: 27, const: 28, enum: 29, export: 30,
extends: 31, import: 32, super: 33, null: 34, true: 35, false: 36
}"#;
let _: Reserved = from_str(json5).unwrap();
}
#[test]
fn test_keys_that_look_like_numbers() {
// Keys that start with digits must be quoted
let mut map = std::collections::HashMap::new();
map.insert("123".to_string(), 1);
map.insert("0xFF".to_string(), 2);
map.insert("3.14".to_string(), 3);
map.insert("1e10".to_string(), 4);
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// All should be quoted since they start with digits
assert!(json_str.contains("\"123\""));
assert!(json_str.contains("\"0xFF\""));
assert!(json_str.contains("\"3.14\""));
assert!(json_str.contains("\"1e10\""));
// Roundtrip
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized.get("123"), Some(&1));
}
// =====================================================================
// Whitespace Torture
// =====================================================================
#[test]
fn test_all_whitespace_characters() {
// Per Table 3 in spec - all valid whitespace
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
// Build a string with all whitespace types
let whitespace =
"\u{0009}\u{000A}\u{000B}\u{000C}\u{000D}\u{0020}\u{00A0}\u{2028}\u{2029}\u{FEFF}";
let json5 = format!(
"{}{{{}value{}:{}42{}}}",
whitespace, whitespace, whitespace, whitespace, whitespace
);
let result: Data = from_str(&json5).unwrap();
assert_eq!(result.value, 42);
}
#[test]
fn test_whitespace_between_every_token() {
#[derive(Debug, Deserialize)]
struct Data {
arr: Vec<i32>,
}
// Whitespace can appear before and after any token
let json5 = " { arr : [ 1 , 2 , 3 , ] , } ";
let result: Data = from_str(json5).unwrap();
assert_eq!(result.arr, vec![1, 2, 3]);
}
// =====================================================================
// Comment Torture
// =====================================================================
#[test]
fn test_comments_everywhere() {
#[derive(Debug, Deserialize)]
struct Data {
a: i32,
b: i32,
}
let json5 = r#"
// comment before object
{ // comment after opening brace
// comment before key
a // comment after key
: // comment after colon
1 // comment after value
, // comment after comma
/* multi
line
comment */
b: /* inline */ 2 /* another */
, // trailing comma comment
} // comment after closing brace
// comment at end
"#;
let result: Data = from_str(json5).unwrap();
assert_eq!(result.a, 1);
assert_eq!(result.b, 2);
}
#[test]
fn test_comment_with_special_content() {
#[derive(Debug, Deserialize, PartialEq)]
struct Data {
value: i32,
}
// Comments can contain anything except */ for multi-line
let json5 = r#"{
// Comment with "quotes" and 'apostrophes' and \escapes
/* Multi-line with
{ "fake": "json" }
and // nested single-line looking things
*/
value: 42
}"#;
let result: Data = from_str(json5).unwrap();
assert_eq!(result.value, 42);
}
// =====================================================================
// Nesting Torture
// =====================================================================
#[test]
fn test_deeply_nested_objects() {
// 20 levels of nesting
let mut json5 = String::new();
for _ in 0..20 {
json5.push_str("{ a: ");
}
json5.push_str("42");
for _ in 0..20 {
json5.push_str(" }");
}
let result: serde_json::Value = from_str(&json5).unwrap();
// Navigate to the innermost value
let mut current = &result;
for _ in 0..20 {
current = &current["a"];
}
assert_eq!(current.as_i64().unwrap(), 42);
}
#[test]
fn test_deeply_nested_arrays() {
// 20 levels of array nesting
let mut json5 = String::new();
for _ in 0..20 {
json5.push('[');
}
json5.push_str("42");
for _ in 0..20 {
json5.push(']');
}
let result: serde_json::Value = from_str(&json5).unwrap();
// Navigate to the innermost value
let mut current = &result;
for _ in 0..20 {
current = &current[0];
}
assert_eq!(current.as_i64().unwrap(), 42);
}
// =====================================================================
// Trailing Comma Torture
// =====================================================================
#[test]
fn test_trailing_commas_everywhere() {
#[derive(Debug, Deserialize)]
struct Inner {
x: i32,
}
#[derive(Debug, Deserialize)]
struct Data {
obj: Inner,
arr: Vec<i32>,
}
let json5 = r#"{
obj: {
x: 1,
},
arr: [
1,
2,
3,
],
}"#;
let result: Data = from_str(json5).unwrap();
assert_eq!(result.obj.x, 1);
assert_eq!(result.arr, vec![1, 2, 3]);
}
// =====================================================================
// Mixed Quote Style Torture
// =====================================================================
#[test]
fn test_mixed_quote_styles() {
#[derive(Debug, Deserialize)]
struct Data {
#[serde(rename = "double-key")]
double_key: String,
#[serde(rename = "single-key")]
single_key: String,
}
let json5 = r#"{
"double-key": 'single value',
'single-key': "double value"
}"#;
let result: Data = from_str(json5).unwrap();
assert_eq!(result.double_key, "single value");
assert_eq!(result.single_key, "double value");
}
// =====================================================================
// Empty/Minimal Structure Torture
// =====================================================================
#[test]
fn test_minimal_valid_json5() {
// Smallest valid JSON5 values
let _: serde_json::Value = from_str("{}").unwrap();
let _: serde_json::Value = from_str("[]").unwrap();
let _: serde_json::Value = from_str("null").unwrap();
let _: serde_json::Value = from_str("true").unwrap();
let _: serde_json::Value = from_str("false").unwrap();
let _: serde_json::Value = from_str("0").unwrap();
let _: serde_json::Value = from_str("\"\"").unwrap();
let _: serde_json::Value = from_str("''").unwrap();
}
#[test]
fn test_object_with_empty_string_key() {
let mut map = std::collections::HashMap::new();
map.insert("".to_string(), 42);
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
// Empty key must be quoted
assert!(json_str.contains("\"\""));
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
assert_eq!(deserialized.get(""), Some(&42));
}
// =====================================================================
// Unicode Torture
// =====================================================================
#[test]
fn test_unicode_identifiers() {
// ECMAScript 5.1 allows Unicode letters in identifiers
// Note: this depends on the json5 crate's support
#[derive(Debug, Deserialize)]
struct Data {
#[serde(rename = "café")]
cafe: i32,
#[serde(rename = "naïve")]
naive: i32,
}
// These may or may not work depending on json5 crate implementation
// Using quoted keys to be safe
let json5 = r#"{ "café": 1, "naïve": 2 }"#;
let result: Data = from_str(json5).unwrap();
assert_eq!(result.cafe, 1);
assert_eq!(result.naive, 2);
}
#[test]
fn test_zero_width_characters_in_strings() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: String,
}
// Zero-width joiner, zero-width non-joiner, zero-width space
let original = Data {
value: "a\u{200D}b\u{200C}c\u{200B}d".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_combining_characters() {
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: String,
}
// é as e + combining acute accent
let original = Data {
value: "e\u{0301}".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
#[test]
fn test_bidi_characters() {
// Bidirectional text control characters
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct Data {
value: String,
}
// LRM, RLM, LRE, RLE, PDF, LRO, RLO
let original = Data {
value: "\u{200E}\u{200F}\u{202A}\u{202B}\u{202C}\u{202D}\u{202E}".to_string(),
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert_eq!(deserialized.value, original.value);
}
// =====================================================================
// Stress Test - Everything Combined
// =====================================================================
#[test]
fn test_ultimate_torture() {
// Combine as many edge cases as possible in one test
// We test parsing succeeds and verify key parts
let json5 = r#"
// Ultimate torture test
{
/* Multi-line
comment */
$weird_key$: 'single \' quote',
"key with spaces": "double \" quote",
'key-with-dashes': [
1,
.5,
5.,
+42,
-0,
0xFF,
1e+10,
Infinity,
-Infinity,
NaN,
null,
true,
false,
"",
'',
"\n\r\t\b\f\v\0",
"\u0000\u001F\u007F",
"\uD83D\uDE00", // 😀
],
nested: {
deeply: {
nested: {
value: 42,
},
},
},
empty_obj: {},
empty_arr: [],
trailing: 'comma',
}
"#;
// Test that it parses at all - this validates the json5 crate handles all features
let result: serde_json::Value = from_str(json5).unwrap();
// Verify key structural parts (avoiding NaN/Infinity which serde_json::Value can't hold)
assert_eq!(result["$weird_key$"], "single ' quote");
assert_eq!(result["key with spaces"], "double \" quote");
// Check array length
assert_eq!(result["key-with-dashes"].as_array().unwrap().len(), 18);
// Check some numeric values that serde_json can hold
assert_eq!(result["key-with-dashes"][0], 1.0);
assert_eq!(result["key-with-dashes"][1], 0.5);
assert_eq!(result["key-with-dashes"][5], 255.0); // 0xFF
// Check nested
assert_eq!(result["nested"]["deeply"]["nested"]["value"], 42);
// Check empty collections
assert!(result["empty_obj"].as_object().unwrap().is_empty());
assert!(result["empty_arr"].as_array().unwrap().is_empty());
assert_eq!(result["trailing"], "comma");
// Now test specific special floats with typed deserialization
#[derive(Debug, Deserialize)]
struct SpecialFloats {
infinity: f64,
neg_infinity: f64,
nan: f64,
}
let special: SpecialFloats = from_str(
r#"{
infinity: Infinity,
neg_infinity: -Infinity,
nan: NaN
}"#,
)
.unwrap();
assert!(special.infinity.is_infinite() && special.infinity.is_sign_positive());
assert!(special.neg_infinity.is_infinite() && special.neg_infinity.is_sign_negative());
assert!(special.nan.is_nan());
}
// =====================================================================
// Roundtrip Stress Tests
// =====================================================================
#[test]
fn test_roundtrip_pathological_keys() {
let mut map = std::collections::HashMap::new();
// Keys that push every edge case
map.insert("".to_string(), 1); // empty
map.insert(" ".to_string(), 2); // space
map.insert(" ".to_string(), 3); // multiple spaces
map.insert("\t".to_string(), 4); // tab
map.insert("\n".to_string(), 5); // newline
map.insert("\"".to_string(), 6); // quote
map.insert("\\".to_string(), 7); // backslash
map.insert("'".to_string(), 8); // apostrophe
map.insert("\u{0000}".to_string(), 9); // null
map.insert("\u{001F}".to_string(), 10); // unit separator
map.insert("\u{007F}".to_string(), 11); // delete
map.insert("\u{2028}".to_string(), 12); // line separator
map.insert("\u{2029}".to_string(), 13); // paragraph separator
map.insert("key\"with\"quotes".to_string(), 14);
map.insert("key\\with\\backslashes".to_string(), 15);
map.insert("key\nwith\nnewlines".to_string(), 16);
map.insert("αβγδ".to_string(), 17); // Greek
map.insert("日本語".to_string(), 18); // Japanese
map.insert("🎉🎊🎁".to_string(), 19); // Emoji
let serialized = to_vec_pretty_sorted(&map).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: std::collections::HashMap<String, i32> = from_str(&json_str).unwrap();
for (key, expected_value) in &map {
assert_eq!(
deserialized.get(key),
Some(expected_value),
"Failed for key: {:?}",
key
);
}
}
#[test]
fn test_roundtrip_pathological_values() {
#[derive(Debug, Serialize, Deserialize)]
struct Data {
a: f64,
b: f64,
c: f64,
d: f64,
e: f64,
f: f64,
g: f64,
h: f64,
i: String,
}
let original = Data {
a: f64::INFINITY,
b: f64::NEG_INFINITY,
c: f64::NAN,
d: f64::MAX,
e: f64::MIN,
f: f64::MIN_POSITIVE,
g: f64::EPSILON,
h: 0.0,
i: (0u8..=127).map(|b| b as char).collect::<String>(), // All ASCII
};
let serialized = to_vec_pretty_sorted(&original).unwrap();
let json_str = String::from_utf8(serialized).unwrap();
let deserialized: Data = from_str(&json_str).unwrap();
assert!(deserialized.a.is_infinite() && deserialized.a.is_sign_positive());
assert!(deserialized.b.is_infinite() && deserialized.b.is_sign_negative());
assert!(deserialized.c.is_nan());
assert_eq!(deserialized.d, f64::MAX);
assert_eq!(deserialized.e, f64::MIN);
assert_eq!(deserialized.f, f64::MIN_POSITIVE);
assert_eq!(deserialized.g, f64::EPSILON);
assert_eq!(deserialized.h, 0.0);
assert_eq!(deserialized.i, original.i);
}
}
}