use bytes::BytesMut;
use postgres_types::{FromSql, IsNull, ToSql, Type};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum MirJzSONError {
#[error("MirJzSON cannot represent the source JSON value: {0}")]
UnsupportedJsonValue(String),
#[error("MirJzSON failed to decode JSONB bytes: {0}")]
JsonDecode(String),
}
#[derive(Clone, Debug)]
#[repr(transparent)]
pub struct MirJzSON {
portable: sassi::JSahibON,
}
impl MirJzSON {
pub fn as_jsahibon(&self) -> &sassi::JSahibON {
&self.portable
}
pub fn into_jsahibon(self) -> sassi::JSahibON {
self.portable
}
}
impl From<sassi::JSahibON> for MirJzSON {
fn from(value: sassi::JSahibON) -> Self {
Self { portable: value }
}
}
impl TryFrom<serde_json::Value> for MirJzSON {
type Error = MirJzSONError;
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
sassi::JSahibON::try_from(value)
.map(MirJzSON::from)
.map_err(|err| MirJzSONError::UnsupportedJsonValue(err.to_string()))
}
}
impl From<MirJzSON> for serde_json::Value {
fn from(value: MirJzSON) -> Self {
value.portable.into()
}
}
impl ToSql for MirJzSON {
fn to_sql(
&self,
ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
let value: serde_json::Value = self.portable.clone().into();
value.to_sql(ty, out)
}
fn accepts(ty: &Type) -> bool {
<serde_json::Value as ToSql>::accepts(ty)
}
postgres_types::to_sql_checked!();
}
impl<'a> FromSql<'a> for MirJzSON {
fn from_sql(
ty: &Type,
raw: &'a [u8],
) -> Result<Self, Box<dyn std::error::Error + Sync + Send>> {
let value = <serde_json::Value as FromSql>::from_sql(ty, raw).map_err(|err| {
Box::new(MirJzSONError::JsonDecode(err.to_string()))
as Box<dyn std::error::Error + Sync + Send>
})?;
let portable = sassi::JSahibON::try_from(value).map_err(|err| {
Box::new(MirJzSONError::UnsupportedJsonValue(err.to_string()))
as Box<dyn std::error::Error + Sync + Send>
})?;
Ok(MirJzSON { portable })
}
fn accepts(ty: &Type) -> bool {
<serde_json::Value as FromSql>::accepts(ty)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn from_jsahibon_roundtrips_through_projection() {
let original = sassi::JSahibON::I64(42);
let mir: MirJzSON = original.clone().into();
let back = mir.into_jsahibon();
assert_eq!(back, original);
}
#[test]
fn as_jsahibon_borrows_without_consuming() {
let mir: MirJzSON = sassi::JSahibON::Bool(true).into();
let borrowed = mir.as_jsahibon();
match borrowed {
sassi::JSahibON::Bool(true) => {}
other => panic!("expected Bool(true), got {other:?}"),
}
let _owned = mir.into_jsahibon();
}
#[test]
fn try_from_json_value_accepts_object() {
let raw = json!({"a": 1, "b": "two"});
let mir = MirJzSON::try_from(raw).expect("plain object must convert");
let portable = mir.into_jsahibon();
match portable {
sassi::JSahibON::Object(obj) => {
assert_eq!(obj.len(), 2);
let keys: Vec<&str> = obj.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, ["a", "b"]);
}
other => panic!("expected Object, got {other:?}"),
}
}
#[test]
fn try_from_json_value_rejects_out_of_range_numbers() {
let huge_str = format!("1{}", "0".repeat(500));
let huge: serde_json::Value =
serde_json::from_str(&huge_str).expect("arbitrary-precision number parses");
let err = MirJzSON::try_from(huge).expect_err("oversized number must fail");
match err {
MirJzSONError::UnsupportedJsonValue(_) => {}
other => panic!("expected UnsupportedJsonValue, got {other:?}"),
}
}
#[test]
fn into_json_value_is_total_for_all_carriers() {
let cases = vec![
sassi::JSahibON::Null,
sassi::JSahibON::Bool(false),
sassi::JSahibON::I64(-7),
sassi::JSahibON::U64(u64::MAX),
sassi::JSahibON::F64(sassi::JFiniteF64::try_new(2.5).unwrap()),
sassi::JSahibON::String("hello".to_string()),
sassi::JSahibON::Array(vec![sassi::JSahibON::I64(1), sassi::JSahibON::I64(2)]),
];
for portable in cases {
let mir: MirJzSON = portable.clone().into();
let json: serde_json::Value = mir.into();
let mir2 = MirJzSON::try_from(json).expect("projected value must round-trip");
assert_eq!(mir2.into_jsahibon(), portable);
}
}
#[test]
fn json_null_and_missing_stay_distinct() {
let mir: MirJzSON = sassi::JSahibON::Null.into();
let json: serde_json::Value = mir.into();
assert_eq!(json, serde_json::Value::Null);
let back = MirJzSON::try_from(json).unwrap();
assert_eq!(back.into_jsahibon(), sassi::JSahibON::Null);
}
}