rojo/src/json.rs
2026-01-27 07:13:02 -06:00

1126 lines
34 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.
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
enum Json5Value {
Null,
Bool(bool),
Number(f64),
String(String),
Array(Vec<Json5Value>),
Object(std::collections::BTreeMap<String, Json5Value>),
}
/// 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 {
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.
///
/// # Errors
///
/// Returns an error if the value cannot be serialized to JSON5.
pub fn to_vec_pretty_sorted<T: Serialize>(value: &T) -> anyhow::Result<Vec<u8>> {
// Serialize to json5 string (preserves NaN/Infinity)
let json5_str = json5::to_string(value).context("Failed to serialize to JSON5")?;
// Parse into Json5Value which uses BTreeMap (sorted keys)
let sorted: Json5Value =
json5::from_str(&json5_str).context("Failed to parse JSON5 for sorting")?;
// Serialize back to json5 (now sorted)
let output = json5::to_string(&sorted).context("Failed to serialize sorted JSON5")?;
Ok(output.into_bytes())
}
#[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);
}
}