use std::ops::Deref;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use super::conformance::Bo4eConformance;
use super::gate::{Bo4eRejection, Bo4eSerialiseError, Bo4eTyped, decode};
pub const REJECTION_SENTINEL: &str = "\u{1}bo4e-rejection\u{1}";
#[derive(Debug, Clone, PartialEq)]
pub struct Bo4e<T>(T);
impl<T> Bo4e<T> {
pub const fn get(&self) -> &T {
&self.0
}
#[allow(clippy::missing_const_for_fn)] pub fn into_inner(self) -> T {
self.0
}
pub const fn from_built(value: T) -> Self {
Self(value)
}
}
impl<T> Bo4e<T>
where
T: Bo4eTyped + Serialize,
{
pub fn canonical_json(&self) -> Result<serde_json::Value, Bo4eSerialiseError> {
super::to_canonical_json(&self.0)
}
}
impl<T> Deref for Bo4e<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> AsRef<T> for Bo4e<T> {
fn as_ref(&self) -> &T {
&self.0
}
}
impl<T: Serialize> Serialize for Bo4e<T> {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.0.serialize(s)
}
}
impl<'de, T> Deserialize<'de> for Bo4e<T>
where
T: serde::de::DeserializeOwned + Bo4eTyped + rubo4e::Bo4eStrict + Bo4eConformance,
T: rubo4e::prelude::Validate<Context = ()> + rubo4e::json::Bo4eJsonExt,
{
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = serde_json::Value::deserialize(d)?;
decode::<T>(raw)
.map(Self)
.map_err(|e| D::Error::custom(encode_rejection(&e)))
}
}
fn encode_rejection(e: &Bo4eRejection) -> String {
let mut detail = e.detail();
detail.insert("message".into(), e.to_string().into());
format!(
"{e}{REJECTION_SENTINEL}{}",
serde_json::Value::Object(detail)
)
}
#[must_use]
pub fn recover_rejection(message: &str) -> Option<(String, serde_json::Value)> {
let (_, rest) = message.split_once(REJECTION_SENTINEL)?;
let mut detail = serde_json::Deserializer::from_str(rest)
.into_iter::<serde_json::Value>()
.next()?
.ok()?;
let obj = detail.as_object_mut()?;
let sentence = obj.remove("message")?.as_str()?.to_owned();
Some((sentence, detail))
}
#[cfg(test)]
mod tests {
use super::{Bo4e, recover_rejection};
use rubo4e::current::{Geschaeftspartner, Marktlokation};
#[derive(Debug, serde::Deserialize)]
struct Req {
geschaeftspartner: Bo4e<Geschaeftspartner>,
}
#[test]
fn a_field_deserialises_through_the_gate_and_canonicalises() {
let req: Req = serde_json::from_value(serde_json::json!({
"geschaeftspartner": { "organisationsname": "Demo Energie GmbH" }
}))
.expect("a valid Geschaeftspartner");
assert_eq!(
req.geschaeftspartner.organisationsname.as_deref(),
Some("Demo Energie GmbH")
);
let stored = req
.geschaeftspartner
.canonical_json()
.expect("serialisable");
assert_eq!(stored["_typ"], "GESCHAEFTSPARTNER");
}
#[test]
fn a_wrong_discriminator_is_refused_during_deserialisation() {
let err = serde_json::from_value::<Req>(serde_json::json!({
"geschaeftspartner": { "_typ": "MARKTLOKATION" }
}))
.expect_err("a Marktlokation is not a Geschaeftspartner");
let (sentence, detail) =
recover_rejection(&err.to_string()).expect("the rejection survives serde");
assert_eq!(detail["code"], "bo4e.discriminator");
assert_eq!(detail["expected_typ"], "GESCHAEFTSPARTNER");
assert_eq!(detail["found_typ"], "MARKTLOKATION");
assert!(
sentence.starts_with("expected a BO4E GESCHAEFTSPARTNER"),
"the sentence is the rejection's own, without serde's field prefix: {sentence}"
);
}
#[test]
fn an_out_of_schema_enum_names_its_path() {
let err = serde_json::from_value::<Bo4e<Marktlokation>>(serde_json::json!({
"marktlokationsId": "51238696781", "sparte": "STROMM"
}))
.expect_err("STROMM is not a Sparte");
let (_, detail) = recover_rejection(&err.to_string()).expect("a recoverable rejection");
assert_eq!(detail["code"], "bo4e.unknown_enum");
assert_eq!(detail["paths"][0], "sparte");
}
#[test]
fn an_unrelated_serde_error_is_not_recovered() {
let err = serde_json::from_value::<Req>(serde_json::json!({})).expect_err("field missing");
assert!(recover_rejection(&err.to_string()).is_none());
}
#[test]
fn a_built_value_serialises_as_the_document() {
let gp = Geschaeftspartner {
organisationsname: Some("Demo".into()),
..Default::default()
};
let wrapped = Bo4e::from_built(gp.clone());
assert_eq!(
serde_json::to_value(&wrapped).expect("serialisable"),
serde_json::to_value(&gp).expect("serialisable")
);
}
}