use std::collections::HashMap;
use std::collections::HashSet;
use std::str::FromStr;
use std::time::Duration;
use bigdecimal::BigDecimal;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use chrono::NaiveTime;
use chrono::TimeZone;
use chrono::Utc;
use num_bigint::BigInt;
use qubit_datatype::DataType;
use qubit_value::MultiValues;
use qubit_value::NamedMultiValues;
use qubit_value::NamedValue;
use qubit_value::Value;
use qubit_value::ValueContainer;
use qubit_value::ValueWireEncodeError;
use qubit_value::ValueWirePayloadRefV1;
use qubit_value::ValueWirePayloadV1;
use qubit_value::ValueWireV1;
use serde_json::Map;
use serde_json::Value as JsonValue;
use serde_json::from_value;
use serde_json::json;
use serde_json::to_string;
use serde_json::to_value;
use serde_json::to_vec;
use url::Url;
#[test]
fn test_value_wire_v1_identity_preserves_shape() {
assert_ne!(
ValueWireV1::try_from(Value::Int32(1)).expect("construct scalar wire"),
ValueWireV1::try_from(MultiValues::Int32(vec![1])).expect("construct collection wire"),
);
}
#[test]
fn test_value_wire_v1_preserves_f64_round_trip() {
let value = Value::Float64(625_026_605_f64 / 3.0);
let wire = ValueWireV1::try_from(value).expect("construct float wire");
let encoded = to_vec(&wire).expect("serialize float wire");
let decoded = crate::decode_value_wire_slice(&encoded).expect("deserialize float wire");
assert_eq!(decoded, wire);
}
#[test]
fn test_value_wire_v1_serializes_string_map_keys_in_dictionary_order() {
let map = (0..128)
.map(|index| (format!("key-{index:03}"), index.to_string()))
.collect::<HashMap<_, _>>();
let wire = ValueWireV1::try_from(Value::StringMap(map)).expect("construct string map wire");
let encoded = to_string(&wire).expect("serialize string map wire");
let expected_entries = (0..128)
.map(|index| format!(r#""key-{index:03}":"{index}""#))
.collect::<Vec<_>>()
.join(",");
let expected = format!(r#"{{"version":1,"value":{{"scalar":{{"stringmap":{{{expected_entries}}}}}}}}}"#,);
assert_eq!(encoded, expected);
}
#[test]
fn test_value_wire_v1_rejects_duplicate_string_map_keys() {
for input in [
r#"{"version":1,"value":{"scalar":{"stringmap":{"key":"first","key":"second"}}}}"#,
r#"{"version":1,"value":{"collection":{"stringmap":[{"key":"first","key":"second"}]}}}"#,
] {
assert!(
crate::decode_value_wire_str(input).is_err(),
"duplicate string-map key was accepted: {input}",
);
}
}
#[test]
fn test_value_wire_v1_rejects_duplicate_nested_json_keys() {
for input in [
r#"{"version":1,"value":{"scalar":{"json":{"key":"first","key":"second"}}}}"#,
r#"{"version":1,"value":{"collection":{"json":[{"nested":{"key":"first","key":"second"}}]}}}"#,
] {
assert!(
crate::decode_value_wire_str(input).is_err(),
"duplicate JSON key was accepted: {input}",
);
}
}
#[test]
fn test_value_wire_v1_preserves_former_json_number_marker_key() {
const MARKER: &str = concat!("$", "serde_json", "::private::Number");
let value = Value::Json(json!({
MARKER: "123",
"other": true,
}));
let wire = ValueWireV1::try_from(value).expect("the marker is an ordinary JSON key");
let text = to_string(&wire).expect("wire value should serialize");
assert!(text.contains(MARKER));
}
#[test]
fn test_value_wire_v1_serializes_string_map_collection_keys_in_dictionary_order() {
let map = HashMap::from([
("z".to_owned(), "26".to_owned()),
("a".to_owned(), "1".to_owned()),
("m".to_owned(), "13".to_owned()),
]);
let wire = ValueWireV1::try_from(MultiValues::StringMap(vec![map])).expect("construct string-map collection wire");
assert_eq!(
to_string(&wire).expect("serialize string-map collection wire"),
r#"{"version":1,"value":{"collection":{"stringmap":[{"a":"1","m":"13","z":"26"}]}}}"#,
);
}
#[test]
fn test_value_wire_v1_borrowed_string_map_keys_in_dictionary_order() {
let value = Value::StringMap(HashMap::from([
("z".to_owned(), "26".to_owned()),
("a".to_owned(), "1".to_owned()),
("m".to_owned(), "13".to_owned()),
]));
let payload = ValueWirePayloadRefV1::try_from(&value).expect("construct borrowed string-map payload");
assert_eq!(
to_string(&payload).expect("serialize borrowed string-map payload"),
r#"{"scalar":{"stringmap":{"a":"1","m":"13","z":"26"}}}"#,
);
}
#[test]
fn test_value_wire_v1_wraps_unversioned_payload_and_rejects_non_finite_float() {
let payload = ValueWirePayloadV1::try_from(Value::Int32(7)).expect("finite scalar should fit the V1 payload");
assert_eq!(
to_value(payload).expect("payload should serialize"),
json!({"scalar": {"int32": 7}}),
);
let wire = ValueWireV1::try_from(Value::Int32(7)).expect("finite scalar should fit the V1 envelope");
assert_eq!(
to_value(wire).expect("envelope should serialize"),
json!({"version": 1, "value": {"scalar": {"int32": 7}}}),
);
assert!(matches!(
ValueWireV1::try_from(Value::Float64(f64::NAN)),
Err(ValueWireEncodeError::NonFiniteFloat {
data_type: DataType::Float64,
})
));
}
#[cfg(feature = "big-decimal")]
#[test]
fn test_value_wire_v1_rejects_excessive_big_decimal_scale() {
let value = BigDecimal::new(BigInt::from(1), 150_001);
assert!(matches!(
ValueWireV1::try_from(Value::BigDecimal(value)),
Err(ValueWireEncodeError::BigDecimalScaleTooLarge {
scale: 150_001,
maximum_absolute_scale: 150_000,
})
));
}
#[cfg(feature = "big-decimal")]
#[test]
fn test_value_wire_v1_rejects_excessive_big_decimal_scale_on_decode() {
let input = scalar_wire("bigdecimal", json!({"coefficient": "1", "scale": 150_001}));
let error = crate::decode_value_wire_value(input).expect_err("excessive decimal scale must be rejected");
assert!(error.to_string().contains("maximum absolute scale"));
}
#[cfg(feature = "big-decimal")]
#[test]
fn test_value_wire_v1_rejects_minimum_big_decimal_scale_on_decode() {
let input = scalar_wire("bigdecimal", json!({"coefficient": "1", "scale": i64::MIN}));
assert!(crate::decode_value_wire_value(input).is_err());
}
#[cfg(feature = "url")]
#[test]
fn test_value_wire_v1_rejects_noncanonical_url_payload() {
let input = r#"{"version":1,"value":{"scalar":{"url":"HTTPS://example.com/"}}}"#;
assert!(crate::decode_value_wire_str(input).is_err());
let collection = r#"{"version":1,"value":{"collection":{"url":["HTTPS://example.com/"]}}}"#;
assert!(crate::decode_value_wire_str(collection).is_err());
}
#[derive(Debug)]
struct ValueFixture {
data_type: DataType,
value: Value,
tag: &'static str,
payload: JsonValue,
}
fn tagged_payload(tag: &str, payload: JsonValue) -> JsonValue {
JsonValue::Object(Map::from_iter([(tag.to_string(), payload)]))
}
fn wire_value(shape: &str, tag: &str, payload: JsonValue) -> JsonValue {
json!({
"version": 1,
"value": shaped_value(shape, tag, payload),
})
}
fn shaped_value(shape: &str, tag: &str, payload: JsonValue) -> JsonValue {
JsonValue::Object(Map::from_iter([(shape.to_string(), tagged_payload(tag, payload))]))
}
fn scalar_wire(tag: &str, payload: JsonValue) -> JsonValue {
wire_value("scalar", tag, payload)
}
fn collection_wire(tag: &str, payload: JsonValue) -> JsonValue {
wire_value("collection", tag, payload)
}
fn value_fixtures() -> Vec<ValueFixture> {
vec![
ValueFixture {
data_type: DataType::Bool,
value: Value::Bool(true),
tag: "bool",
payload: json!(true),
},
ValueFixture {
data_type: DataType::Char,
value: Value::Char('界'),
tag: "char",
payload: json!("界"),
},
ValueFixture {
data_type: DataType::Int8,
value: Value::Int8(-8),
tag: "int8",
payload: json!(-8),
},
ValueFixture {
data_type: DataType::Int16,
value: Value::Int16(-16),
tag: "int16",
payload: json!(-16),
},
ValueFixture {
data_type: DataType::Int32,
value: Value::Int32(-32),
tag: "int32",
payload: json!(-32),
},
ValueFixture {
data_type: DataType::Int64,
value: Value::Int64(-64),
tag: "int64",
payload: json!(-64),
},
ValueFixture {
data_type: DataType::Int128,
value: Value::Int128(i128::MIN),
tag: "int128",
payload: json!(i128::MIN.to_string()),
},
ValueFixture {
data_type: DataType::UInt8,
value: Value::UInt8(8),
tag: "uint8",
payload: json!(8),
},
ValueFixture {
data_type: DataType::UInt16,
value: Value::UInt16(16),
tag: "uint16",
payload: json!(16),
},
ValueFixture {
data_type: DataType::UInt32,
value: Value::UInt32(32),
tag: "uint32",
payload: json!(32),
},
ValueFixture {
data_type: DataType::UInt64,
value: Value::UInt64(64),
tag: "uint64",
payload: json!(64),
},
ValueFixture {
data_type: DataType::UInt128,
value: Value::UInt128(u128::MAX),
tag: "uint128",
payload: json!(u128::MAX.to_string()),
},
ValueFixture {
data_type: DataType::Float32,
value: Value::Float32(1.25),
tag: "float32",
payload: json!(1.25),
},
ValueFixture {
data_type: DataType::Float64,
value: Value::Float64(2.5),
tag: "float64",
payload: json!(2.5),
},
ValueFixture {
data_type: DataType::BigInteger,
value: Value::BigInteger(BigInt::from(123)),
tag: "biginteger",
payload: json!("123"),
},
ValueFixture {
data_type: DataType::BigDecimal,
value: Value::BigDecimal(BigDecimal::from_str("123.4500").expect("valid decimal")),
tag: "bigdecimal",
payload: json!({"coefficient": "1234500", "scale": 4}),
},
ValueFixture {
data_type: DataType::String,
value: Value::String("text".to_string()),
tag: "string",
payload: json!("text"),
},
ValueFixture {
data_type: DataType::Date,
value: Value::Date(NaiveDate::from_ymd_opt(2026, 7, 14).unwrap()),
tag: "date",
payload: json!("2026-07-14"),
},
ValueFixture {
data_type: DataType::Time,
value: Value::Time(NaiveTime::from_hms_opt(1, 2, 3).unwrap()),
tag: "time",
payload: json!("01:02:03"),
},
ValueFixture {
data_type: DataType::DateTime,
value: Value::DateTime(NaiveDateTime::parse_from_str("2026-07-14 01:02:03", "%Y-%m-%d %H:%M:%S").unwrap()),
tag: "datetime",
payload: json!("2026-07-14T01:02:03"),
},
ValueFixture {
data_type: DataType::Instant,
value: Value::Instant(Utc.with_ymd_and_hms(2026, 7, 14, 1, 2, 3).unwrap()),
tag: "instant",
payload: json!("2026-07-14T01:02:03Z"),
},
ValueFixture {
data_type: DataType::Duration,
value: Value::Duration(Duration::new(1, 2)),
tag: "duration",
payload: json!({"secs": 1, "nanos": 2}),
},
ValueFixture {
data_type: DataType::Url,
value: Value::new(Url::parse("https://example.com/path").unwrap()),
tag: "url",
payload: json!("https://example.com/path"),
},
ValueFixture {
data_type: DataType::StringMap,
value: Value::StringMap(HashMap::from([("key".to_string(), "value".to_string())])),
tag: "stringmap",
payload: json!({"key": "value"}),
},
ValueFixture {
data_type: DataType::Json,
value: Value::Json(json!({"nested": true})),
tag: "json",
payload: json!({"nested": true}),
},
]
}
#[test]
fn test_value_wire_v1_fixtures_cover_every_data_type() {
let mut actual = value_fixtures()
.into_iter()
.map(|fixture| fixture.data_type)
.collect::<Vec<_>>();
let mut expected = DataType::ALL.to_vec();
actual.sort_by_key(|data_type| data_type.as_str());
expected.sort_by_key(|data_type| data_type.as_str());
assert_eq!(actual, expected);
}
#[test]
fn test_value_wire_v1_tags_are_unique_and_stable() {
let fixtures = value_fixtures();
let tags = fixtures.iter().map(|fixture| fixture.tag).collect::<HashSet<_>>();
assert_eq!(tags.len(), fixtures.len());
assert_eq!(
fixtures
.iter()
.map(|fixture| (fixture.data_type, fixture.tag))
.collect::<Vec<_>>(),
vec![
(DataType::Bool, "bool"),
(DataType::Char, "char"),
(DataType::Int8, "int8"),
(DataType::Int16, "int16"),
(DataType::Int32, "int32"),
(DataType::Int64, "int64"),
(DataType::Int128, "int128"),
(DataType::UInt8, "uint8"),
(DataType::UInt16, "uint16"),
(DataType::UInt32, "uint32"),
(DataType::UInt64, "uint64"),
(DataType::UInt128, "uint128"),
(DataType::Float32, "float32"),
(DataType::Float64, "float64"),
(DataType::BigInteger, "biginteger"),
(DataType::BigDecimal, "bigdecimal"),
(DataType::String, "string"),
(DataType::Date, "date"),
(DataType::Time, "time"),
(DataType::DateTime, "datetime"),
(DataType::Instant, "instant"),
(DataType::Duration, "duration"),
(DataType::Url, "url"),
(DataType::StringMap, "stringmap"),
(DataType::Json, "json"),
],
);
}
#[test]
fn test_value_wire_v1_unset_tags_cover_every_data_type() {
for &data_type in DataType::ALL {
let scalar = ValueContainer::Scalar(Value::Unset(data_type));
let collection = ValueContainer::Collection(MultiValues::Unset(data_type));
let expected_scalar = scalar_wire("unset", json!(data_type.as_str()));
let expected_collection = collection_wire("unset", json!(data_type.as_str()));
assert_eq!(
to_value(ValueWireV1::try_from(scalar.clone()).expect("construct scalar wire"),)
.expect("serialize unset scalar"),
expected_scalar
);
assert_eq!(
crate::decode_value_wire_value(expected_scalar)
.expect("deserialize unset scalar")
.into_container(),
scalar
);
assert_eq!(
to_value(ValueWireV1::try_from(collection.clone()).expect("construct collection wire"),)
.expect("serialize unset collection"),
expected_collection
);
assert_eq!(
crate::decode_value_wire_value(expected_collection)
.expect("deserialize unset collection")
.into_container(),
collection
);
}
}
#[test]
fn test_value_wire_v1_scalar_golden_round_trips_all_types() {
for fixture in value_fixtures() {
let expected = scalar_wire(fixture.tag, fixture.payload);
let dto = ValueWireV1::try_from(fixture.value.clone()).expect("construct scalar wire");
assert_eq!(to_value(&dto).unwrap(), expected);
let restored = crate::decode_value_wire_value(expected).unwrap();
assert_eq!(ValueContainer::from(restored), ValueContainer::Scalar(fixture.value),);
}
}
#[test]
fn test_value_wire_v1_collection_golden_round_trips_all_types() {
for fixture in value_fixtures() {
let values = MultiValues::from(fixture.value);
let expected = collection_wire(fixture.tag, json!([fixture.payload]));
let dto = ValueWireV1::try_from(values.clone()).expect("construct collection wire");
assert_eq!(to_value(&dto).unwrap(), expected);
let restored = crate::decode_value_wire_value(expected).unwrap();
assert_eq!(ValueContainer::from(restored), ValueContainer::Collection(values),);
}
}
#[test]
fn test_value_wire_v1_borrowed_payload_golden_round_trips_all_types() {
for fixture in value_fixtures() {
let expected_scalar = shaped_value("scalar", fixture.tag, fixture.payload.clone());
let scalar = ValueWirePayloadRefV1::try_from(&fixture.value).expect("construct borrowed scalar payload");
assert_eq!(to_value(&scalar).unwrap(), expected_scalar);
assert_eq!(
crate::decode_value_wire_payload_value(expected_scalar)
.unwrap()
.into_container(),
ValueContainer::Scalar(fixture.value.clone()),
);
let values = MultiValues::from(fixture.value.clone());
let expected_collection = shaped_value("collection", fixture.tag, json!([fixture.payload]));
let collection = ValueWirePayloadRefV1::try_from(&values).expect("construct borrowed collection payload");
assert_eq!(to_value(&collection).unwrap(), expected_collection,);
assert_eq!(
crate::decode_value_wire_payload_value(expected_collection)
.unwrap()
.into_container(),
ValueContainer::Collection(values),
);
}
}
#[test]
fn test_value_wire_v1_preserves_unset_empty_singleton_and_json_null() {
let cases = [
(
ValueContainer::Scalar(Value::Unset(DataType::Int32)),
scalar_wire("unset", json!("int32")),
),
(
ValueContainer::Collection(MultiValues::Unset(DataType::Int32)),
collection_wire("unset", json!("int32")),
),
(
ValueContainer::Collection(MultiValues::Int32(Vec::new())),
collection_wire("int32", json!([])),
),
(
ValueContainer::Collection(MultiValues::Int32(vec![42])),
collection_wire("int32", json!([42])),
),
(
ValueContainer::Scalar(Value::Json(JsonValue::Null)),
scalar_wire("json", JsonValue::Null),
),
(
ValueContainer::Scalar(Value::Unset(DataType::Json)),
scalar_wire("unset", json!("json")),
),
];
for (container, expected) in cases {
assert_eq!(
to_value(ValueWireV1::try_from(container.clone()).expect("construct V1 wire"),).unwrap(),
expected
);
assert_eq!(
crate::decode_value_wire_value(expected).unwrap().into_container(),
container,
);
}
}
#[test]
fn test_value_wire_v1_owned_conversions_preserve_shape() {
let into_container: fn(ValueWireV1) -> ValueContainer = ValueWireV1::into_container;
let scalar = ValueWireV1::try_from(Value::Int32(42)).expect("construct scalar wire");
assert_eq!(scalar.container(), &ValueContainer::Scalar(Value::Int32(42)),);
let collection = ValueWireV1::try_from(MultiValues::Int32(vec![42])).expect("construct collection wire");
assert_eq!(
collection.container(),
&ValueContainer::Collection(MultiValues::Int32(vec![42])),
);
assert_eq!(ValueContainer::from(scalar), ValueContainer::Scalar(Value::Int32(42)),);
assert_eq!(
into_container(collection),
ValueContainer::Collection(MultiValues::Int32(vec![42])),
);
let container = ValueContainer::Scalar(Value::String("explicit".to_string()));
assert_eq!(
into_container(ValueWireV1::try_from(container.clone()).expect("construct explicit-shape wire")),
container,
);
}
#[test]
fn test_named_values_keep_outer_fields_and_embed_value_wire_v1() {
let named = NamedValue::new("port", Value::Int32(8080));
let expected = json!({
"name": "port",
"value": scalar_wire("int32", json!(8080)),
});
assert_eq!(to_value(&named).unwrap(), expected);
assert_eq!(from_value::<NamedValue>(expected).unwrap(), named);
let named = NamedMultiValues::new("ports", MultiValues::Int32(vec![8080, 8081]));
let expected = json!({
"name": "ports",
"value": collection_wire("int32", json!([8080, 8081])),
});
assert_eq!(to_value(&named).unwrap(), expected);
assert_eq!(from_value::<NamedMultiValues>(expected).unwrap(), named,);
}
#[test]
fn test_value_wire_v1_rejects_invalid_envelopes_and_unknown_tags() {
let valid_value = json!({"scalar": {"int32": 42}});
for invalid in [
json!({"value": valid_value}),
json!({"version": "1", "value": valid_value}),
json!({"version": 2, "value": valid_value}),
json!({"version": 1}),
json!({"version": 1, "value": valid_value, "extra": true}),
json!({"version": 1, "value": {"unknown": {"int32": 42}}}),
json!({"version": 1, "value": {"scalar": {"unknown": 42}}}),
json!({"version": 1, "value": {"scalar": {"int32": 42, "bool": true}}}),
] {
assert!(
crate::decode_value_wire_value(invalid.clone()).is_err(),
"unexpectedly accepted {invalid}",
);
}
}
#[test]
fn test_value_wire_v1_rejects_noncanonical_external_tag_shapes() {
for noncanonical in [
json!({"Int32": 42}),
json!({"Unset": "int32"}),
json!({"Scalar": {"Int32": 42}}),
json!({"Collection": {"Int32": [42]}}),
] {
assert!(crate::decode_value_wire_value(noncanonical).is_err());
}
}
#[test]
fn test_value_wire_v1_wide_integer_payloads_require_canonical_decimal_strings() {
for invalid in [
scalar_wire("int128", json!(128)),
scalar_wire("int128", json!("12x")),
scalar_wire("int128", json!("+1")),
scalar_wire("int128", json!("01")),
scalar_wire("uint128", json!("-1")),
scalar_wire("uint128", json!("01")),
] {
assert!(crate::decode_value_wire_value(invalid).is_err());
}
for invalid in [
collection_wire("uint128", json!(["1", 2])),
collection_wire("uint128", json!(["1", "02"])),
] {
assert!(crate::decode_value_wire_value(invalid).is_err());
}
}
#[test]
fn test_value_wire_v1_big_number_payloads_require_canonical_structures() {
for invalid in [
scalar_wire("biginteger", json!([1, [123]])),
scalar_wire("biginteger", json!("12x")),
scalar_wire("biginteger", json!("+1")),
scalar_wire("biginteger", json!("001")),
scalar_wire("bigdecimal", json!(12.5)),
scalar_wire("bigdecimal", json!("1.0")),
scalar_wire("bigdecimal", json!({"coefficient": "01", "scale": 1})),
scalar_wire("bigdecimal", json!({"coefficient": "1", "scale": 1, "extra": true})),
] {
assert!(crate::decode_value_wire_value(invalid).is_err());
}
assert!(crate::decode_value_wire_value(collection_wire("biginteger", json!(["1", "02"]),)).is_err(),);
}
#[test]
fn test_value_wire_v1_duration_payload_is_strict() {
assert!(
crate::decode_value_wire_value(scalar_wire("duration", json!({"secs": 1, "nanos": 1_000_000_000}),)).is_err(),
);
assert!(
crate::decode_value_wire_value(scalar_wire("duration", json!({"secs": 1, "nanos": 2, "extra": 3}),)).is_err(),
);
assert!(
crate::decode_value_wire_value(collection_wire(
"duration",
json!([{"secs": 1, "nanos": 2, "extra": 3}]),
))
.is_err(),
);
}