use std::sync::OnceLock;
use dyn_encoding::{CborCodec, CodecRegistry, JsonCodec, ProtobufCodec, WireTypeId, WireValue};
use prost::Message;
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, PartialEq, Message, Serialize, Deserialize)]
pub struct HttpIndex {
#[prost(string, tag = "1")]
pub name: String,
#[prost(string, tag = "2")]
pub value: String,
}
#[derive(Clone, Eq, PartialEq, Message, Serialize, Deserialize)]
pub struct HttpLink {
#[prost(string, tag = "1")]
pub bucket: String,
#[prost(string, tag = "2")]
pub key: String,
#[prost(string, tag = "3")]
pub tag: String,
}
#[derive(Clone, Eq, PartialEq, Message, Serialize, Deserialize)]
pub struct HttpObject {
#[prost(bytes = "vec", tag = "1")]
#[serde(default)]
pub value: Vec<u8>,
#[prost(string, optional, tag = "2")]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content_type: Option<String>,
#[prost(message, repeated, tag = "3")]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub indexes: Vec<HttpIndex>,
#[prost(message, repeated, tag = "4")]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub links: Vec<HttpLink>,
}
impl WireValue for HttpObject {
fn wire_type_id() -> WireTypeId {
WireTypeId::new("riak.http.Object")
}
}
impl HttpObject {
#[must_use]
pub fn to_storage_bytes(&self) -> Vec<u8> {
self.encode_to_vec()
}
pub fn from_storage_bytes(bytes: &[u8]) -> Result<Self, prost::DecodeError> {
Self::decode(bytes)
}
#[must_use]
pub fn index_pairs(&self) -> Vec<(Vec<u8>, Vec<u8>)> {
self.indexes
.iter()
.map(|i| (i.name.clone().into_bytes(), i.value.clone().into_bytes()))
.collect()
}
}
#[must_use]
pub fn object_codecs() -> &'static CodecRegistry {
static REGISTRY: OnceLock<CodecRegistry> = OnceLock::new();
REGISTRY.get_or_init(|| {
let mut json = JsonCodec::new();
json.register::<HttpObject>();
let mut cbor = CborCodec::new();
cbor.register::<HttpObject>();
let mut protobuf = ProtobufCodec::new();
protobuf.register::<HttpObject>();
let mut registry = CodecRegistry::new();
registry.register(json);
registry.register(cbor);
registry.register(protobuf);
registry
})
}
#[cfg(test)]
mod tests {
use super::*;
use dyn_encoding::WireValue;
fn fixture() -> HttpObject {
HttpObject {
value: b"hello world".to_vec(),
content_type: Some("text/plain".to_string()),
indexes: vec![
HttpIndex {
name: "age_int".to_string(),
value: "42".to_string(),
},
HttpIndex {
name: "city_bin".to_string(),
value: "seattle".to_string(),
},
],
links: vec![HttpLink {
bucket: "people".to_string(),
key: "bob".to_string(),
tag: "friend".to_string(),
}],
}
}
#[test]
fn storage_form_round_trips() {
let obj = fixture();
let bytes = obj.to_storage_bytes();
let back = HttpObject::from_storage_bytes(&bytes).expect("decode");
assert_eq!(back, obj);
}
#[test]
fn links_round_trip_through_storage_form() {
let obj = fixture();
let bytes = obj.to_storage_bytes();
let back = HttpObject::from_storage_bytes(&bytes).expect("decode");
assert_eq!(back.links, obj.links);
assert_eq!(back.links.len(), 1);
assert_eq!(back.links[0].tag, "friend");
}
#[test]
fn objects_stored_before_links_decode_with_empty_links() {
#[derive(Clone, PartialEq, ::prost::Message)]
struct LegacyObject {
#[prost(bytes = "vec", tag = "1")]
value: Vec<u8>,
#[prost(string, optional, tag = "2")]
content_type: Option<String>,
#[prost(message, repeated, tag = "3")]
indexes: Vec<HttpIndex>,
}
let legacy = LegacyObject {
value: b"old".to_vec(),
content_type: Some("text/plain".to_string()),
indexes: vec![HttpIndex {
name: "age_int".to_string(),
value: "7".to_string(),
}],
};
let bytes = legacy.encode_to_vec();
let obj = HttpObject::from_storage_bytes(&bytes).expect("decode legacy");
assert_eq!(obj.value, b"old");
assert_eq!(obj.indexes.len(), 1);
assert!(obj.links.is_empty());
}
#[test]
fn corrupt_storage_form_is_an_error() {
let err = HttpObject::from_storage_bytes(&[0x0a, 0xff]);
assert!(err.is_err());
}
#[test]
fn cross_encoding_preserves_logical_object() {
let obj = fixture();
let registry = object_codecs();
let json = registry.for_content_type("application/json").expect("json");
let cbor = registry.for_content_type("application/cbor").expect("cbor");
let pb = registry
.for_content_type("application/x-protobuf")
.expect("protobuf");
let json_bytes = json.encode(&obj).expect("json encode");
let from_json = json
.decode(HttpObject::wire_type_id(), &json_bytes)
.expect("json decode");
let from_json = from_json
.as_any()
.downcast_ref::<HttpObject>()
.expect("downcast json");
assert_eq!(from_json, &obj);
let cbor_bytes = cbor.encode(from_json).expect("cbor encode");
let from_cbor = cbor
.decode(HttpObject::wire_type_id(), &cbor_bytes)
.expect("cbor decode");
let from_cbor = from_cbor
.as_any()
.downcast_ref::<HttpObject>()
.expect("downcast cbor");
assert_eq!(from_cbor, &obj);
let pb_bytes = pb.encode(from_cbor).expect("pb encode");
let from_pb = pb
.decode(HttpObject::wire_type_id(), &pb_bytes)
.expect("pb decode");
let from_pb = from_pb
.as_any()
.downcast_ref::<HttpObject>()
.expect("downcast pb");
assert_eq!(from_pb, &obj);
}
#[test]
fn index_pairs_render_name_value_bytes() {
let obj = fixture();
let pairs = obj.index_pairs();
assert_eq!(
pairs,
vec![
(b"age_int".to_vec(), b"42".to_vec()),
(b"city_bin".to_vec(), b"seattle".to_vec()),
]
);
}
#[test]
fn object_codecs_is_stable_across_calls() {
let a = object_codecs();
let b = object_codecs();
assert!(std::ptr::eq(a, b));
}
}