use std::{error::Error, fmt};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::Value;
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProtocolIdentity {
pub name: String,
pub major: u32,
pub revision: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub features: Vec<String>,
}
impl ProtocolIdentity {
#[must_use]
pub fn new(name: impl Into<String>, major: u32, revision: impl Into<String>) -> Self {
Self {
name: name.into(),
major,
revision: revision.into(),
features: Vec::new(),
}
}
#[must_use]
pub fn with_features(mut self, features: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.features = features.into_iter().map(Into::into).collect();
self
}
pub fn validate(&self, expected_name: &str, expected_major: u32) -> Result<(), ProtocolError> {
if self.name != expected_name {
return Err(ProtocolError::UnexpectedProtocol {
expected: expected_name.to_string(),
actual: self.name.clone(),
});
}
if self.major != expected_major {
return Err(ProtocolError::UnsupportedMajor {
protocol: self.name.clone(),
expected: expected_major,
actual: self.major,
});
}
Ok(())
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct VersionedEnvelope<T> {
pub schema: String,
pub version: u32,
pub payload: T,
}
impl<T> VersionedEnvelope<T> {
#[must_use]
pub fn new(schema: impl Into<String>, version: u32, payload: T) -> Self {
Self {
schema: schema.into(),
version,
payload,
}
}
}
pub trait VersionedRecord: Sized {
const SCHEMA: &'static str;
const VERSION: u32 = 1;
const ALLOW_BARE_V0: bool = false;
fn decode_version(version: u32, payload: Value) -> Result<Self, VersionedRecordError>
where
Self: DeserializeOwned,
{
if version == 0 || version != Self::VERSION {
return Err(VersionedRecordError::UnsupportedVersion {
schema: Self::SCHEMA,
supported: Self::VERSION,
actual: version,
});
}
serde_json::from_value(payload).map_err(VersionedRecordError::Json)
}
fn decode_bare_v0(payload: Value) -> Result<Self, VersionedRecordError>
where
Self: DeserializeOwned,
{
if !Self::ALLOW_BARE_V0 {
return Err(VersionedRecordError::BareV0Unsupported {
schema: Self::SCHEMA,
});
}
serde_json::from_value(payload).map_err(VersionedRecordError::Json)
}
}
pub fn to_versioned_json<T>(value: &T) -> Result<String, serde_json::Error>
where
T: Serialize + VersionedRecord,
{
serde_json::to_string(&VersionedEnvelope::new(T::SCHEMA, T::VERSION, value))
}
pub fn to_versioned_value<T>(value: &T) -> Result<Value, serde_json::Error>
where
T: Serialize + VersionedRecord,
{
serde_json::to_value(VersionedEnvelope::new(T::SCHEMA, T::VERSION, value))
}
pub fn from_versioned_json<T>(input: &str) -> Result<T, VersionedRecordError>
where
T: DeserializeOwned + VersionedRecord,
{
let value = serde_json::from_str(input).map_err(VersionedRecordError::Json)?;
from_versioned_value(value)
}
pub fn from_versioned_value<T>(value: Value) -> Result<T, VersionedRecordError>
where
T: DeserializeOwned + VersionedRecord,
{
let looks_enveloped = value
.as_object()
.is_some_and(|object| object.contains_key("schema") || object.contains_key("version"));
if !looks_enveloped {
return T::decode_bare_v0(value);
}
let envelope = serde_json::from_value::<VersionedEnvelope<Value>>(value)
.map_err(VersionedRecordError::Json)?;
if envelope.schema != T::SCHEMA {
return Err(VersionedRecordError::WrongSchema {
expected: T::SCHEMA,
actual: envelope.schema,
});
}
T::decode_version(envelope.version, envelope.payload)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProtocolError {
UnexpectedProtocol {
expected: String,
actual: String,
},
UnsupportedMajor {
protocol: String,
expected: u32,
actual: u32,
},
}
impl fmt::Display for ProtocolError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedProtocol { expected, actual } => {
write!(formatter, "expected protocol {expected}, received {actual}")
}
Self::UnsupportedMajor {
protocol,
expected,
actual,
} => write!(
formatter,
"unsupported {protocol} major {actual}; supported major is {expected}"
),
}
}
}
impl Error for ProtocolError {}
#[derive(Debug)]
pub enum VersionedRecordError {
Json(serde_json::Error),
BareV0Unsupported {
schema: &'static str,
},
WrongSchema {
expected: &'static str,
actual: String,
},
UnsupportedVersion {
schema: &'static str,
supported: u32,
actual: u32,
},
}
impl fmt::Display for VersionedRecordError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Json(error) => write!(formatter, "invalid durable JSON: {error}"),
Self::BareV0Unsupported { schema } => {
write!(
formatter,
"bare v0 JSON is not supported for durable schema {schema}"
)
}
Self::WrongSchema { expected, actual } => {
write!(
formatter,
"expected durable schema {expected}, received {actual}"
)
}
Self::UnsupportedVersion {
schema,
supported,
actual,
} => write!(
formatter,
"unsupported {schema} version {actual}; supported version is {supported}"
),
}
}
}
impl Error for VersionedRecordError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Json(error) => Some(error),
Self::BareV0Unsupported { .. }
| Self::WrongSchema { .. }
| Self::UnsupportedVersion { .. } => None,
}
}
}