use serde_json::Value;
use zerodds_ast::{Ast, TypeNode};
use zerodds_ast_core::Id;
use zerodds_wire_codec::encode::ProbeError;
pub use zerodds_wire_codec::canonical::Difference;
#[inline]
pub fn canonicalise(
ast: &Ast,
type_: Id<TypeNode>,
value: &Value,
path: &str,
) -> Result<Value, ProbeError> {
zerodds_wire_codec::canonical::canonicalise(ast, type_, value, path)
}
#[must_use]
#[inline]
pub fn text(value: &Value) -> String {
zerodds_wire_codec::canonical::text(value)
}
#[must_use]
#[inline]
pub fn difference(expected: &Value, found: &Value, path: &str) -> Option<Difference> {
zerodds_wire_codec::canonical::difference(expected, found, path)
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::encode::model_from_idl;
use serde_json::json;
fn model(idl: &str) -> Ast {
model_from_idl(idl, "canonical.idl").expect("the fixture parses")
}
fn type_of(ast: &Ast, name: &str) -> Id<TypeNode> {
ast.find_by_scoped_name(&zerodds_ast_core::ScopedName::absolute(vec![
name.to_string(),
]))
.ok()
.and_then(zerodds_ast::AnyId::as_type)
.expect("the type is in the model")
}
#[test]
fn the_text_does_not_depend_on_the_order_the_keys_went_in() {
let one: Value = serde_json::from_str(r#"{"b":2,"a":1}"#).unwrap();
let other: Value = serde_json::from_str(r#"{"a":1,"b":2}"#).unwrap();
assert_eq!(text(&one), text(&other));
assert_eq!(text(&one), r#"{"a":1,"b":2}"#);
}
#[test]
fn a_sixty_four_bit_integer_stays_a_string_and_a_long_stays_a_number() {
let ast = model("@final struct S { long small; long long big; };");
let type_ = type_of(&ast, "S");
let out = canonicalise(
&ast,
type_,
&json!({ "small": -7, "big": "9007199254740993" }),
"S",
)
.expect("the sample is well formed");
assert_eq!(text(&out), r#"{"big":"9007199254740993","small":-7}"#);
}
#[test]
fn a_float_member_is_rounded_to_the_width_it_is_written_at() {
let ast = model("@final struct S { float f; double d; };");
let type_ = type_of(&ast, "S");
let out = canonicalise(&ast, type_, &json!({ "f": 0.1, "d": 0.1 }), "S")
.expect("the sample is well formed");
assert_eq!(text(&out), r#"{"d":0.1,"f":0.10000000149011612}"#);
}
#[test]
fn an_absent_optional_is_a_missing_key_and_an_explicit_null_is_an_error() {
let ast = model("@mutable struct S { long a; @optional long b; };");
let type_ = type_of(&ast, "S");
let out = canonicalise(&ast, type_, &json!({ "a": 1 }), "S").expect("b may be absent");
assert_eq!(text(&out), r#"{"a":1}"#);
let refused = canonicalise(&ast, type_, &json!({ "a": 1, "b": null }), "S");
assert!(refused.is_err(), "an explicit null is not an absent member");
}
#[test]
fn a_bitmask_is_ordered_by_declaration_and_deduplicated() {
let ast = model("@bit_bound(8) bitmask M { A, B, C };\n@final struct S { M flags; };");
let type_ = type_of(&ast, "S");
let out = canonicalise(&ast, type_, &json!({ "flags": ["C", "A", "A"] }), "S")
.expect("the names are declared");
assert_eq!(text(&out), r#"{"flags":["A","C"]}"#);
}
#[test]
fn a_fixed_is_written_with_the_digits_the_type_has() {
let ast = model("@final struct S { fixed<7, 3> amount; };");
let type_ = type_of(&ast, "S");
for (written, canonical) in [
("1.5", "1.500"),
("0001.500", "1.500"),
("-0.000", "0.000"),
("-12.25", "-12.250"),
] {
let out = canonicalise(&ast, type_, &json!({ "amount": written }), "S")
.expect("the decimal fits");
assert_eq!(text(&out), format!(r#"{{"amount":"{canonical}"}}"#));
}
}
#[test]
fn the_difference_names_the_first_place_two_samples_part() {
let left = json!({ "a": 1, "b": [1, 2, 3] });
let right = json!({ "a": 1, "b": [1, 9, 3] });
let found = difference(&left, &right, "S").expect("they differ");
assert_eq!(found.path, "S.b[1]");
assert_eq!(found.expected.as_deref(), Some("2"));
assert_eq!(found.found.as_deref(), Some("9"));
assert_eq!(difference(&left, &left, "S"), None);
}
#[test]
fn a_key_that_names_no_member_is_refused_rather_than_dropped() {
let ast = model("@final struct S { long a; };");
let type_ = type_of(&ast, "S");
let refused = canonicalise(&ast, type_, &json!({ "a": 1, "typo": 2 }), "S");
assert!(matches!(refused, Err(ProbeError::Unknown { .. })));
}
}