use cratestack_core::CoolError;
use crate::codec::HttpClientCodec;
use crate::rpc::batch::BatchBuilder;
use crate::rpc::client::RpcClient;
use crate::rpc::error::RpcClientError;
fn strip_json_null_entries(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
map.retain(|_, child| !child.is_null());
for child in map.values_mut() {
strip_json_null_entries(child);
}
}
serde_json::Value::Array(items) => {
for item in items.iter_mut() {
strip_json_null_entries(item);
}
}
_ => {}
}
}
#[must_use = "BatchableCall does nothing until `.await`ed or `.queue(&mut batch)`d"]
pub struct BatchableCall<C, O> {
rpc: RpcClient<C>,
op_id: String,
input_value: Result<serde_json::Value, CoolError>,
_output: std::marker::PhantomData<fn() -> O>,
}
impl<C, O> BatchableCall<C, O>
where
C: HttpClientCodec + Clone + Send + 'static,
O: serde::de::DeserializeOwned + Send + 'static,
{
pub fn new<I>(rpc: RpcClient<C>, op_id: impl Into<String>, input: &I) -> Self
where
I: serde::Serialize,
{
let input_value = serde_json::to_value(input)
.map(|mut value| {
strip_json_null_entries(&mut value);
value
})
.map_err(|error| CoolError::Codec(format!("encode batch input: {error}")));
Self {
rpc,
op_id: op_id.into(),
input_value,
_output: std::marker::PhantomData,
}
}
pub fn queue(self, batch: &mut BatchBuilder<C>) -> BatchHandle<O> {
let id = match self.input_value {
Ok(value) => batch.push_frame(self.op_id, value),
Err(error) => batch.push_failed_frame(error),
};
BatchHandle {
id,
_output: std::marker::PhantomData,
}
}
}
impl<C, O> std::future::IntoFuture for BatchableCall<C, O>
where
C: HttpClientCodec + Clone + Send + 'static,
O: serde::de::DeserializeOwned + Send + 'static,
{
type Output = Result<O, RpcClientError>;
type IntoFuture = std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
let value = self.input_value.map_err(RpcClientError::Codec)?;
self.rpc
.call::<serde_json::Value, O>(&self.op_id, &value)
.await
})
}
}
pub struct BatchHandle<O> {
pub(crate) id: u64,
pub(crate) _output: std::marker::PhantomData<fn() -> O>,
}
impl<O> Clone for BatchHandle<O> {
fn clone(&self) -> Self {
Self {
id: self.id,
_output: std::marker::PhantomData,
}
}
}
impl<O> Copy for BatchHandle<O> {}
impl<O> std::fmt::Debug for BatchHandle<O> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BatchHandle").field("id", &self.id).finish()
}
}
#[cfg(test)]
mod null_strip_tests {
use super::strip_json_null_entries;
use cratestack_codec_cbor::CborCodec;
use cratestack_core::CoolCodec;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Req {
required: String,
#[serde(default)]
optional: Option<String>,
#[serde(default)]
nested: Option<Inner>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Inner {
#[serde(default)]
maybe: Option<String>,
kept: String,
}
#[test]
fn stripped_none_optionals_round_trip_through_cbor() {
let req = Req {
required: "x".to_owned(),
optional: None,
nested: Some(Inner { maybe: None, kept: "k".to_owned() }),
};
let mut value = serde_json::to_value(&req).expect("to_value");
assert!(value.get("optional").expect("present").is_null());
strip_json_null_entries(&mut value);
assert!(value.get("optional").is_none(), "top-level null entry dropped");
assert!(
value["nested"].get("maybe").is_none(),
"nested null entry dropped"
);
assert_eq!(value["nested"]["kept"], serde_json::json!("k"));
let bytes = CborCodec.encode(&value).expect("encode");
let decoded: Req = CborCodec.decode(&bytes).expect("decode");
assert_eq!(decoded, req);
}
#[test]
fn unstripped_null_breaks_typed_decode() {
let value = serde_json::json!({ "required": "x", "optional": null });
let bytes = CborCodec.encode(&value).expect("encode");
assert!(
CborCodec.decode::<Req>(&bytes).is_err(),
"unstripped Value::Null must break the typed decode"
);
}
}