use std::collections::BTreeMap;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::value::RawValue;
use serde_json::{Map, Value};
use crate::error::{DurableError, EngineOnly, Error, Result};
use crate::sysdb::types::EncodedValue;
mod non_finite;
mod shape;
pub(crate) use non_finite::{Floats, NonFinite};
use shape::{Shape, takes_the_envelope};
pub(crate) const RUST_SERDE: &str = "rust_serde";
pub(crate) use crate::sysdb::PORTABLE_JSON;
const ENVELOPE: &str = "dbos::PortableWorkflowArgs";
pub trait CustomSerializer: Send + Sync + std::fmt::Debug {
fn name(&self) -> &str;
fn serialize(
&self,
value: Payload<'_>,
) -> std::result::Result<String, Box<dyn std::error::Error + Send + Sync>>;
fn deserialize(
&self,
text: &str,
into: PayloadSlot<'_>,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
pub struct Payload<'a>(&'a dyn erased_serde::Serialize);
impl Serialize for Payload<'_> {
fn serialize<S: serde::Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
erased_serde::serialize(self.0, serializer)
}
}
impl std::fmt::Debug for Payload<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("Payload")
}
}
pub struct PayloadSlot<'a> {
visit: &'a mut dyn for<'de> FnMut(
&mut dyn erased_serde::Deserializer<'de>,
) -> std::result::Result<(), erased_serde::Error>,
}
impl PayloadSlot<'_> {
pub fn fill<'de, D: Deserializer<'de>>(
self,
deserializer: D,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut erased = <dyn erased_serde::Deserializer>::erase(deserializer);
(self.visit)(&mut erased).map_err(|error| error.to_string().into())
}
}
impl std::fmt::Debug for PayloadSlot<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("PayloadSlot")
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub enum Serializer {
#[default]
RustSerde,
Custom(Arc<dyn CustomSerializer>),
}
impl PartialEq for Serializer {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Serializer::RustSerde, Serializer::RustSerde) => true,
(Serializer::Custom(a), Serializer::Custom(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
}
impl Eq for Serializer {}
impl Serializer {
pub fn custom(serializer: impl CustomSerializer + 'static) -> Self {
Serializer::Custom(Arc::new(serializer))
}
pub fn name(&self) -> &str {
match self {
Serializer::RustSerde => RUST_SERDE,
Serializer::Custom(custom) => custom.name(),
}
}
pub(crate) fn encoding(&self) -> PayloadEncoding {
match self {
Serializer::RustSerde => PayloadEncoding::Native,
Serializer::Custom(custom) => PayloadEncoding::Custom(Arc::clone(custom)),
}
}
pub(crate) fn validate(&self) -> crate::Result<()> {
let Serializer::Custom(custom) = self else {
return Ok(());
};
match custom.name() {
"" => Err(crate::Error::Config(
"a custom serializer's name cannot be empty".to_owned(),
)),
name @ (RUST_SERDE | PORTABLE_JSON) => Err(crate::Error::Config(format!(
"a custom serializer cannot be named {name}, which is a built-in encoding"
))),
name if name.contains('\0') => Err(crate::Error::Config(format!(
"a custom serializer's name cannot hold a NUL byte: {name:?}"
))),
_ => Ok(()),
}
}
}
impl crate::sysdb::types::StepCodec for Serializer {
fn encode(&self, value: Value) -> std::result::Result<EncodedValue, crate::sysdb::Error> {
let encoding = self.encoding();
let text = encode::<_, EngineOnly>(&value, &encoding, "checkpoint").map_err(to_sysdb)?;
Ok(EncodedValue {
value: text,
serialization: Some(encoding.name().to_owned()),
})
}
fn decode(
&self,
value: &str,
serialization: Option<&str>,
) -> std::result::Result<Value, crate::sysdb::Error> {
decode_tagged::<Value, EngineOnly>(Some(value), serialization, self, "checkpoint")
.map_err(to_sysdb)
}
}
fn to_sysdb(error: Error) -> crate::sysdb::Error {
match error {
Error::UnknownSerialization { name } => crate::sysdb::Error::UnknownSerialization { name },
other => crate::sysdb::Error::Malformed(other.to_string()),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SerializationFormat {
#[default]
Default,
Portable,
Native,
}
#[derive(Debug, Clone, Default)]
pub(crate) enum PayloadEncoding {
#[default]
Native,
Portable,
Custom(Arc<dyn CustomSerializer>),
}
impl PartialEq for PayloadEncoding {
fn eq(&self, other: &Self) -> bool {
self.name() == other.name()
}
}
impl PayloadEncoding {
pub(crate) fn name(&self) -> &str {
match self {
PayloadEncoding::Native => RUST_SERDE,
PayloadEncoding::Portable => PORTABLE_JSON,
PayloadEncoding::Custom(custom) => custom.name(),
}
}
pub(crate) fn of<E>(tag: Option<&str>, configured: &Serializer) -> Result<PayloadEncoding, E> {
match tag {
None | Some("") | Some(RUST_SERDE) => Ok(PayloadEncoding::Native),
Some(PORTABLE_JSON) => Ok(PayloadEncoding::Portable),
Some(other) => match configured {
Serializer::Custom(custom) if custom.name() == other => {
Ok(PayloadEncoding::Custom(Arc::clone(custom)))
}
_ => Err(Error::UnknownSerialization {
name: other.to_owned(),
}),
},
}
}
pub(crate) fn resolve(
requested: SerializationFormat,
inherited: PayloadEncoding,
) -> PayloadEncoding {
match requested {
SerializationFormat::Default => inherited,
SerializationFormat::Portable => PayloadEncoding::Portable,
SerializationFormat::Native => PayloadEncoding::Native,
}
}
pub(crate) fn resolve_ambient(
requested: SerializationFormat,
configured: &Serializer,
) -> PayloadEncoding {
let inherited = crate::context::Ctx::with_current(|ctx| {
ctx.map_or_else(|| configured.encoding(), crate::context::Ctx::encoding)
});
PayloadEncoding::resolve(requested, inherited)
}
}
pub(crate) fn decode<T: DeserializeOwned, E>(
value: Option<&str>,
encoding: &PayloadEncoding,
what: &'static str,
) -> Result<T, E> {
match (value, encoding) {
(Some(text), PayloadEncoding::Custom(custom)) => {
decode_custom(&**custom, text).map_err(|message| Error::Deserialization {
what: what.into(),
message,
source: None,
})
}
(_, PayloadEncoding::Native) => non_finite::from_str(value.unwrap_or("null"))
.map_err(|source| Error::decode_failed(what, source)),
_ => serde_json::from_str(value.unwrap_or("null"))
.map_err(|source| Error::decode_failed(what, source)),
}
}
fn decode_custom<T: DeserializeOwned>(
custom: &dyn CustomSerializer,
text: &str,
) -> std::result::Result<T, String> {
let mut decoded = None;
let mut visit = |deserializer: &mut dyn erased_serde::Deserializer<'_>| {
decoded = Some(erased_serde::deserialize::<T>(deserializer)?);
Ok(())
};
custom
.deserialize(text, PayloadSlot { visit: &mut visit })
.map_err(|error| error.to_string())?;
decoded.ok_or_else(|| {
format!(
"serializer {} returned without filling its PayloadSlot",
custom.name()
)
})
}
pub(crate) fn decode_tagged<T: DeserializeOwned, E>(
value: Option<&str>,
tag: Option<&str>,
configured: &Serializer,
what: &'static str,
) -> Result<T, E> {
decode(value, &PayloadEncoding::of(tag, configured)?, what)
}
pub(crate) fn encode<T: Serialize, E>(
value: &T,
encoding: &PayloadEncoding,
what: &'static str,
) -> Result<String, E> {
match encoding {
PayloadEncoding::Custom(custom) => {
let text = custom
.serialize(Payload(value))
.map_err(|error| Error::Serialization {
what: what.into(),
message: error.to_string(),
source: None,
})?;
refuse_nul(text, what)
}
PayloadEncoding::Native | PayloadEncoding::Portable => {
let mode = if matches!(encoding, PayloadEncoding::Native) {
NonFinite::Name
} else {
NonFinite::Refuse
};
serde_json::to_string(&Floats::new(value, mode))
.map_err(|source| Error::encode_failed(what, source))
}
}
}
fn refuse_nul<E>(text: String, what: &'static str) -> Result<String, E> {
if !text.contains('\0') {
return Ok(text);
}
Err(Error::Serialization {
what: what.into(),
message: "the serializer wrote a NUL byte, which a text column cannot store".to_owned(),
source: None,
})
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename = "dbos::PortableWorkflowArgs")]
pub struct PortableWorkflowArgs {
#[serde(
rename = "positionalArgs",
default,
deserialize_with = "null_as_default"
)]
pub positional_args: Vec<Value>,
#[serde(rename = "namedArgs", default, deserialize_with = "null_as_default")]
pub named_args: Map<String, Value>,
}
fn null_as_default<'de, D, T>(deserializer: D) -> std::result::Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
}
#[derive(Serialize)]
struct Envelope<'a, A: ?Sized> {
#[serde(rename = "positionalArgs")]
positional_args: &'a A,
#[serde(rename = "namedArgs")]
named_args: Map<String, Value>,
}
pub(crate) fn encode_input<P: Serialize, E>(
input: &P,
encoding: &PayloadEncoding,
) -> Result<String, E> {
const WHAT: &str = "argument";
if !matches!(encoding, PayloadEncoding::Portable) {
return encode(input, encoding, WHAT);
}
match Shape::of(input) {
Shape::Arguments => encode_envelope(input, encoding),
Shape::NoArguments => encode_envelope(&[(); 0], encoding),
Shape::Envelope => encode(input, encoding, WHAT),
Shape::OneArgument => encode_envelope(&[input], encoding),
}
}
fn encode_envelope<A: Serialize + ?Sized, E>(
positional_args: &A,
encoding: &PayloadEncoding,
) -> Result<String, E> {
encode(
&Envelope {
positional_args,
named_args: Map::new(),
},
encoding,
"argument",
)
}
pub(crate) fn decode_input<P: DeserializeOwned + 'static, E>(
value: Option<&str>,
encoding: &PayloadEncoding,
) -> Result<P, E> {
const WHAT: &str = "argument";
if !matches!(encoding, PayloadEncoding::Portable) {
return decode(value, encoding, WHAT);
}
let stored = value.filter(|value| *value != "null").unwrap_or("{}");
if takes_the_envelope::<P>() {
return decode(Some(stored), encoding, WHAT);
}
let RawEnvelope { positional, named } =
serde_json::from_str(stored).map_err(|source| Error::decode_failed(WHAT, source))?;
let from = match (positional.len(), named.is_empty()) {
(0, true) => "null".to_owned(),
(1, true) => positional[0].get().to_owned(),
(_, true) => {
let elements: Vec<&str> = positional.iter().map(|arg| arg.get()).collect();
format!("[{}]", elements.join(","))
}
(0, false) => {
let fields: Vec<String> = named
.iter()
.map(|(name, arg)| {
let name = serde_json::to_string(name).expect("a string is JSON");
format!("{name}:{}", arg.get())
})
.collect();
format!("{{{}}}", fields.join(","))
}
(_, false) => {
return Err(Error::Deserialization {
what: WHAT.into(),
message: "a portable input with both positional and named arguments cannot be \
decoded into one value"
.to_owned(),
source: None,
});
}
};
serde_json::from_str(&from).map_err(|source| Error::decode_failed(WHAT, source))
}
#[derive(Deserialize)]
struct RawEnvelope<'a> {
#[serde(
rename = "positionalArgs",
default,
borrow,
deserialize_with = "null_as_default"
)]
positional: Vec<&'a RawValue>,
#[serde(
rename = "namedArgs",
default,
borrow,
deserialize_with = "null_as_default"
)]
named: BTreeMap<String, &'a RawValue>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PortableError {
name: String,
#[serde(default, deserialize_with = "null_as_default")]
message: String,
#[serde(default)]
code: Option<Value>,
#[serde(default)]
data: Option<Box<RawValue>>,
}
pub(crate) fn encode_error<E: DurableError, E2>(
error: &Error<E>,
encoding: &PayloadEncoding,
) -> Result<String, E2> {
match encoding {
PayloadEncoding::Portable => {
let portable =
to_portable(error).map_err(|source| Error::encode_failed("error", source))?;
encode(&portable, encoding, "error")
}
PayloadEncoding::Native | PayloadEncoding::Custom(_) => encode(error, encoding, "error"),
}
}
fn to_portable<E: DurableError>(
error: &Error<E>,
) -> std::result::Result<PortableError, serde_json::Error> {
Ok(match error {
Error::Application(application) => PortableError {
name: type_name::<E>().to_owned(),
message: application.to_string(),
code: None,
data: present(serde_json::value::to_raw_value(&Floats::new(
application,
NonFinite::Refuse,
))?),
},
Error::Portable {
name,
message,
code,
data,
} => PortableError {
name: name.clone(),
message: message.clone(),
code: code.clone(),
data: data
.as_ref()
.map(serde_json::value::to_raw_value)
.transpose()?,
},
engine => {
let (name, data) = variant(engine)?;
PortableError {
name,
message: engine.to_string(),
code: None,
data: data
.map(|data| serde_json::value::to_raw_value(&data))
.transpose()?
.and_then(present),
}
}
})
}
fn present(data: Box<RawValue>) -> Option<Box<RawValue>> {
(data.get() != "null").then_some(data)
}
const DBOS_ERRORS: &str = "dbos::";
fn type_name<E>() -> &'static str {
let full = std::any::type_name::<E>();
full.split('<').next().unwrap_or(full)
}
fn variant<E: DurableError>(
error: &Error<E>,
) -> std::result::Result<(String, Option<Value>), serde_json::Error> {
let (name, fields) = match serde_json::to_value(Floats::new(error, NonFinite::Refuse))? {
Value::String(unit) => (unit, None),
Value::Object(tagged) if tagged.len() == 1 => {
let (name, fields) = tagged.into_iter().next().expect("one entry");
(name, Some(fields))
}
_ => ("Error".to_owned(), None),
};
Ok((format!("{DBOS_ERRORS}{name}"), fields))
}
fn rebuild<E: DurableError>(name: &str, data: Option<&RawValue>) -> Option<Error<E>> {
let data = data.map_or("null", RawValue::get);
if name == type_name::<E>()
&& let Ok(application) = serde_json::from_str::<E>(data)
{
return Some(Error::Application(application));
}
let variant = serde_json::to_string(name.strip_prefix(DBOS_ERRORS)?).expect("a string is JSON");
let tagged = match data {
"null" => variant,
fields => format!("{{{variant}:{fields}}}"),
};
serde_json::from_str(&tagged).ok()
}
pub(crate) fn decode_error<E: DurableError>(
encoded: &str,
encoding: &PayloadEncoding,
) -> Option<Error<E>> {
match encoding {
PayloadEncoding::Native => {
decode::<Error<E>, EngineOnly>(Some(encoded), encoding, "error").ok()
}
PayloadEncoding::Custom(_) => {
Some(decode::<Error<E>, E>(Some(encoded), encoding, "error").unwrap_or_else(|e| e))
}
PayloadEncoding::Portable => {
let PortableError {
name,
message,
code,
data,
} = serde_json::from_str(encoded).ok()?;
if let Some(rebuilt) = rebuild(&name, data.as_deref()) {
return Some(rebuilt);
}
Some(Error::Portable {
name,
message,
code,
data: data.and_then(|data| serde_json::from_str(data.get()).ok()),
})
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct EncodedOutput {
pub(crate) encoded: Option<String>,
pub(crate) encoding: PayloadEncoding,
}
impl EncodedOutput {
pub(crate) fn decode<R: DeserializeOwned, E>(&self) -> Result<R, E> {
decode(self.encoded.as_deref(), &self.encoding, "result")
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
use crate::error::EngineOnly;
#[test]
fn an_absent_value_decodes_as_the_unit() {
assert_eq!(
decode::<(), EngineOnly>(None, &PayloadEncoding::Native, "argument").unwrap(),
()
);
assert_eq!(
decode::<Option<u32>, EngineOnly>(None, &PayloadEncoding::Native, "argument").unwrap(),
None
);
}
#[test]
fn a_value_round_trips() {
let encoded =
encode::<_, EngineOnly>(&(1u32, "two"), &PayloadEncoding::Native, "argument").unwrap();
assert_eq!(encoded, r#"[1,"two"]"#);
assert_eq!(
decode::<(u32, String), EngineOnly>(
Some(&encoded),
&PayloadEncoding::Native,
"argument"
)
.unwrap(),
(1, "two".to_owned())
);
}
#[test]
fn the_unit_encodes_as_a_value_rather_than_an_absence() {
assert_eq!(
encode::<_, EngineOnly>(&(), &PayloadEncoding::Native, "argument").unwrap(),
"null"
);
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Order {
sku: String,
quantity: u32,
}
fn portable_input<P: DeserializeOwned + 'static>(stored: &str) -> Result<P> {
decode_input(Some(stored), &PayloadEncoding::Portable)
}
#[test]
fn the_serializer_names_itself_as_the_column_records_it() {
assert_eq!(Serializer::default().name(), "rust_serde");
}
#[test]
fn a_tag_names_its_format_and_an_unknown_one_is_refused() {
for tag in [None, Some(""), Some("rust_serde")] {
assert_eq!(
PayloadEncoding::of::<EngineOnly>(tag, &Serializer::RustSerde).unwrap(),
PayloadEncoding::Native,
"{tag:?}"
);
}
assert_eq!(
PayloadEncoding::of::<EngineOnly>(Some(PORTABLE_JSON), &Serializer::RustSerde).unwrap(),
PayloadEncoding::Portable
);
let err = PayloadEncoding::of::<EngineOnly>(Some("py_pickle"), &Serializer::RustSerde)
.unwrap_err();
assert!(
matches!(&err, Error::UnknownSerialization { name } if name == "py_pickle"),
"{err}"
);
assert_eq!(err.to_string(), "serialization py_pickle is not available");
}
#[test]
fn a_portable_input_is_the_envelopes_one_positional_argument() {
let order = Order {
sku: "tea".to_owned(),
quantity: 2,
};
let encoded = encode_input::<_, EngineOnly>(&order, &PayloadEncoding::Portable).unwrap();
assert_eq!(
encoded,
r#"{"positionalArgs":[{"sku":"tea","quantity":2}],"namedArgs":{}}"#
);
assert_eq!(portable_input::<Order>(&encoded).unwrap(), order);
assert_eq!(
encode_input::<_, EngineOnly>(&order, &PayloadEncoding::Native).unwrap(),
r#"{"sku":"tea","quantity":2}"#
);
}
fn portable_round_trip<P>(input: P, expected: &str)
where
P: Serialize + DeserializeOwned + PartialEq + std::fmt::Debug + 'static,
{
let encoded = encode_input::<_, EngineOnly>(&input, &PayloadEncoding::Portable).unwrap();
assert_eq!(encoded, expected);
assert_eq!(portable_input::<P>(&encoded).unwrap(), input);
}
#[test]
fn a_tuple_is_written_as_one_argument_per_element() {
portable_round_trip(
("a".to_owned(), 1i64),
r#"{"positionalArgs":["a",1],"namedArgs":{}}"#,
);
portable_round_trip(
(1u8, (2u8, 3u8), vec![4u8]),
r#"{"positionalArgs":[1,[2,3],[4]],"namedArgs":{}}"#,
);
}
#[test]
fn the_unit_is_written_as_no_arguments() {
portable_round_trip((), r#"{"positionalArgs":[],"namedArgs":{}}"#);
}
#[test]
fn a_borrowed_or_boxed_unit_is_no_arguments() {
const NONE: &str = r#"{"positionalArgs":[],"namedArgs":{}}"#;
let portable = PayloadEncoding::Portable;
assert_eq!(
encode_input::<_, EngineOnly>(&&(), &portable).unwrap(),
NONE
);
assert_eq!(
encode_input::<_, EngineOnly>(&Box::new(()), &portable).unwrap(),
NONE
);
}
#[test]
fn none_is_one_null_argument() {
portable_round_trip(None::<u32>, r#"{"positionalArgs":[null],"namedArgs":{}}"#);
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Pair(String, i64);
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Nothing;
#[test]
fn anything_else_is_one_argument() {
portable_round_trip((7u32,), r#"{"positionalArgs":[[7]],"namedArgs":{}}"#);
portable_round_trip(
Pair("a".to_owned(), 1),
r#"{"positionalArgs":[["a",1]],"namedArgs":{}}"#,
);
portable_round_trip(vec![1u8, 2], r#"{"positionalArgs":[[1,2]],"namedArgs":{}}"#);
portable_round_trip(
Some(("a".to_owned(), 1i64)),
r#"{"positionalArgs":[["a",1]],"namedArgs":{}}"#,
);
portable_round_trip(None::<u8>, r#"{"positionalArgs":[null],"namedArgs":{}}"#);
portable_round_trip(Nothing, r#"{"positionalArgs":[null],"namedArgs":{}}"#);
portable_round_trip(7u32, r#"{"positionalArgs":[7],"namedArgs":{}}"#);
}
#[test]
fn a_fixed_size_array_is_spread_and_a_vec_is_not() {
portable_round_trip([1u8, 2, 3], r#"{"positionalArgs":[1,2,3],"namedArgs":{}}"#);
portable_round_trip(
vec![1u8, 2, 3],
r#"{"positionalArgs":[[1,2,3]],"namedArgs":{}}"#,
);
}
#[test]
fn an_absent_input_is_an_empty_envelope() {
assert_eq!(
decode_input::<PortableWorkflowArgs, EngineOnly>(None, &PayloadEncoding::Portable)
.unwrap(),
PortableWorkflowArgs::default()
);
assert_eq!(
portable_input::<PortableWorkflowArgs>("null").unwrap(),
PortableWorkflowArgs::default()
);
}
#[test]
fn an_envelope_input_is_written_as_given() {
portable_round_trip(
PortableWorkflowArgs {
positional_args: vec![serde_json::json!(1)],
named_args: Map::from_iter([("two".to_owned(), serde_json::json!(2))]),
},
r#"{"positionalArgs":[1],"namedArgs":{"two":2}}"#,
);
}
#[test]
fn named_arguments_alone_decode_as_a_struct() {
assert_eq!(
portable_input::<Order>(
r#"{"positionalArgs":[],"namedArgs":{"quantity":3,"sku":"x"}}"#
)
.unwrap(),
Order {
sku: "x".to_owned(),
quantity: 3
}
);
}
#[test]
fn several_positional_arguments_decode_as_a_tuple() {
assert_eq!(
portable_input::<(String, u32)>(r#"{"positionalArgs":["x",3],"namedArgs":{}}"#)
.unwrap(),
("x".to_owned(), 3)
);
}
#[test]
fn no_arguments_decode_as_the_unit() {
portable_input::<()>(r#"{"positionalArgs":[],"namedArgs":{}}"#).unwrap();
portable_input::<()>("{}").unwrap();
portable_input::<()>("null").unwrap();
assert_eq!(
decode_input::<(), EngineOnly>(None, &PayloadEncoding::Portable).unwrap(),
()
);
assert_eq!(portable_input::<Option<u32>>("{}").unwrap(), None);
}
#[test]
fn positional_and_named_arguments_together_are_refused() {
let err = portable_input::<Order>(r#"{"positionalArgs":["x"],"namedArgs":{"quantity":3}}"#)
.unwrap_err();
assert!(
matches!(
err,
Error::Deserialization {
what: Cow::Borrowed("argument"),
..
}
),
"{err}"
);
}
#[test]
fn the_envelope_is_read_however_it_is_spelt() {
assert_eq!(
portable_input::<u32>(r#"{"positionalArgs" : [7], "namedArgs" : null}"#).unwrap(),
7
);
assert_eq!(
portable_input::<u32>(r#"{"namedArgs":{},"positionalArgs":[7]}"#).unwrap(),
7
);
assert_eq!(
portable_input::<u32>(r#"{"positionalArgs":[7]}"#).unwrap(),
7
);
}
#[test]
fn a_workflow_taking_the_envelope_gets_it_whole() {
let args = portable_input::<PortableWorkflowArgs>(
r#"{"positionalArgs":[1,"two"],"namedArgs":{"three":3}}"#,
)
.unwrap();
assert_eq!(
args.positional_args,
[serde_json::json!(1), serde_json::json!("two")]
);
assert_eq!(args.named_args["three"], 3);
assert_eq!(
portable_input::<PortableWorkflowArgs>(r#"{"positionalArgs":null}"#).unwrap(),
PortableWorkflowArgs::default()
);
}
#[test]
fn a_wrapped_envelope_is_read_as_it_is_written() {
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
struct Args(PortableWorkflowArgs);
let args = PortableWorkflowArgs {
positional_args: vec![serde_json::json!(1)],
named_args: Map::from_iter([("x".to_owned(), serde_json::json!(2))]),
};
let written = r#"{"positionalArgs":[1],"namedArgs":{"x":2}}"#;
portable_round_trip(Box::new(args.clone()), written);
portable_round_trip(Args(args), written);
}
#[test]
fn an_integer_past_64_bits_survives_the_envelope() {
let big = (1u128 << 64) + 7;
let small = i128::MIN + 7;
portable_round_trip(
big,
&format!(r#"{{"positionalArgs":[{big}],"namedArgs":{{}}}}"#),
);
portable_round_trip(
(big, small),
&format!(r#"{{"positionalArgs":[{big},{small}],"namedArgs":{{}}}}"#),
);
#[derive(Debug, PartialEq, Deserialize)]
struct Named {
big: u128,
}
assert_eq!(
portable_input::<Named>(&format!(r#"{{"namedArgs":{{"big":{big}}}}}"#)).unwrap(),
Named { big }
);
}
#[test]
fn an_application_error_with_a_large_integer_round_trips() {
#[derive(Debug, PartialEq, thiserror::Error, Serialize, Deserialize)]
#[error("over by {0}")]
struct Over(u128);
let error: Error<Over> = Over((1u128 << 64) + 7).into();
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
assert!(
matches!(
decode_error::<Over>(&encoded, &PayloadEncoding::Portable),
Some(Error::Application(Over(n))) if n == (1u128 << 64) + 7
),
"{encoded}"
);
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
#[error("the card was declined")]
struct CardDeclined;
fn envelope(encoded: &str) -> serde_json::Value {
serde_json::from_str(encoded).unwrap()
}
#[test]
fn an_application_error_is_written_under_its_type_name() {
let error: Error<CardDeclined> = Error::Application(CardDeclined);
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
assert_eq!(
encoded,
r#"{"name":"dbos::serialization::tests::CardDeclined","message":"the card was declined","code":null,"data":null}"#
);
}
#[test]
fn a_dbos_error_is_written_under_its_variant() {
let error: Error<CardDeclined> = Error::StepTimeout {
step: "charge".to_owned(),
timeout: std::time::Duration::from_millis(1500),
};
let encoded =
envelope(&encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap());
assert_eq!(encoded["name"], "dbos::StepTimeout");
assert_eq!(encoded["message"], error.to_string());
assert!(encoded["code"].is_null());
assert_eq!(
encoded["data"],
serde_json::to_value(&error).unwrap()["StepTimeout"],
"the variant's fields"
);
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize, PartialEq)]
enum Checkout {
#[error("declined after {attempts} attempts")]
Declined { attempts: u32 },
}
#[test]
fn an_application_errors_fields_are_its_data() {
let error: Error<Checkout> = Error::Application(Checkout::Declined { attempts: 3 });
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
assert_eq!(
encoded,
r#"{"name":"dbos::serialization::tests::Checkout","message":"declined after 3 attempts","code":null,"data":{"Declined":{"attempts":3}}}"#
);
}
#[test]
fn a_portable_error_keeps_what_it_came_with() {
let error: Error<CardDeclined> = Error::Portable {
name: "ValueError".to_owned(),
message: "bad".to_owned(),
code: Some(serde_json::json!(42)),
data: Some(serde_json::json!({"field": "x"})),
};
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
assert_eq!(
encoded,
r#"{"name":"ValueError","message":"bad","code":42,"data":{"field":"x"}}"#
);
}
#[test]
fn an_application_error_reads_back_as_itself() {
let error: Error<Checkout> = Error::Application(Checkout::Declined { attempts: 3 });
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
let read = decode_error::<Checkout>(&encoded, &PayloadEncoding::Portable).unwrap();
assert!(
matches!(read, Error::Application(Checkout::Declined { attempts: 3 })),
"{read:?}"
);
let encoded = encode_error::<_, EngineOnly>(
&Error::Application(CardDeclined),
&PayloadEncoding::Portable,
)
.unwrap();
assert!(matches!(
decode_error::<CardDeclined>(&encoded, &PayloadEncoding::Portable),
Some(Error::Application(CardDeclined))
));
}
#[test]
fn a_dbos_error_reads_back_as_itself() {
let error: Error<Checkout> = Error::StepTimeout {
step: "charge".to_owned(),
timeout: std::time::Duration::from_millis(1500),
};
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
let read = decode_error::<Checkout>(&encoded, &PayloadEncoding::Portable).unwrap();
assert!(
matches!(&read, Error::StepTimeout { step, timeout }
if step == "charge" && *timeout == std::time::Duration::from_millis(1500)),
"{read:?}"
);
}
#[test]
fn an_error_the_reader_cannot_hold_stays_portable() {
let error: Error<Checkout> = Error::Application(Checkout::Declined { attempts: 3 });
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap();
let read = decode_error::<CardDeclined>(&encoded, &PayloadEncoding::Portable).unwrap();
assert!(
matches!(&read, Error::Portable { name, data: Some(_), .. }
if name == "dbos::serialization::tests::Checkout"),
"{read:?}"
);
let stays_portable = |envelope: &str| {
let read = decode_error::<Checkout>(envelope, &PayloadEncoding::Portable).unwrap();
assert!(
matches!(read, Error::Portable { .. }),
"{envelope}: {read:?}"
);
};
stays_portable(
r#"{"name":"dbos::serialization::tests::Checkout","message":"x","data":{"field":"y"}}"#,
);
stays_portable(r#"{"name":"Checkout","message":"x","data":{"Declined":{"attempts":3}}}"#);
stays_portable(
r#"{"name":"other::Checkout","message":"x","data":{"Declined":{"attempts":3}}}"#,
);
stays_portable(r#"{"name":"dbos::StepTimeout","message":"x","data":null}"#);
stays_portable(
r#"{"name":"StepTimeout","message":"x","data":{"step":"charge","timeout":{"secs":1,"nanos":0}}}"#,
);
}
#[test]
fn an_application_error_that_cannot_be_written_is_refused() {
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
#[error("no keys fit")]
struct BadKeys {
by_pair: std::collections::HashMap<(u8, u8), u8>,
}
let error: Error<BadKeys> = BadKeys {
by_pair: [((1, 2), 3)].into_iter().collect(),
}
.into();
let refused =
encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).unwrap_err();
assert!(
matches!(&refused, Error::Serialization { what, .. } if what == "error"),
"{refused:?}"
);
}
#[test]
fn an_object_without_a_name_is_not_an_error_envelope() {
for stored in [r#"{"error":"boom"}"#, r#"{"name":null,"message":"boom"}"#] {
assert!(
decode_error::<EngineOnly>(stored, &PayloadEncoding::Portable).is_none(),
"{stored}"
);
}
}
#[test]
fn another_sdks_error_is_read_whatever_it_leaves_out() {
let java = decode_error::<EngineOnly>(
r#"{"name":"IllegalStateException","message":null,"code":null,"data":null}"#,
&PayloadEncoding::Portable,
)
.unwrap();
assert!(
matches!(&java, Error::Portable { name, message, .. } if name == "IllegalStateException" && message.is_empty()),
"{java:?}"
);
let ts = decode_error::<EngineOnly>(
r#"{"name":"Error","message":"boom"}"#,
&PayloadEncoding::Portable,
)
.unwrap();
assert!(
matches!(
&ts,
Error::Portable {
code: None,
data: None,
..
}
),
"{ts:?}"
);
let go = decode_error::<EngineOnly>(
r#"{"name":"WorkflowExecution","message":"x","code":"E1"}"#,
&PayloadEncoding::Portable,
)
.unwrap();
assert!(
matches!(&go, Error::Portable { code: Some(code), .. } if code == "E1"),
"{go:?}"
);
assert!(decode_error::<EngineOnly>("not json", &PayloadEncoding::Portable).is_none());
}
#[test]
fn a_native_error_still_round_trips_as_itself() {
let error: Error<CardDeclined> = Error::Application(CardDeclined);
let encoded = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Native).unwrap();
assert!(matches!(
decode_error::<CardDeclined>(&encoded, &PayloadEncoding::Native),
Some(Error::Application(CardDeclined))
));
}
#[test]
fn a_type_name_keeps_its_path_and_loses_its_arguments() {
assert_eq!(
type_name::<CardDeclined>(),
"dbos::serialization::tests::CardDeclined"
);
assert_eq!(type_name::<Vec<String>>(), "alloc::vec::Vec");
}
#[test]
fn values_follow_serde_json() {
let map = std::collections::BTreeMap::from([(1u32, "one")]);
assert_eq!(
encode::<_, EngineOnly>(&map, &PayloadEncoding::Native, "value").unwrap(),
r#"{"1":"one"}"#
);
let tuple_keys = std::collections::BTreeMap::from([((1u32, 2u32), "pair")]);
assert!(encode::<_, EngineOnly>(&tuple_keys, &PayloadEncoding::Native, "value").is_err());
}
#[test]
fn non_finite_floats_round_trip_natively_and_are_refused_portably() {
#[derive(Debug, Serialize, Deserialize)]
struct Reading {
name: String,
value: Option<f64>,
many: Vec<f32>,
}
#[derive(Debug, Serialize, Deserialize)]
enum Sample {
Point(f32),
Pair(f64, f64),
Range { low: f64, high: f64 },
}
fn round_trip<T: Serialize + DeserializeOwned>(value: &T) -> (String, T) {
let text = encode::<_, EngineOnly>(value, &PayloadEncoding::Native, "result")
.expect("a native write");
let back = decode::<T, EngineOnly>(Some(&text), &PayloadEncoding::Native, "result")
.unwrap_or_else(|e| panic!("{text} did not read back: {e}"));
(text, back)
}
fn same(a: f64, b: f64) -> bool {
a == b || (a.is_nan() && b.is_nan())
}
let (text, back) = round_trip(&f64::NAN);
assert_eq!(text, r#""NaN""#);
assert!(back.is_nan());
assert_eq!(
round_trip(&f64::INFINITY),
(r#""Infinity""#.to_owned(), f64::INFINITY)
);
assert_eq!(
round_trip(&f64::NEG_INFINITY),
(r#""-Infinity""#.to_owned(), f64::NEG_INFINITY)
);
assert!(round_trip(&f32::NAN).1.is_nan());
assert_eq!(round_trip(&f32::NEG_INFINITY).1, f32::NEG_INFINITY);
let reading = Reading {
name: "NaN".to_owned(),
value: Some(f64::INFINITY),
many: vec![1.5, f32::NAN, f32::NEG_INFINITY],
};
let (text, back) = round_trip(&reading);
assert_eq!(
text,
r#"{"name":"NaN","value":"Infinity","many":[1.5,"NaN","-Infinity"]}"#
);
assert_eq!(back.name, "NaN", "a string field keeps the string");
assert_eq!(back.value, Some(f64::INFINITY));
assert_eq!(back.many[0], 1.5);
assert!(back.many[1].is_nan());
assert_eq!(back.many[2], f32::NEG_INFINITY);
let map = BTreeMap::from([("k".to_owned(), f64::NAN)]);
assert!(round_trip(&map).1["k"].is_nan());
let (_, tuple) = round_trip(&(1u32, f64::INFINITY));
assert_eq!(tuple, (1, f64::INFINITY));
for sample in [
Sample::Point(f32::INFINITY),
Sample::Pair(f64::NAN, 2.0),
Sample::Range {
low: f64::NEG_INFINITY,
high: 0.0,
},
] {
let (_, back) = round_trip(&sample);
let ok = match (&sample, &back) {
(Sample::Point(a), Sample::Point(b)) => same(f64::from(*a), f64::from(*b)),
(Sample::Pair(a, b), Sample::Pair(c, d)) => same(*a, *c) && same(*b, *d),
(Sample::Range { low: a, high: b }, Sample::Range { low: c, high: d }) => {
same(*a, *c) && same(*b, *d)
}
_ => false,
};
assert!(ok, "{sample:?} read back as {back:?}");
}
assert!(
decode::<u32, EngineOnly>(Some(r#""NaN""#), &PayloadEncoding::Native, "result")
.is_err()
);
assert!(
decode::<f64, EngineOnly>(Some(r#""nan""#), &PayloadEncoding::Native, "result")
.is_err(),
"only the exact names are floats"
);
fn refused<T: Serialize>(value: &T) {
let error = encode::<_, EngineOnly>(value, &PayloadEncoding::Portable, "result")
.expect_err("a non-finite float was written portably");
let Error::Serialization { what, message, .. } = &error else {
panic!("not a serialization error: {error}");
};
assert_eq!(what, "result");
assert!(message.contains("not a finite number"), "{message}");
}
refused(&f64::NAN);
refused(&f64::INFINITY);
refused(&f32::NEG_INFINITY);
refused(&reading);
refused(&map);
refused(&Sample::Pair(1.0, f64::NAN));
for encoding in [PayloadEncoding::Native, PayloadEncoding::Portable] {
assert_eq!(
encode::<_, EngineOnly>(
&Sample::Range {
low: -1.0,
high: 2.0
},
&encoding,
"r"
)
.unwrap(),
r#"{"Range":{"low":-1.0,"high":2.0}}"#
);
assert_eq!(
encode::<_, EngineOnly>(&f64::MAX, &encoding, "result").unwrap(),
serde_json::to_string(&f64::MAX).unwrap()
);
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
struct Key(f64);
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
self.0.total_cmp(&other.0).is_eq()
}
}
impl Eq for Key {}
impl PartialOrd for Key {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Key {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}
#[derive(Debug, Clone, Copy, Deserialize)]
struct Key32(f32);
impl PartialEq for Key32 {
fn eq(&self, other: &Self) -> bool {
self.0.total_cmp(&other.0).is_eq()
}
}
impl Eq for Key32 {}
impl PartialOrd for Key32 {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Key32 {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.total_cmp(&other.0)
}
}
let keyed = BTreeMap::from([(Key(1.5), 7u32), (Key(-2.0), 8)]);
let (text, back) = round_trip(&keyed);
assert_eq!(text, r#"{"-2.0":8,"1.5":7}"#);
assert_eq!(back, keyed);
assert_eq!(
decode::<f32, EngineOnly>(Some(r#""2.5""#), &PayloadEncoding::Native, "result")
.unwrap(),
2.5
);
for text in ["7.038531e-26", "1.0000000596046448"] {
let read =
decode::<f32, EngineOnly>(Some(text), &PayloadEncoding::Native, "result").unwrap();
assert_eq!(
read.to_bits(),
serde_json::from_str::<f32>(text).unwrap().to_bits(),
"{text}"
);
let key = format!(r#"{{"{text}":1}}"#);
let keyed = decode::<BTreeMap<Key32, u32>, EngineOnly>(
Some(&key),
&PayloadEncoding::Native,
"result",
)
.unwrap();
assert_eq!(
keyed.keys().next().unwrap().0.to_bits(),
read.to_bits(),
"{key}"
);
}
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
#[error("insufficient funds")]
struct Insufficient {
balance: f64,
}
let error: Error<Insufficient> = Error::Application(Insufficient { balance: f64::NAN });
assert!(
encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Portable).is_err(),
"a portable error wrote a non-finite float"
);
let native = encode_error::<_, EngineOnly>(&error, &PayloadEncoding::Native)
.expect("a native error");
let Some(Error::Application(Insufficient { balance })) =
decode_error::<Insufficient>(&native, &PayloadEncoding::Native)
else {
panic!("{native} did not read back as the application error");
};
assert!(balance.is_nan());
assert!(
encode_input::<_, EngineOnly>(&(1u32, f64::NAN), &PayloadEncoding::Portable).is_err()
);
let input = encode_input::<_, EngineOnly>(&(1u32, f64::NAN), &PayloadEncoding::Native)
.expect("a native input");
let (n, x) = decode_input::<(u32, f64), EngineOnly>(Some(&input), &PayloadEncoding::Native)
.expect("the input read back");
assert!(n == 1 && x.is_nan(), "{input}");
}
#[derive(Debug)]
struct Backwards;
impl CustomSerializer for Backwards {
fn name(&self) -> &str {
"backwards_json"
}
fn serialize(
&self,
value: Payload<'_>,
) -> std::result::Result<String, Box<dyn std::error::Error + Send + Sync>> {
Ok(serde_json::to_string(&value)?.chars().rev().collect())
}
fn deserialize(
&self,
text: &str,
into: PayloadSlot<'_>,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
let json: String = text.chars().rev().collect();
let mut deserializer = serde_json::Deserializer::from_str(&json);
into.fill(&mut deserializer)?;
Ok(deserializer.end()?)
}
}
fn backwards() -> (Serializer, PayloadEncoding) {
let serializer = Serializer::custom(Backwards);
let format = serializer.encoding();
(serializer, format)
}
#[test]
fn a_custom_serializer_sees_the_real_type() {
let (_, format) = backwards();
let value = (u128::MAX, "two".to_owned());
let encoded = encode::<_, EngineOnly>(&value, &format, "value").unwrap();
assert_eq!(
encoded,
r#"]"owt",554112867134706473364364839029663282043["#
);
assert_eq!(
decode::<(u128, String), EngineOnly>(Some(&encoded), &format, "value").unwrap(),
value
);
decode::<(), EngineOnly>(None, &format, "value").unwrap();
}
#[test]
fn a_custom_tag_is_readable_only_where_that_serializer_is_configured() {
let (serializer, format) = backwards();
assert_eq!(
PayloadEncoding::of::<EngineOnly>(Some("backwards_json"), &serializer).unwrap(),
format
);
assert_eq!(
PayloadEncoding::of::<EngineOnly>(Some("rust_serde"), &serializer).unwrap(),
PayloadEncoding::Native
);
let err = PayloadEncoding::of::<EngineOnly>(Some("backwards_json"), &Serializer::RustSerde)
.unwrap_err();
assert!(
matches!(&err, Error::UnknownSerialization { name } if name == "backwards_json"),
"{err}"
);
}
#[test]
fn a_custom_serializer_carries_an_input_and_an_error_whole() {
let (_, format) = backwards();
let order = Order {
sku: "tea".to_owned(),
quantity: 2,
};
let encoded = encode_input::<_, EngineOnly>(&order, &format).unwrap();
assert_eq!(encoded, r#"}2:"ytitnauq","aet":"uks"{"#);
assert_eq!(
decode_input::<Order, EngineOnly>(Some(&encoded), &format).unwrap(),
order
);
let error: Error<CardDeclined> = Error::Application(CardDeclined);
let encoded = encode_error::<_, EngineOnly>(&error, &format).unwrap();
assert!(matches!(
decode_error::<CardDeclined>(&encoded, &format),
Some(Error::Application(CardDeclined))
));
}
#[test]
fn an_error_a_custom_serializer_cannot_read_is_a_decode_failure() {
let (_, format) = backwards();
let decoded = decode_error::<CardDeclined>("sealed", &format);
assert!(
matches!(&decoded, Some(Error::Deserialization { what, .. }) if what == "error"),
"{decoded:?}"
);
}
#[test]
fn a_custom_serializer_refuses_trailing_text() {
let (_, format) = backwards();
let err = decode::<u32, EngineOnly>(Some("x 7"), &format, "result").unwrap_err();
assert!(matches!(&err, Error::Deserialization { .. }), "{err}");
}
#[test]
fn a_serializer_that_fills_nothing_is_an_error_not_a_value() {
#[derive(Debug)]
struct Forgetful;
impl CustomSerializer for Forgetful {
fn name(&self) -> &str {
"forgetful"
}
fn serialize(
&self,
_: Payload<'_>,
) -> std::result::Result<String, Box<dyn std::error::Error + Send + Sync>> {
Ok(String::new())
}
fn deserialize(
&self,
_: &str,
_: PayloadSlot<'_>,
) -> std::result::Result<(), Box<dyn std::error::Error + Send + Sync>> {
Ok(())
}
}
let format = Serializer::custom(Forgetful).encoding();
let err = decode::<u32, EngineOnly>(Some("x"), &format, "result").unwrap_err();
assert!(
matches!(&err, Error::Deserialization { message, .. } if message.contains("forgetful")),
"{err}"
);
}
#[test]
fn a_mismatch_names_the_half_that_failed() {
let err = decode::<u32, EngineOnly>(
Some(r#""not a number""#),
&PayloadEncoding::Native,
"result",
)
.unwrap_err();
assert!(
matches!(
err,
Error::Deserialization {
what: Cow::Borrowed("result"),
..
}
),
"{err}"
);
}
}