use std::borrow::Cow;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use derive_more::Display;
use serde::{Deserialize, Serialize};
use tempest_core::encoding::{
BufGetLexicalExt, BufGetRawExt, BufPutLexicalExt, BufPutRawExt, LexicalDecodeError,
RawDecodeError,
};
#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum TempestType {
#[display("Int64")]
Int64,
#[display("Bool")]
Bool,
#[display("String")]
String,
#[display("Enum({_0})")]
Enum(u32),
}
impl TempestType {
pub fn name(&self) -> &'static str {
match self {
Self::Int64 => "Int64",
Self::Bool => "Bool",
Self::String => "String",
Self::Enum(_) => "Enum",
}
}
}
impl std::str::FromStr for TempestType {
type Err = ();
fn from_str(s: &str) -> Result<Self, ()> {
match s {
"Int64" => Ok(Self::Int64),
"Bool" => Ok(Self::Bool),
"String" => Ok(Self::String),
_ => Err(()),
}
}
}
#[derive(derive_more::Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub enum TempestValue<'a> {
Int64(i64) = 0,
Bool(bool) = 1,
String(Cow<'a, str>) = 2,
#[display("Enum({type_id}:{variant_id})")]
Enum { type_id: u32, variant_id: u32, fields: Vec<TempestValue<'a>> } = 3,
}
impl<'a> TempestValue<'a> {
pub fn ty(&self) -> TempestType {
match self {
TempestValue::Int64(_) => TempestType::Int64,
TempestValue::Bool(_) => TempestType::Bool,
TempestValue::String(_) => TempestType::String,
TempestValue::Enum { type_id, .. } => TempestType::Enum(*type_id),
}
}
pub fn encode(&self, buf: &mut BytesMut) {
match self {
&TempestValue::Int64(i) => buf.put_i64_raw(i),
&TempestValue::Bool(b) => buf.put_bool_raw(b),
TempestValue::String(s) => buf.put_str_raw(s),
TempestValue::Enum { variant_id, fields, .. } => {
buf.put_u32(*variant_id);
for field in fields {
field.encode(buf);
}
}
}
}
pub fn decode(
buf: &mut Bytes,
ty: TempestType,
) -> Result<TempestValue<'static>, RawDecodeError> {
match ty {
TempestType::Int64 => buf.get_i64_raw().map(TempestValue::Int64),
TempestType::Bool => buf.get_bool_raw().map(TempestValue::Bool),
TempestType::String => buf
.get_str_raw()
.map(|s| TempestValue::String(Cow::Owned(s))),
TempestType::Enum(type_id) => {
let variant_id = buf.get_u32();
Ok(TempestValue::Enum { type_id, variant_id, fields: vec![] })
}
}
}
pub fn encode_lexical(&self, buf: &mut BytesMut) {
match self {
&TempestValue::Int64(i) => buf.put_i64_lexical(i),
&TempestValue::Bool(b) => buf.put_bool_lexical(b),
TempestValue::String(s) => buf.put_str_lexical(s),
TempestValue::Enum { .. } => panic!("enum values cannot be used as primary key"),
}
}
pub fn decode_lexical(
buf: &mut Bytes,
ty: TempestType,
) -> Result<TempestValue<'static>, LexicalDecodeError> {
match ty {
TempestType::Int64 => buf.get_i64_lexical().map(TempestValue::Int64),
TempestType::Bool => buf.get_bool_lexical().map(TempestValue::Bool),
TempestType::String => buf
.get_str_lexical()
.map(|s| TempestValue::String(Cow::Owned(s))),
TempestType::Enum(_) => panic!("enum values cannot be used as primary key"),
}
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use bytes::{Bytes, BytesMut};
use super::*;
fn roundtrip(val: TempestValue<'_>) -> TempestValue<'static> {
let ty = val.ty();
let mut buf = BytesMut::new();
val.encode(&mut buf);
TempestValue::decode(&mut buf.freeze(), ty).unwrap()
}
fn roundtrip_lexical(val: TempestValue<'_>) -> TempestValue<'static> {
let ty = val.ty();
let mut buf = BytesMut::new();
val.encode_lexical(&mut buf);
TempestValue::decode_lexical(&mut buf.freeze(), ty).unwrap()
}
fn encode_lexical_bytes(val: TempestValue<'_>) -> Bytes {
let mut buf = BytesMut::new();
val.encode_lexical(&mut buf);
buf.freeze()
}
#[test]
fn test_int64_roundtrip() {
for val in [0i64, 1, -1, i64::MIN, i64::MAX, 42, -1000] {
let result = roundtrip(TempestValue::Int64(val));
assert_eq!(result, TempestValue::Int64(val));
}
}
#[test]
fn test_bool_roundtrip() {
for val in [true, false] {
let result = roundtrip(TempestValue::Bool(val));
assert_eq!(result, TempestValue::Bool(val));
}
}
#[test]
fn test_string_roundtrip() {
for val in ["", "hello", "tempest", "unicode: ??", "hel\x00lo"] {
let result = roundtrip(TempestValue::String(Cow::Borrowed(val)));
assert_eq!(result, TempestValue::String(Cow::Borrowed(val)));
}
}
#[test]
fn test_enum_roundtrip() {
let val = TempestValue::Enum { type_id: 5, variant_id: 2, fields: vec![] };
let result = roundtrip(val.clone());
assert_eq!(result, val);
}
#[test]
fn test_decode_returns_owned_string() {
let result = roundtrip(TempestValue::String(Cow::Borrowed("hello")));
if let TempestValue::String(cow) = result {
assert!(
matches!(cow, Cow::Owned(_)),
"decoded string should be Cow::Owned"
);
} else {
panic!("expected String variant");
}
}
#[test]
fn test_int64_lexical_roundtrip() {
for val in [0i64, 1, -1, i64::MIN, i64::MAX, 42, -1000] {
let result = roundtrip_lexical(TempestValue::Int64(val));
assert_eq!(result, TempestValue::Int64(val));
}
}
#[test]
fn test_bool_lexical_roundtrip() {
for val in [true, false] {
let result = roundtrip_lexical(TempestValue::Bool(val));
assert_eq!(result, TempestValue::Bool(val));
}
}
#[test]
fn test_string_lexical_roundtrip() {
for val in ["", "hello", "tempest", "hel\x00lo", "\x00\x00\x00"] {
let result = roundtrip_lexical(TempestValue::String(Cow::Borrowed(val)));
assert_eq!(result, TempestValue::String(Cow::Borrowed(val)));
}
}
#[test]
fn test_int64_lexical_ordering() {
let cases = [i64::MIN, -1000, -1, 0, 1, 1000, i64::MAX];
for pair in cases.windows(2) {
let (a, b) = (pair[0], pair[1]);
assert!(
encode_lexical_bytes(TempestValue::Int64(a))
< encode_lexical_bytes(TempestValue::Int64(b)),
"{} should encode less than {}",
a,
b
);
}
}
#[test]
fn test_bool_lexical_ordering() {
assert!(
encode_lexical_bytes(TempestValue::Bool(false))
< encode_lexical_bytes(TempestValue::Bool(true))
);
}
#[test]
fn test_string_lexical_ordering() {
let cases = ["", "a", "aa", "ab", "b", "z"];
for pair in cases.windows(2) {
let (a, b) = (pair[0], pair[1]);
assert!(
encode_lexical_bytes(TempestValue::String(Cow::Borrowed(a)))
< encode_lexical_bytes(TempestValue::String(Cow::Borrowed(b))),
"{:?} should encode less than {:?}",
a,
b
);
}
}
#[test]
fn test_ty_returns_correct_discriminant() {
assert_eq!(TempestValue::Int64(Default::default()).ty(), TempestType::Int64);
assert_eq!(TempestValue::Bool(Default::default()).ty(), TempestType::Bool);
assert_eq!(TempestValue::String(Default::default()).ty(), TempestType::String);
assert_eq!(
TempestValue::Enum { type_id: 3, variant_id: 0, fields: vec![] }.ty(),
TempestType::Enum(3)
);
}
#[test]
fn test_raw_and_lexical_differ_for_negative_i64() {
let val = TempestValue::Int64(-1);
let mut raw_buf = BytesMut::new();
let mut lex_buf = BytesMut::new();
val.encode(&mut raw_buf);
val.encode_lexical(&mut lex_buf);
assert_ne!(
raw_buf, lex_buf,
"raw and lexical encodings of -1 must differ"
);
}
#[test]
fn test_decode_advances_cursor() {
let mut buf = BytesMut::new();
TempestValue::Int64(42).encode(&mut buf);
TempestValue::Bool(true).encode(&mut buf);
TempestValue::String(Cow::Borrowed("hi")).encode(&mut buf);
let mut bytes = buf.freeze();
assert_eq!(
TempestValue::decode(&mut bytes, TempestType::Int64).unwrap(),
TempestValue::Int64(42)
);
assert_eq!(
TempestValue::decode(&mut bytes, TempestType::Bool).unwrap(),
TempestValue::Bool(true)
);
assert_eq!(
TempestValue::decode(&mut bytes, TempestType::String).unwrap(),
TempestValue::String(Cow::Borrowed("hi"))
);
assert!(bytes.is_empty());
}
#[test]
fn test_decode_lexical_advances_cursor() {
let mut buf = BytesMut::new();
TempestValue::Int64(-99).encode_lexical(&mut buf);
TempestValue::String(Cow::Borrowed("foo")).encode_lexical(&mut buf);
TempestValue::Bool(false).encode_lexical(&mut buf);
let mut bytes = buf.freeze();
assert_eq!(
TempestValue::decode_lexical(&mut bytes, TempestType::Int64).unwrap(),
TempestValue::Int64(-99)
);
assert_eq!(
TempestValue::decode_lexical(&mut bytes, TempestType::String).unwrap(),
TempestValue::String(Cow::Borrowed("foo"))
);
assert_eq!(
TempestValue::decode_lexical(&mut bytes, TempestType::Bool).unwrap(),
TempestValue::Bool(false)
);
assert!(bytes.is_empty());
}
}