use crate::encode::ProbeError;
use crate::sink::{Aggregate, Sink};
use crate::source::Source;
use serde_json::{Map, Number, Value};
use std::string::String;
use std::vec::Vec;
use zerodds_ast::PrimitiveKind;
use zerodds_float128::Float128;
pub const EXACT_JSON_INTEGER: i128 = 1 << 53;
impl Source for Value {
fn kind(&self) -> &'static str {
match self {
Self::Null => "null",
Self::Bool(_) => "a boolean",
Self::Number(_) => "a number",
Self::String(_) => "a string",
Self::Array(_) => "an array",
Self::Object(_) => "an object",
}
}
fn is_null(&self) -> bool {
Self::is_null(self)
}
fn is_object(&self) -> bool {
Self::is_object(self)
}
fn as_bool(&self) -> Option<bool> {
Self::as_bool(self)
}
fn as_text(&self) -> Option<&str> {
self.as_str()
}
fn as_float(&self) -> Option<f64> {
self.as_f64()
}
fn as_items(&self) -> Option<&[Self]> {
self.as_array().map(Vec::as_slice)
}
fn field(&self, name: &str) -> Option<&Self> {
self.as_object().and_then(|map| map.get(name))
}
fn entries(&self) -> Vec<(&str, &Self)> {
self.as_object().map_or_else(Vec::new, |map| {
map.iter()
.map(|(name, value)| (name.as_str(), value))
.collect()
})
}
fn as_integer(&self, path: &str) -> Result<i128, ProbeError> {
if let Some(text) = self.as_str() {
return text.parse::<i128>().map_err(|_| ProbeError::Mismatch {
path: path.to_string(),
expected: "an integer in a string".to_string(),
found: format!("`{text}`"),
});
}
let number = self
.as_i64()
.map(i128::from)
.or_else(|| self.as_u64().map(i128::from))
.ok_or_else(|| ProbeError::Mismatch {
path: path.to_string(),
expected: "an integer".to_string(),
found: Source::kind(self).to_string(),
})?;
if number.abs() > EXACT_JSON_INTEGER {
return Err(ProbeError::Mismatch {
path: path.to_string(),
expected: "an integer past 2^53 written as a string, which a JSON number \
cannot carry exactly"
.to_string(),
found: format!("the number {number}"),
});
}
Ok(number)
}
fn as_float128(&self, path: &str) -> Result<Float128, ProbeError> {
match self {
Self::String(text) => match float128_from_hex_bits(text) {
Some(Ok(value)) => Ok(value),
Some(Err(expected)) => Err(ProbeError::Mismatch {
path: path.to_string(),
expected: expected.to_string(),
found: format!("`{text}`"),
}),
None => text
.parse::<Float128>()
.map_err(|error| ProbeError::Mismatch {
path: path.to_string(),
expected: "a decimal a long double can hold, or 0x and 32 hex digits"
.to_string(),
found: format!("`{text}` — {error}"),
}),
},
Self::Number(_) => {
self.as_f64()
.map(Float128::from_f64)
.ok_or_else(|| ProbeError::Mismatch {
path: path.to_string(),
expected: "a number".to_string(),
found: Source::kind(self).to_string(),
})
}
other => Err(ProbeError::Mismatch {
path: path.to_string(),
expected: "a long double, as a decimal string or a number".to_string(),
found: Source::kind(other).to_string(),
}),
}
}
fn render(&self) -> String {
let mut out = String::new();
write_text(self, &mut out);
out
}
}
fn write_text(value: &Value, out: &mut String) {
use std::fmt::Write as _;
match value {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort();
out.push('{');
for (index, key) in keys.into_iter().enumerate() {
if index > 0 {
out.push(',');
}
write_text(&Value::String(key.clone()), out);
out.push(':');
match map.get(key) {
Some(entry) => write_text(entry, out),
None => out.push_str("\"<missing>\""),
}
}
out.push('}');
}
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
write_text(item, out);
}
out.push(']');
}
Value::String(text) => {
out.push('"');
for ch in text.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
control if (control as u32) < 0x20 => {
let _ = write!(out, "\\u{:04x}", control as u32);
}
other => out.push(other),
}
}
out.push('"');
}
Value::Number(number) => out.push_str(&number.to_string()),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Null => out.push_str("null"),
}
}
impl Sink for Value {
fn boolean(value: bool) -> Self {
Self::Bool(value)
}
fn integer(kind: PrimitiveKind, value: i128, path: &str) -> Result<Self, ProbeError> {
if matches!(kind, PrimitiveKind::Int64 | PrimitiveKind::UInt64) {
return Ok(Self::String(format!("{value}")));
}
match i64::try_from(value) {
Ok(fits) => Ok(Self::Number(fits.into())),
Err(_) => Err(ProbeError::Mismatch {
path: path.to_string(),
expected: "an integer a JSON number can hold".to_string(),
found: format!("{value}"),
}),
}
}
fn float(kind: PrimitiveKind, value: f64, path: &str) -> Result<Self, ProbeError> {
let _ = kind;
Self::number(value, path)
}
fn float128(value: Float128) -> Self {
Self::String(value.to_exact_decimal())
}
fn character(kind: PrimitiveKind, value: char) -> Self {
let _ = kind;
Self::String(value.to_string())
}
fn text(value: String) -> Self {
Self::String(value)
}
fn fixed(decimal: String) -> Self {
Self::String(decimal)
}
fn enumerator(name: String) -> Self {
Self::String(name)
}
fn bit_names(names: Vec<String>) -> Self {
Self::Array(names.into_iter().map(Self::String).collect())
}
fn bitfields(fields: Vec<(String, i128)>, path: &str) -> Result<Self, ProbeError> {
let mut out = Map::new();
for (name, raw) in fields {
match i64::try_from(raw) {
Ok(number) => {
out.insert(name, Self::Number(number.into()));
}
Err(_) => {
return Err(ProbeError::Mismatch {
path: format!("{path}.{name}"),
expected: "a bitfield a JSON number can hold".to_string(),
found: format!("{raw}"),
});
}
}
}
Ok(Self::Object(out))
}
fn collection(items: Vec<Self>) -> Self {
Self::Array(items)
}
fn map_entry(key: Self, value: Self) -> Self {
Self::Array(vec![key, value])
}
fn aggregate(members: Aggregate<Self>) -> Self {
Self::Object(members.into_entries().into_iter().collect())
}
fn number(value: f64, path: &str) -> Result<Self, ProbeError> {
match Number::from_f64(value) {
Some(number) => Ok(Self::Number(number)),
None => Err(ProbeError::Mismatch {
path: path.to_string(),
expected: "a finite number, which is all JSON can write".to_string(),
found: format!("{value}"),
}),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
use crate::contract::{Member, character, integer, integer_for, member_value, object};
use serde_json::json;
use zerodds_ast_core::Id;
#[test]
fn an_integer_past_two_to_the_fifty_third_has_to_be_a_string() {
assert_eq!(
integer(&json!("9007199254740993"), "p"),
Ok(9_007_199_254_740_993)
);
let refused = integer(&json!(9_007_199_254_740_993i64), "p").expect_err("a refusal");
assert!(matches!(refused, ProbeError::Mismatch { ref path, .. } if path == "p"));
assert_eq!(
integer(&json!(9_007_199_254_740_992i64), "p"),
Ok(EXACT_JSON_INTEGER)
);
}
#[test]
fn a_value_outside_its_primitive_is_refused_and_not_wrapped() {
assert_eq!(integer_for(PrimitiveKind::Octet, &json!(255), "p"), Ok(255));
assert!(integer_for(PrimitiveKind::Octet, &json!(300), "p").is_err());
assert!(integer_for(PrimitiveKind::Int8, &json!(-129), "p").is_err());
assert!(integer_for(PrimitiveKind::UInt32, &json!(-1), "p").is_err());
}
#[test]
fn a_char_is_one_character_and_never_the_first_of_several() {
assert_eq!(character(&json!("a"), "p"), Ok('a'));
assert!(character(&json!("ab"), "p").is_err());
assert!(character(&json!(""), "p").is_err());
}
#[test]
fn null_is_neither_a_value_nor_an_absence() {
let value = json!({ "a": null });
let fields = object(&value, "S").expect("it is an object");
let required = Member {
node: zerodds_ast_core::NodeId::from_parts(zerodds_ast_core::IdSpace::MODEL, 1),
name: "a".to_string(),
type_: Id::first_at(0),
optional: false,
};
let optional = Member {
optional: true,
..required.clone()
};
for member in [&required, &optional] {
assert!(matches!(
member_value(fields, member, "S"),
Err(ProbeError::Mismatch { ref path, .. }) if path == "S.a"
));
}
}
#[test]
fn a_sixty_four_bit_width_is_a_string_at_every_magnitude() {
assert_eq!(
<Value as Sink>::integer(PrimitiveKind::Int64, 3, "p"),
Ok(Value::String("3".to_string()))
);
assert_eq!(
<Value as Sink>::integer(PrimitiveKind::Int32, 3, "p"),
Ok(Value::Number(3.into()))
);
}
}
fn float128_from_hex_bits(text: &str) -> Option<Result<Float128, &'static str>> {
let digits = text
.strip_prefix("0x")
.or_else(|| text.strip_prefix("0X"))?;
if digits.len() != 32 {
return Some(Err(
"exactly 32 hexadecimal digits, one per binary128 nibble",
));
}
let nibble = |b: u8| match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
};
let mut octets = [0u8; 16];
for (slot, pair) in octets.iter_mut().zip(digits.as_bytes().chunks_exact(2)) {
match (nibble(pair[0]), nibble(pair[1])) {
(Some(high), Some(low)) => *slot = (high << 4) | low,
_ => return Some(Err("a hexadecimal digit")),
}
}
Some(Ok(Float128::from_le_bytes(octets)))
}
#[cfg(test)]
mod float128_hex_tests {
#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use super::*;
#[test]
fn the_suites_own_float128_literals_are_read_as_bits() {
let half = float128_from_hex_bits("0x0000000000000000000000000000fe3f")
.expect("recognised as hex")
.expect("well formed");
assert_eq!(half.to_le_bytes()[15], 0x3f, "0.5 keeps its exponent octet");
assert_eq!(half, Float128::from_f64(0.5), "struct_float128_x1 is 0.5");
let twelve_three = float128_from_hex_bits("0x9A999999999999999999999999890240")
.expect("recognised as hex")
.expect("well formed");
let want: [u8; 16] = [
0x9A, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x99, 0x89,
0x02, 0x40,
];
assert_eq!(
twelve_three.to_le_bytes(),
want,
"struct_primitives x12 round-trips its octets"
);
}
#[test]
fn a_decimal_still_reaches_the_decimal_parser() {
assert!(
float128_from_hex_bits("12.3").is_none(),
"a decimal is not this spelling and must fall through"
);
}
#[test]
fn a_malformed_hex_literal_is_refused_rather_than_substituted() {
assert!(
matches!(float128_from_hex_bits("0xfe3f"), Some(Err(_))),
"too few digits is an error, not a zero"
);
assert!(
matches!(float128_from_hex_bits("0xZZ"), Some(Err(_))),
"a non-hex digit is an error, not a zero"
);
}
}