use base64::Engine;
use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SpecialValue {
#[serde(rename = "null")]
Null,
#[serde(rename = "undefined")]
Undefined,
#[serde(rename = "NaN")]
NaN,
#[serde(rename = "Infinity")]
Infinity,
#[serde(rename = "-Infinity")]
NegInfinity,
#[serde(rename = "-0")]
NegZero,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegExpValue {
pub p: String,
pub f: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ErrorValue {
pub m: String,
pub n: String,
pub s: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TypedArrayKind {
#[serde(rename = "i8")]
I8,
#[serde(rename = "ui8")]
U8,
#[serde(rename = "ui8c")]
U8Clamped,
#[serde(rename = "i16")]
I16,
#[serde(rename = "ui16")]
U16,
#[serde(rename = "i32")]
I32,
#[serde(rename = "ui32")]
U32,
#[serde(rename = "f32")]
F32,
#[serde(rename = "f64")]
F64,
#[serde(rename = "bi64")]
BI64,
#[serde(rename = "bui64")]
BUI64,
}
impl TypedArrayKind {
#[must_use]
pub fn bytes_per_element(self) -> usize {
match self {
Self::I8 | Self::U8 | Self::U8Clamped => 1,
Self::I16 | Self::U16 => 2,
Self::I32 | Self::U32 | Self::F32 => 4,
Self::F64 | Self::BI64 | Self::BUI64 => 8,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypedArrayValue {
pub b: Vec<u8>,
pub k: TypedArrayKind,
}
#[derive(Serialize, Deserialize)]
struct TypedArrayWire<'a> {
b: &'a str,
k: TypedArrayKind,
}
#[derive(Deserialize)]
struct TypedArrayWireOwned {
b: String,
k: TypedArrayKind,
}
impl Serialize for TypedArrayValue {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let encoded = base64::engine::general_purpose::STANDARD.encode(&self.b);
let wire = TypedArrayWire { b: &encoded, k: self.k };
wire.serialize(s)
}
}
impl<'de> Deserialize<'de> for TypedArrayValue {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let wire = TypedArrayWireOwned::deserialize(d)?;
let b = base64::engine::general_purpose::STANDARD
.decode(&wire.b)
.map_err(de::Error::custom)?;
Ok(Self { b, k: wire.k })
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArrayBufferValue {
pub b: Vec<u8>,
}
#[derive(Serialize, Deserialize)]
struct ArrayBufferWire<'a> {
b: &'a str,
}
#[derive(Deserialize)]
struct ArrayBufferWireOwned {
b: String,
}
impl Serialize for ArrayBufferValue {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let encoded = base64::engine::general_purpose::STANDARD.encode(&self.b);
let wire = ArrayBufferWire { b: &encoded };
wire.serialize(s)
}
}
impl<'de> Deserialize<'de> for ArrayBufferValue {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let wire = ArrayBufferWireOwned::deserialize(d)?;
let b = base64::engine::general_purpose::STANDARD
.decode(&wire.b)
.map_err(de::Error::custom)?;
Ok(Self { b })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PropertyEntry {
pub k: String,
pub v: SerializedValue,
}
#[derive(Debug, Clone, PartialEq)]
pub enum SerializedValue {
Bool(bool),
Number(f64),
Str(String),
Special(SpecialValue),
Date(String),
Url(String),
BigInt(String),
Error(ErrorValue),
RegExp(RegExpValue),
TypedArray(TypedArrayValue),
ArrayBuffer(ArrayBufferValue),
Array { id: u32, items: Vec<SerializedValue> },
Object { id: u32, entries: Vec<PropertyEntry> },
Handle(u32),
Reference(u32),
}
impl SerializedValue {
#[must_use]
pub fn boolean(b: bool) -> Self {
Self::Bool(b)
}
#[must_use]
pub fn number(n: f64) -> Self {
Self::Number(n)
}
#[must_use]
pub fn string(s: impl Into<String>) -> Self {
Self::Str(s.into())
}
#[must_use]
pub fn special(v: SpecialValue) -> Self {
Self::Special(v)
}
#[must_use]
pub fn null() -> Self {
Self::Special(SpecialValue::Null)
}
#[must_use]
pub fn undefined() -> Self {
Self::Special(SpecialValue::Undefined)
}
#[must_use]
pub fn from_f64(n: f64) -> Self {
if n.is_nan() {
return Self::Special(SpecialValue::NaN);
}
if n == f64::INFINITY {
return Self::Special(SpecialValue::Infinity);
}
if n == f64::NEG_INFINITY {
return Self::Special(SpecialValue::NegInfinity);
}
if n == 0.0 && n.to_bits() == (-0.0_f64).to_bits() {
return Self::Special(SpecialValue::NegZero);
}
Self::Number(n)
}
#[must_use]
pub fn date(iso: impl Into<String>) -> Self {
Self::Date(iso.into())
}
#[must_use]
pub fn url(url: impl Into<String>) -> Self {
Self::Url(url.into())
}
#[must_use]
pub fn bigint(decimal: impl Into<String>) -> Self {
Self::BigInt(decimal.into())
}
#[must_use]
pub fn regexp(pattern: impl Into<String>, flags: impl Into<String>) -> Self {
Self::RegExp(RegExpValue {
p: pattern.into(),
f: flags.into(),
})
}
#[must_use]
pub fn error(name: impl Into<String>, message: impl Into<String>, stack: impl Into<String>) -> Self {
Self::Error(ErrorValue {
n: name.into(),
m: message.into(),
s: stack.into(),
})
}
#[must_use]
pub fn typed_array(bytes: Vec<u8>, kind: TypedArrayKind) -> Self {
Self::TypedArray(TypedArrayValue { b: bytes, k: kind })
}
#[must_use]
pub fn array_buffer(bytes: Vec<u8>) -> Self {
Self::ArrayBuffer(ArrayBufferValue { b: bytes })
}
#[must_use]
pub fn array(id: u32, items: Vec<SerializedValue>) -> Self {
Self::Array { id, items }
}
#[must_use]
pub fn object(id: u32, entries: Vec<PropertyEntry>) -> Self {
Self::Object { id, entries }
}
#[must_use]
pub fn reference(id: u32) -> Self {
Self::Reference(id)
}
#[must_use]
pub fn handle(handle_index: u32) -> Self {
Self::Handle(handle_index)
}
#[must_use]
pub fn from_json(value: &serde_json::Value, ctx: &mut SerializationContext) -> Self {
match value {
serde_json::Value::Null => Self::null(),
serde_json::Value::Bool(b) => Self::Bool(*b),
serde_json::Value::Number(num) => num.as_f64().map_or_else(
|| Self::BigInt(num.to_string()),
Self::from_f64,
),
serde_json::Value::String(s) => Self::Str(s.clone()),
serde_json::Value::Array(items) => {
let id = ctx.alloc_id();
let wire_items = items.iter().map(|v| Self::from_json(v, ctx)).collect();
Self::Array { id, items: wire_items }
},
serde_json::Value::Object(map) => {
let id = ctx.alloc_id();
let entries = map
.iter()
.map(|(k, v)| PropertyEntry {
k: k.clone(),
v: Self::from_json(v, ctx),
})
.collect();
Self::Object { id, entries }
},
}
}
#[must_use]
pub fn to_json_like(&self) -> Option<serde_json::Value> {
match self {
Self::Bool(b) => Some(serde_json::Value::Bool(*b)),
Self::Number(n) => {
const F64_INT_MAX: f64 = 9_007_199_254_740_992.0;
if n.is_finite() && n.fract() == 0.0 && n.abs() <= F64_INT_MAX {
#[allow(clippy::cast_possible_truncation)]
let as_i64 = *n as i64;
Some(serde_json::Value::Number(as_i64.into()))
} else {
serde_json::Number::from_f64(*n).map(serde_json::Value::Number)
}
},
Self::Str(s) => Some(serde_json::Value::String(s.clone())),
Self::Special(SpecialValue::Null) => Some(serde_json::Value::Null),
Self::Array { items, .. } => {
let mut out = Vec::with_capacity(items.len());
for v in items {
out.push(v.to_json_like()?);
}
Some(serde_json::Value::Array(out))
},
Self::Object { entries, .. } => {
let mut map = serde_json::Map::with_capacity(entries.len());
for e in entries {
map.insert(e.k.clone(), e.v.to_json_like()?);
}
Some(serde_json::Value::Object(map))
},
Self::Special(_)
| Self::Date(_)
| Self::Url(_)
| Self::BigInt(_)
| Self::Error(_)
| Self::RegExp(_)
| Self::TypedArray(_)
| Self::ArrayBuffer(_)
| Self::Handle(_)
| Self::Reference(_) => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(b) => Some(*b),
_ => None,
}
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
match self {
Self::Str(s) => Some(s.as_str()),
_ => None,
}
}
#[must_use]
pub fn as_number(&self) -> Option<f64> {
match self {
Self::Number(n) => Some(*n),
_ => None,
}
}
#[must_use]
pub fn as_array(&self) -> Option<&[SerializedValue]> {
match self {
Self::Array { items, .. } => Some(items),
_ => None,
}
}
#[must_use]
pub fn as_string_lossy(&self) -> String {
match self {
Self::Str(s) | Self::Date(s) | Self::Url(s) | Self::BigInt(s) => s.clone(),
Self::Number(n) => n.to_string(),
Self::Bool(b) => b.to_string(),
Self::Special(SpecialValue::Null) => "null".to_string(),
Self::RegExp(RegExpValue { p, .. }) => p.clone(),
_ => String::new(),
}
}
}
impl Serialize for SerializedValue {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
Self::Bool(b) => s.serialize_bool(*b),
Self::Number(n) => s.serialize_f64(*n),
Self::Str(v) => s.serialize_str(v),
Self::Special(v) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("v", v)?;
m.end()
},
Self::Date(d) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("d", d)?;
m.end()
},
Self::Url(u) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("u", u)?;
m.end()
},
Self::BigInt(bi) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("bi", bi)?;
m.end()
},
Self::Error(e) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("e", e)?;
m.end()
},
Self::RegExp(r) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("r", r)?;
m.end()
},
Self::TypedArray(ta) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("ta", ta)?;
m.end()
},
Self::ArrayBuffer(ab) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("ab", ab)?;
m.end()
},
Self::Array { id, items } => {
let mut m = s.serialize_map(Some(2))?;
m.serialize_entry("a", items)?;
m.serialize_entry("id", id)?;
m.end()
},
Self::Object { id, entries } => {
let mut m = s.serialize_map(Some(2))?;
m.serialize_entry("o", entries)?;
m.serialize_entry("id", id)?;
m.end()
},
Self::Handle(idx) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("h", idx)?;
m.end()
},
Self::Reference(id) => {
let mut m = s.serialize_map(Some(1))?;
m.serialize_entry("ref", id)?;
m.end()
},
}
}
}
impl<'de> Deserialize<'de> for SerializedValue {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
d.deserialize_any(SerializedValueVisitor)
}
}
const AP_NUMBER_SENTINEL: &str = "$serde_json::private::Number";
struct SerializedValueVisitor;
impl<'de> Visitor<'de> for SerializedValueVisitor {
type Value = SerializedValue;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a Playwright isomorphic SerializedValue: raw bool/number/string, or single-key tagged object")
}
fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
Ok(SerializedValue::Bool(v))
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
#[allow(clippy::cast_precision_loss)]
let as_f64 = v as f64;
Ok(SerializedValue::Number(as_f64))
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
#[allow(clippy::cast_precision_loss)]
let as_f64 = v as f64;
Ok(SerializedValue::Number(as_f64))
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(SerializedValue::Number(v))
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(SerializedValue::Str(v.to_string()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(SerializedValue::Str(v))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(SerializedValue::Special(SpecialValue::Undefined))
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(SerializedValue::Special(SpecialValue::Null))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut bag = serde_json::Map::new();
while let Some((k, v)) = map.next_entry::<String, serde_json::Value>()? {
bag.insert(k, v);
}
if bag.len() == 1 {
if let Some(serde_json::Value::String(lit)) = bag.get(AP_NUMBER_SENTINEL) {
if let Ok(n) = lit.parse::<f64>() {
return Ok(SerializedValue::Number(n));
}
}
}
decode_tagged_object(bag).map_err(de::Error::custom)
}
}
#[derive(Debug, thiserror::Error)]
enum DecodeError {
#[error("invalid tag {tag:?}: {reason}")]
InvalidTag {
tag: &'static str,
reason: std::string::String,
},
#[error("id companion out of range: {0}")]
IdRange(#[from] std::num::TryFromIntError),
#[error("nested decode: {0}")]
Nested(#[from] serde_json::Error),
#[error("no recognised tag in object (keys: {0:?})")]
UnknownShape(Vec<std::string::String>),
}
impl DecodeError {
fn tag(tag: &'static str, reason: impl Into<std::string::String>) -> Self {
Self::InvalidTag {
tag,
reason: reason.into(),
}
}
}
fn decode_tagged_object(mut bag: serde_json::Map<String, serde_json::Value>) -> Result<SerializedValue, DecodeError> {
if let Some(v) = bag.remove("ref") {
let id = v
.as_u64()
.ok_or_else(|| DecodeError::tag("ref", format!("must be a u64, got {v}")))?;
return Ok(SerializedValue::Reference(u32::try_from(id)?));
}
if let Some(v) = bag.remove("v") {
let special: SpecialValue = serde_json::from_value(v)?;
return Ok(SerializedValue::Special(special));
}
if let Some(v) = bag.remove("d") {
let iso = v
.as_str()
.ok_or_else(|| DecodeError::tag("d", "must be string"))?
.to_string();
return Ok(SerializedValue::Date(iso));
}
if let Some(v) = bag.remove("u") {
let s = v
.as_str()
.ok_or_else(|| DecodeError::tag("u", "must be string"))?
.to_string();
return Ok(SerializedValue::Url(s));
}
if let Some(v) = bag.remove("bi") {
let s = v
.as_str()
.ok_or_else(|| DecodeError::tag("bi", "must be string"))?
.to_string();
return Ok(SerializedValue::BigInt(s));
}
if let Some(v) = bag.remove("e") {
let e: ErrorValue = serde_json::from_value(v)?;
return Ok(SerializedValue::Error(e));
}
if let Some(v) = bag.remove("r") {
let r: RegExpValue = serde_json::from_value(v)?;
return Ok(SerializedValue::RegExp(r));
}
if let Some(v) = bag.remove("a") {
let items: Vec<SerializedValue> = serde_json::from_value(v)?;
let id = bag
.remove("id")
.and_then(|v| v.as_u64())
.ok_or_else(|| DecodeError::tag("a", "must be paired with numeric id"))?;
return Ok(SerializedValue::Array {
id: u32::try_from(id)?,
items,
});
}
if let Some(v) = bag.remove("o") {
let entries: Vec<PropertyEntry> = serde_json::from_value(v)?;
let id = bag
.remove("id")
.and_then(|v| v.as_u64())
.ok_or_else(|| DecodeError::tag("o", "must be paired with numeric id"))?;
return Ok(SerializedValue::Object {
id: u32::try_from(id)?,
entries,
});
}
if let Some(v) = bag.remove("h") {
let idx = v.as_u64().ok_or_else(|| DecodeError::tag("h", "must be u64"))?;
return Ok(SerializedValue::Handle(u32::try_from(idx)?));
}
if let Some(v) = bag.remove("ta") {
let ta: TypedArrayValue = serde_json::from_value(v)?;
return Ok(SerializedValue::TypedArray(ta));
}
if let Some(v) = bag.remove("ab") {
let ab: ArrayBufferValue = serde_json::from_value(v)?;
return Ok(SerializedValue::ArrayBuffer(ab));
}
Err(DecodeError::UnknownShape(bag.keys().cloned().collect()))
}
#[derive(Debug, Clone, Default)]
pub struct SerializationContext {
next_id: u32,
}
impl SerializationContext {
pub fn alloc_id(&mut self) -> u32 {
self.next_id = self.next_id.saturating_add(1);
self.next_id
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SerializedArgument {
pub value: SerializedValue,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub handles: Vec<HandleId>,
}
impl Default for SerializedValue {
fn default() -> Self {
Self::Special(SpecialValue::Undefined)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum HandleId {
Cdp(String),
Bidi { shared_id: String, handle: Option<String> },
WebKit(String),
}
#[must_use]
pub fn encode_typed_array_bytes(bytes: &[u8]) -> String {
base64::engine::general_purpose::STANDARD.encode(bytes)
}
pub fn decode_typed_array_bytes(encoded: &str) -> Result<Vec<u8>, base64::DecodeError> {
base64::engine::general_purpose::STANDARD.decode(encoded)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serializes_bool_raw() {
assert_eq!(serde_json::to_value(SerializedValue::Bool(true)).unwrap(), json!(true));
assert_eq!(
serde_json::to_value(SerializedValue::Bool(false)).unwrap(),
json!(false)
);
}
#[test]
fn serializes_number_raw() {
assert_eq!(serde_json::to_value(SerializedValue::Number(1.5)).unwrap(), json!(1.5));
}
#[test]
fn serializes_string_raw() {
assert_eq!(
serde_json::to_value(SerializedValue::Str("hi".into())).unwrap(),
json!("hi")
);
}
#[test]
fn serializes_special_values() {
for (special, expected) in [
(SpecialValue::Null, json!({"v": "null"})),
(SpecialValue::Undefined, json!({"v": "undefined"})),
(SpecialValue::NaN, json!({"v": "NaN"})),
(SpecialValue::Infinity, json!({"v": "Infinity"})),
(SpecialValue::NegInfinity, json!({"v": "-Infinity"})),
(SpecialValue::NegZero, json!({"v": "-0"})),
] {
assert_eq!(
serde_json::to_value(SerializedValue::Special(special)).unwrap(),
expected,
"variant {special:?}"
);
}
}
#[test]
fn from_f64_routes_ieee_specials() {
assert!(matches!(
SerializedValue::from_f64(f64::NAN),
SerializedValue::Special(SpecialValue::NaN)
));
assert!(matches!(
SerializedValue::from_f64(f64::INFINITY),
SerializedValue::Special(SpecialValue::Infinity)
));
assert!(matches!(
SerializedValue::from_f64(f64::NEG_INFINITY),
SerializedValue::Special(SpecialValue::NegInfinity)
));
assert!(matches!(
SerializedValue::from_f64(-0.0_f64),
SerializedValue::Special(SpecialValue::NegZero)
));
assert!(matches!(
SerializedValue::from_f64(0.0_f64),
SerializedValue::Number(n) if (n - 0.0_f64).abs() < f64::EPSILON
));
assert!(matches!(
SerializedValue::from_f64(1.5),
SerializedValue::Number(n) if (n - 1.5_f64).abs() < f64::EPSILON
));
}
#[test]
fn serializes_date_url_bigint() {
assert_eq!(
serde_json::to_value(SerializedValue::date("2024-01-01T00:00:00.000Z")).unwrap(),
json!({"d": "2024-01-01T00:00:00.000Z"})
);
assert_eq!(
serde_json::to_value(SerializedValue::url("https://example.com/")).unwrap(),
json!({"u": "https://example.com/"})
);
assert_eq!(
serde_json::to_value(SerializedValue::bigint("9007199254740993")).unwrap(),
json!({"bi": "9007199254740993"})
);
}
#[test]
fn serializes_regexp() {
let wire = serde_json::to_value(SerializedValue::regexp("foo.*bar", "gi")).unwrap();
assert_eq!(wire, json!({"r": {"p": "foo.*bar", "f": "gi"}}));
}
#[test]
fn serializes_error() {
let wire = serde_json::to_value(SerializedValue::error(
"TypeError",
"nope",
"TypeError: nope\n at foo",
))
.unwrap();
assert_eq!(
wire,
json!({"e": {"n": "TypeError", "m": "nope", "s": "TypeError: nope\n at foo"}})
);
}
#[test]
fn serializes_typed_array_with_base64() {
let wire = serde_json::to_value(SerializedValue::typed_array(vec![1, 2, 3, 4], TypedArrayKind::U8)).unwrap();
assert_eq!(wire, json!({"ta": {"b": "AQIDBA==", "k": "ui8"}}));
}
#[test]
fn serializes_array_buffer_with_base64() {
let wire = serde_json::to_value(SerializedValue::array_buffer(vec![0xca, 0xfe, 0xba, 0xbe])).unwrap();
assert_eq!(wire, json!({"ab": {"b": "yv66vg=="}}));
}
#[test]
fn typed_array_bytes_per_element_matches_js() {
assert_eq!(TypedArrayKind::I8.bytes_per_element(), 1);
assert_eq!(TypedArrayKind::U8.bytes_per_element(), 1);
assert_eq!(TypedArrayKind::U8Clamped.bytes_per_element(), 1);
assert_eq!(TypedArrayKind::I16.bytes_per_element(), 2);
assert_eq!(TypedArrayKind::U16.bytes_per_element(), 2);
assert_eq!(TypedArrayKind::I32.bytes_per_element(), 4);
assert_eq!(TypedArrayKind::U32.bytes_per_element(), 4);
assert_eq!(TypedArrayKind::F32.bytes_per_element(), 4);
assert_eq!(TypedArrayKind::F64.bytes_per_element(), 8);
assert_eq!(TypedArrayKind::BI64.bytes_per_element(), 8);
assert_eq!(TypedArrayKind::BUI64.bytes_per_element(), 8);
}
#[test]
fn serializes_array_with_id() {
let wire = serde_json::to_value(SerializedValue::array(
1,
vec![SerializedValue::Number(1.0), SerializedValue::Number(2.0)],
))
.unwrap();
assert_eq!(wire, json!({"a": [1.0, 2.0], "id": 1}));
}
#[test]
fn serializes_object_with_id_preserves_order() {
let wire = serde_json::to_value(SerializedValue::object(
2,
vec![
PropertyEntry {
k: "first".into(),
v: SerializedValue::Number(1.0),
},
PropertyEntry {
k: "second".into(),
v: SerializedValue::Str("two".into()),
},
],
))
.unwrap();
assert_eq!(
wire,
json!({"o": [{"k": "first", "v": 1.0}, {"k": "second", "v": "two"}], "id": 2})
);
}
#[test]
fn serializes_handle_and_reference() {
assert_eq!(
serde_json::to_value(SerializedValue::handle(3)).unwrap(),
json!({"h": 3})
);
assert_eq!(
serde_json::to_value(SerializedValue::reference(5)).unwrap(),
json!({"ref": 5})
);
}
#[test]
fn deserializes_primitives_raw() {
assert_eq!(
serde_json::from_value::<SerializedValue>(json!(true)).unwrap(),
SerializedValue::Bool(true)
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!(42)).unwrap(),
SerializedValue::Number(42.0)
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!("hi")).unwrap(),
SerializedValue::Str("hi".into())
);
}
#[test]
fn deserializes_special_tag() {
for (wire, expected) in [
(json!({"v": "null"}), SpecialValue::Null),
(json!({"v": "undefined"}), SpecialValue::Undefined),
(json!({"v": "NaN"}), SpecialValue::NaN),
(json!({"v": "Infinity"}), SpecialValue::Infinity),
(json!({"v": "-Infinity"}), SpecialValue::NegInfinity),
(json!({"v": "-0"}), SpecialValue::NegZero),
] {
let parsed = serde_json::from_value::<SerializedValue>(wire.clone()).unwrap();
assert_eq!(parsed, SerializedValue::Special(expected), "wire: {wire}");
}
}
#[test]
fn deserializes_rich_tagged_forms() {
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"d": "2024"})).unwrap(),
SerializedValue::Date("2024".into())
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"u": "https://a/"})).unwrap(),
SerializedValue::Url("https://a/".into())
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"bi": "-42"})).unwrap(),
SerializedValue::BigInt("-42".into())
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"r": {"p": "a", "f": "g"}})).unwrap(),
SerializedValue::RegExp(RegExpValue {
p: "a".into(),
f: "g".into()
})
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"e": {"n": "E", "m": "m", "s": "s"}})).unwrap(),
SerializedValue::Error(ErrorValue {
n: "E".into(),
m: "m".into(),
s: "s".into()
})
);
}
#[test]
fn deserializes_array_and_object_with_id() {
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"a": [1, 2], "id": 1})).unwrap(),
SerializedValue::Array {
id: 1,
items: vec![SerializedValue::Number(1.0), SerializedValue::Number(2.0)],
}
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"o": [{"k": "x", "v": true}], "id": 2})).unwrap(),
SerializedValue::Object {
id: 2,
entries: vec![PropertyEntry {
k: "x".into(),
v: SerializedValue::Bool(true),
}],
}
);
}
#[test]
fn deserializes_handle_and_reference() {
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"h": 0})).unwrap(),
SerializedValue::Handle(0)
);
assert_eq!(
serde_json::from_value::<SerializedValue>(json!({"ref": 5})).unwrap(),
SerializedValue::Reference(5)
);
}
#[test]
fn deserializes_typed_array_base64() {
let parsed: SerializedValue = serde_json::from_value(json!({"ta": {"b": "AQIDBA==", "k": "ui8"}})).unwrap();
assert_eq!(
parsed,
SerializedValue::TypedArray(TypedArrayValue {
b: vec![1, 2, 3, 4],
k: TypedArrayKind::U8
})
);
}
#[test]
fn deserializes_array_buffer_base64() {
let parsed: SerializedValue = serde_json::from_value(json!({"ab": {"b": "yv66vg=="}})).unwrap();
assert_eq!(
parsed,
SerializedValue::ArrayBuffer(ArrayBufferValue {
b: vec![0xca, 0xfe, 0xba, 0xbe],
})
);
}
#[test]
fn rejects_empty_tagged_object() {
let err = serde_json::from_value::<SerializedValue>(json!({})).unwrap_err();
assert!(
err.to_string().contains("no recognised tag"),
"unexpected error message: {err}"
);
}
#[test]
fn roundtrips_every_variant_via_serde() {
let values = vec![
SerializedValue::Bool(true),
SerializedValue::Bool(false),
SerializedValue::Number(1.5),
SerializedValue::Number(0.0),
SerializedValue::Str("hello".into()),
SerializedValue::Special(SpecialValue::Null),
SerializedValue::Special(SpecialValue::Undefined),
SerializedValue::Special(SpecialValue::NaN),
SerializedValue::Special(SpecialValue::Infinity),
SerializedValue::Special(SpecialValue::NegInfinity),
SerializedValue::Special(SpecialValue::NegZero),
SerializedValue::date("2024-06-01T12:00:00.000Z"),
SerializedValue::url("https://a.test/"),
SerializedValue::bigint("-12345678901234567890"),
SerializedValue::regexp("a|b", "i"),
SerializedValue::error("Error", "boom", ""),
SerializedValue::typed_array(vec![0xde, 0xad, 0xbe, 0xef], TypedArrayKind::U32),
SerializedValue::array_buffer(vec![0x01, 0x02]),
SerializedValue::array(1, vec![SerializedValue::Number(1.0)]),
SerializedValue::object(
2,
vec![PropertyEntry {
k: "x".into(),
v: SerializedValue::Bool(true),
}],
),
SerializedValue::handle(0),
SerializedValue::reference(1),
];
for v in values {
let wire = serde_json::to_string(&v).unwrap();
let back: SerializedValue = serde_json::from_str(&wire).unwrap();
assert_eq!(v, back, "roundtrip drift for {wire}");
}
}
#[test]
fn from_json_maps_scalars() {
let mut ctx = SerializationContext::default();
assert_eq!(
SerializedValue::from_json(&json!(null), &mut ctx),
SerializedValue::Special(SpecialValue::Null)
);
assert_eq!(
SerializedValue::from_json(&json!(true), &mut ctx),
SerializedValue::Bool(true)
);
assert_eq!(
SerializedValue::from_json(&json!("hi"), &mut ctx),
SerializedValue::Str("hi".into())
);
assert!(matches!(
SerializedValue::from_json(&json!(42), &mut ctx),
SerializedValue::Number(n) if (n - 42.0).abs() < f64::EPSILON
));
}
#[test]
fn from_json_allocates_ids_for_collections() {
let mut ctx = SerializationContext::default();
let val = SerializedValue::from_json(&json!({"a": [1, 2], "b": null}), &mut ctx);
let (outer_id, entries) = match &val {
SerializedValue::Object { id, entries } => (*id, entries),
_ => panic!("expected Object"),
};
let a_entry = entries.iter().find(|e| e.k == "a").expect("has key `a`");
let a_id = match &a_entry.v {
SerializedValue::Array { id, .. } => *id,
_ => panic!("expected Array"),
};
assert_ne!(outer_id, a_id, "ids are distinct");
}
#[test]
fn to_json_like_roundtrips_json_subset() {
let original = json!({"arr": [1, true, "x", null], "n": 2.5});
let mut ctx = SerializationContext::default();
let wire = SerializedValue::from_json(&original, &mut ctx);
let back = wire.to_json_like().expect("JSON subset should round-trip");
assert_eq!(back, original);
}
#[test]
fn to_json_like_returns_none_for_rich_types() {
assert_eq!(SerializedValue::undefined().to_json_like(), None);
assert_eq!(SerializedValue::date("2024-01-01T00:00:00Z").to_json_like(), None);
assert_eq!(SerializedValue::handle(0).to_json_like(), None);
assert_eq!(SerializedValue::reference(1).to_json_like(), None);
assert_eq!(
SerializedValue::from_f64(f64::NAN).to_json_like(),
None,
"NaN has no JSON form"
);
}
#[test]
fn serialization_context_allocates_distinct_ids() {
let mut ctx = SerializationContext::default();
let ids: Vec<u32> = (0..5).map(|_| ctx.alloc_id()).collect();
assert_eq!(ids, vec![1, 2, 3, 4, 5]);
}
#[test]
fn serialized_argument_omits_empty_handles() {
let arg = SerializedArgument {
value: SerializedValue::Number(1.0),
handles: vec![],
};
let s = serde_json::to_string(&arg).unwrap();
assert_eq!(s, r#"{"value":1.0}"#);
}
#[test]
fn serialized_argument_carries_handle_list() {
let arg = SerializedArgument {
value: SerializedValue::array(1, vec![SerializedValue::handle(0), SerializedValue::handle(1)]),
handles: vec![
HandleId::Cdp("obj-1".into()),
HandleId::Bidi {
shared_id: "shared-1".into(),
handle: None,
},
],
};
let wire = serde_json::to_value(&arg).unwrap();
assert_eq!(
wire,
json!({
"value": {"a": [{"h": 0}, {"h": 1}], "id": 1},
"handles": [
{"Cdp": "obj-1"},
{"Bidi": {"shared_id": "shared-1", "handle": null}}
]
})
);
}
#[test]
fn parity_array_of_mixed_primitives() {
let v = SerializedValue::array(
1,
vec![
SerializedValue::Number(1.0),
SerializedValue::Bool(true),
SerializedValue::Str("x".into()),
SerializedValue::Special(SpecialValue::Null),
SerializedValue::Special(SpecialValue::NaN),
],
);
assert_eq!(
serde_json::to_value(&v).unwrap(),
json!({"a": [1.0, true, "x", {"v": "null"}, {"v": "NaN"}], "id": 1})
);
}
#[test]
fn parity_nested_object_with_handle_refs() {
let v = SerializedValue::object(
1,
vec![
PropertyEntry {
k: "el".into(),
v: SerializedValue::handle(0),
},
PropertyEntry {
k: "meta".into(),
v: SerializedValue::object(
2,
vec![PropertyEntry {
k: "count".into(),
v: SerializedValue::Number(3.0),
}],
),
},
],
);
assert_eq!(
serde_json::to_value(&v).unwrap(),
json!({
"o": [
{"k": "el", "v": {"h": 0}},
{"k": "meta", "v": {"o": [{"k": "count", "v": 3.0}], "id": 2}}
],
"id": 1
})
);
}
}