pub mod mirjzson;
pub mod path;
pub mod schema;
pub mod unknown;
pub use mirjzson::{MirJzSON, MirJzSONError};
pub use path::{JsonbPathComparable, JsonbPathRef, JsonbSqlCast};
pub use schema::JsonbSchema;
pub use unknown::{UnknownField, UnknownFieldError, UnknownFieldExt};
use bytes::BytesMut;
use indexmap::IndexMap;
use postgres_types::{FromSql, IsNull, ToSql, Type};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct Jsonb<T> {
pub data: T,
pub(crate) extra: IndexMap<String, UnknownField>,
}
impl<T: Default> Default for Jsonb<T> {
fn default() -> Self {
Jsonb {
data: T::default(),
extra: IndexMap::new(),
}
}
}
impl<T> Jsonb<T> {
pub fn new(data: T) -> Self {
Jsonb {
data,
extra: IndexMap::new(),
}
}
pub fn extra(&self) -> &IndexMap<String, UnknownField> {
&self.extra
}
}
impl<T> Jsonb<T>
where
T: serde::Serialize,
{
pub fn to_jsahibon(&self) -> Result<sassi::JSahibON, MirJzSONError> {
let value: serde_json::Value =
serde_json::to_value(self).map_err(|err| MirJzSONError::JsonDecode(err.to_string()))?;
sassi::JSahibON::try_from(value)
.map_err(|err| MirJzSONError::UnsupportedJsonValue(err.to_string()))
}
}
impl<T: Serialize> Serialize for Jsonb<T> {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut map = match serde_json::to_value(&self.data).map_err(serde::ser::Error::custom)? {
serde_json::Value::Object(m) => m,
other => {
return other.serialize(serializer);
}
};
for (k, v) in &self.extra {
map.entry(k.clone()).or_insert_with(|| v.clone());
}
serde_json::Value::Object(map).serialize(serializer)
}
}
impl<'de, T> Deserialize<'de> for Jsonb<T>
where
T: Deserialize<'de> + Serialize,
{
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(deserializer)?;
let raw_obj = match &raw {
serde_json::Value::Object(m) => m,
_ => {
let data = T::deserialize(raw).map_err(serde::de::Error::custom)?;
return Ok(Jsonb {
data,
extra: IndexMap::new(),
});
}
};
let data: T = T::deserialize(raw.clone()).map_err(serde::de::Error::custom)?;
let known_keys: std::collections::HashSet<String> =
match serde_json::to_value(&data).map_err(serde::de::Error::custom)? {
serde_json::Value::Object(m) => m.keys().cloned().collect(),
_ => std::collections::HashSet::new(),
};
let mut extra = IndexMap::new();
for (k, v) in raw_obj {
if !known_keys.contains(k) {
extra.insert(k.clone(), v.clone());
}
}
Ok(Jsonb { data, extra })
}
}
impl<T> ToSql for Jsonb<T>
where
T: Serialize + std::fmt::Debug,
{
fn to_sql(
&self,
ty: &Type,
out: &mut BytesMut,
) -> Result<IsNull, Box<dyn std::error::Error + Sync + Send>> {
let value: serde_json::Value = serde_json::to_value(self)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Sync + Send>)?;
value.to_sql(ty, out)
}
fn accepts(ty: &Type) -> bool {
<serde_json::Value as ToSql>::accepts(ty)
}
postgres_types::to_sql_checked!();
}
impl<'a, T> FromSql<'a> for Jsonb<T>
where
T: for<'de> Deserialize<'de> + Serialize,
{
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)?;
let jsonb = serde_json::from_value(value)
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Sync + Send>)?;
Ok(jsonb)
}
fn accepts(ty: &Type) -> bool {
<serde_json::Value as FromSql>::accepts(ty)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct KnownSpec {
name: String,
value: i32,
}
#[test]
fn new_creates_empty_extra() {
let j = Jsonb::new(KnownSpec {
name: "x".to_string(),
value: 1,
});
assert!(j.extra().is_empty());
}
#[test]
fn serialize_produces_flat_object() {
let j = Jsonb {
data: KnownSpec {
name: "x".to_string(),
value: 1,
},
extra: {
let mut m = IndexMap::new();
m.insert("extra_key".to_string(), json!("extra_val"));
m
},
};
let v = serde_json::to_value(&j).unwrap();
assert_eq!(v["name"], json!("x"));
assert_eq!(v["value"], json!(1));
assert_eq!(v["extra_key"], json!("extra_val"));
}
#[test]
fn deserialize_splits_known_and_unknown() {
let raw = json!({
"name": "engine",
"value": 42,
"experimental": true,
"future_field": "hello"
});
let j: Jsonb<KnownSpec> = serde_json::from_value(raw).unwrap();
assert_eq!(j.data.name, "engine");
assert_eq!(j.data.value, 42);
assert_eq!(j.extra.len(), 2);
assert_eq!(j.extra.get("experimental").unwrap(), &json!(true));
assert_eq!(j.extra.get("future_field").unwrap(), &json!("hello"));
}
#[test]
fn round_trip_preserves_unknown_fields() {
let raw = json!({
"name": "test",
"value": 7,
"legacy": {"nested": 99}
});
let j: Jsonb<KnownSpec> = serde_json::from_value(raw).unwrap();
let mut j2 = j.clone();
j2.data.value = 100;
let out = serde_json::to_value(&j2).unwrap();
assert_eq!(out["value"], json!(100));
assert_eq!(out["legacy"], json!({"nested": 99}));
}
#[test]
fn deserialize_non_object_falls_back_gracefully() {
let raw = json!(42);
let j: Jsonb<serde_json::Value> = serde_json::from_value(raw).unwrap();
assert_eq!(j.data, json!(42));
assert!(j.extra.is_empty());
}
#[test]
fn to_jsahibon_merges_data_and_extra() {
let mut extra = IndexMap::new();
extra.insert("legacy_key".to_string(), json!("legacy_value"));
let j = Jsonb {
data: KnownSpec {
name: "engine".to_string(),
value: 42,
},
extra,
};
let portable = j
.to_jsahibon()
.expect("typed data + unknown extra must project");
match portable {
sassi::JSahibON::Object(obj) => {
let keys: Vec<&str> = obj.iter().map(|(k, _)| k.as_str()).collect();
assert_eq!(keys, ["name", "value", "legacy_key"]);
match obj
.iter()
.find(|(k, _)| k.as_str() == "value")
.map(|(_, v)| v.clone())
.unwrap()
{
sassi::JSahibON::I64(42) => {}
other => panic!("expected I64(42), got {other:?}"),
}
}
other => panic!("expected Object, got {other:?}"),
}
}
#[test]
fn to_jsahibon_handles_non_object_serializations() {
let j = Jsonb {
data: json!(7),
extra: IndexMap::new(),
};
let portable = j.to_jsahibon().expect("non-object T must still project");
match portable {
sassi::JSahibON::I64(7) => {}
other => panic!("expected I64(7), got {other:?}"),
}
}
#[test]
fn to_jsahibon_round_trips_through_mirjzson() {
let j = Jsonb {
data: KnownSpec {
name: "x".to_string(),
value: 9,
},
extra: IndexMap::new(),
};
let portable = j.to_jsahibon().unwrap();
let mir: crate::jsonb::MirJzSON = portable.into();
let back: serde_json::Value = mir.into();
assert_eq!(back["name"], json!("x"));
assert_eq!(back["value"], json!(9));
}
}