use anyhow::anyhow;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use surrealdb_protocol::fb::v1 as proto_fb;
use super::{FromFlatbuffers, ToFlatbuffers};
use crate::{
Array, Bytes, Datetime, Duration, File, Geometry, Number, Object, Range, RecordId, Regex, Set,
Table, Uuid, Value,
};
impl ToFlatbuffers for Value {
type Output<'bldr> = flatbuffers::WIPOffset<proto_fb::Value<'bldr>>;
#[inline]
fn to_fb<'bldr>(
&self,
builder: &mut flatbuffers::FlatBufferBuilder<'bldr>,
) -> anyhow::Result<Self::Output<'bldr>> {
let args = match self {
Self::None => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::NONE,
value: None,
},
Self::Null => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Null,
value: Some(
proto_fb::NullValue::create(builder, &proto_fb::NullValueArgs {})
.as_union_value(),
),
},
Self::Bool(b) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Bool,
value: Some(b.to_fb(builder)?.as_union_value()),
},
Self::Number(n) => match n {
crate::Number::Int(i) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Int64,
value: Some(i.to_fb(builder)?.as_union_value()),
},
crate::Number::Float(f) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Float64,
value: Some(f.to_fb(builder)?.as_union_value()),
},
crate::Number::Decimal(d) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Decimal,
value: Some(d.to_fb(builder)?.as_union_value()),
},
},
Self::String(s) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::String,
value: Some(s.to_fb(builder)?.as_union_value()),
},
Self::Bytes(b) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Bytes,
value: Some(b.to_fb(builder)?.as_union_value()),
},
Self::Table(t) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Table,
value: Some(t.to_fb(builder)?.as_union_value()),
},
Self::RecordId(record_id) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::RecordId,
value: Some(record_id.to_fb(builder)?.as_union_value()),
},
Self::Duration(d) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Duration,
value: Some(d.to_fb(builder)?.as_union_value()),
},
Self::Datetime(dt) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Datetime,
value: Some(dt.0.to_fb(builder)?.as_union_value()),
},
Self::Uuid(uuid) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Uuid,
value: Some(uuid.to_fb(builder)?.as_union_value()),
},
Self::Object(obj) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Object,
value: Some(obj.to_fb(builder)?.as_union_value()),
},
Self::Array(arr) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Array,
value: Some(arr.to_fb(builder)?.as_union_value()),
},
Self::Set(set) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Set,
value: Some(set.to_fb(builder)?.as_union_value()),
},
Self::Geometry(geometry) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Geometry,
value: Some(geometry.to_fb(builder)?.as_union_value()),
},
Self::File(file) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::File,
value: Some(file.to_fb(builder)?.as_union_value()),
},
Self::Regex(regex) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Regex,
value: Some(regex.to_fb(builder)?.as_union_value()),
},
Self::Range(range) => proto_fb::ValueArgs {
value_type: proto_fb::ValueType::Range,
value: Some(range.to_fb(builder)?.as_union_value()),
},
};
Ok(proto_fb::Value::create(builder, &args))
}
}
impl FromFlatbuffers for Value {
type Input<'a> = proto_fb::Value<'a>;
#[inline]
fn from_fb(input: Self::Input<'_>) -> anyhow::Result<Self> {
match input.value_type() {
proto_fb::ValueType::NONE => Ok(Value::None),
proto_fb::ValueType::Null => Ok(Value::Null),
proto_fb::ValueType::Bool => {
Ok(Value::Bool(input.value_as_bool().expect("Guaranteed to be a Bool").value()))
}
proto_fb::ValueType::Int64 => Ok(Value::Number(Number::Int(
input.value_as_int_64().expect("Guaranteed to be an Int64").value(),
))),
proto_fb::ValueType::Float64 => Ok(Value::Number(Number::Float(
input.value_as_float_64().expect("Guaranteed to be a Float64").value(),
))),
proto_fb::ValueType::Decimal => {
let decimal_value = input.value_as_decimal().expect("Guaranteed to be a Decimal");
Ok(Value::Number(Number::Decimal(Decimal::from_fb(decimal_value)?)))
}
proto_fb::ValueType::String => {
let string_value = input.value_as_string().expect("Guaranteed to be a String");
let value = string_value
.value()
.expect("String value is guaranteed to be present")
.to_string();
Ok(Value::String(value))
}
proto_fb::ValueType::Bytes => {
let bytes_value = input.value_as_bytes().expect("Guaranteed to be Bytes");
Ok(Value::Bytes(Bytes::from_fb(bytes_value)?))
}
proto_fb::ValueType::Table => {
let table_value = input.value_as_table().expect("Guaranteed to be a Table");
let table = Table::from_fb(table_value)?;
Ok(Value::Table(table))
}
proto_fb::ValueType::RecordId => {
let record_id_value =
input.value_as_record_id().expect("Guaranteed to be a RecordId");
let thing = RecordId::from_fb(record_id_value)?;
Ok(Value::RecordId(thing))
}
proto_fb::ValueType::Duration => {
let duration_value =
input.value_as_duration().expect("Guaranteed to be a Duration");
let duration = Duration::from_fb(duration_value)?;
Ok(Value::Duration(duration))
}
proto_fb::ValueType::Datetime => {
let datetime_value =
input.value_as_datetime().expect("Guaranteed to be a Datetime");
let dt = DateTime::<Utc>::from_fb(datetime_value)?;
Ok(Value::Datetime(Datetime(dt)))
}
proto_fb::ValueType::Uuid => {
let uuid_value = input.value_as_uuid().expect("Guaranteed to be a Uuid");
let uuid = Uuid::from_fb(uuid_value)?;
Ok(Value::Uuid(uuid))
}
proto_fb::ValueType::Object => {
let object_value = input.value_as_object().expect("Guaranteed to be an Object");
let object = Object::from_fb(object_value)?;
Ok(Value::Object(object))
}
proto_fb::ValueType::Array => {
let array_value = input.value_as_array().expect("Guaranteed to be an Array");
let array = Array::from_fb(array_value)?;
Ok(Value::Array(array))
}
proto_fb::ValueType::Set => {
let set_value = input.value_as_set().expect("Guaranteed to be a Set");
let set = Set::from_fb(set_value)?;
Ok(Value::Set(set))
}
proto_fb::ValueType::Geometry => {
let geometry_value =
input.value_as_geometry().expect("Guaranteed to be a Geometry");
let geometry = Geometry::from_fb(geometry_value)?;
Ok(Value::Geometry(geometry))
}
proto_fb::ValueType::File => {
let file_value = input.value_as_file().expect("Guaranteed to be a File");
let file = File::from_fb(file_value)?;
Ok(Value::File(file))
}
proto_fb::ValueType::Regex => {
let regex_value = input.value_as_regex().expect("Guaranteed to be a Regex");
let regex = Regex::from_fb(regex_value)?;
Ok(Value::Regex(regex))
}
proto_fb::ValueType::Range => {
let range_value = input.value_as_range().expect("Guaranteed to be a Range");
let range = Range::from_fb(range_value)?;
Ok(Value::Range(Box::new(range)))
}
_ => Err(anyhow!(
"Unsupported value type for Flatbuffers deserialization: {:?}",
input.value_type()
)),
}
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::ops::Bound;
use std::str::FromStr;
use chrono::{DateTime, Utc};
use rstest::rstest;
use rust_decimal::Decimal;
use surrealdb_protocol::fb::v1 as proto_fb;
use super::*;
use crate::*;
#[rstest]
#[case::none(Value::None)]
#[case::null(Value::Null)]
#[case::bool(Value::Bool(true))]
#[case::bool(Value::Bool(false))]
#[case::int(Value::Number(Number::Int(42)))]
#[case::int(Value::Number(Number::Int(i64::MIN)))]
#[case::int(Value::Number(Number::Int(i64::MAX)))]
#[case::float(Value::Number(Number::Float(1.23)))]
#[case::float(Value::Number(Number::Float(f64::MIN)))]
#[case::float(Value::Number(Number::Float(f64::MAX)))]
#[case::float(Value::Number(Number::Float(f64::NAN)))]
#[case::float(Value::Number(Number::Float(f64::INFINITY)))]
#[case::float(Value::Number(Number::Float(f64::NEG_INFINITY)))]
#[case::decimal(Value::Number(Number::Decimal(Decimal::new(123, 2))))]
#[case::duration(Value::Duration(Duration::default()))]
#[case::datetime(Value::Datetime(Datetime(DateTime::<Utc>::from_timestamp(1_000_000_000, 0).unwrap())))]
#[case::uuid(Value::Uuid(Uuid::default()))]
#[case::string(Value::String("Hello, World!".to_string()))]
#[case::bytes(Value::Bytes(Bytes(::bytes::Bytes::from(vec![1_u8, 2, 3, 4, 5]))))]
#[case::thing(Value::RecordId(RecordId::new("test_table", RecordIdKey::Number(42))))] #[case::thing_range(Value::RecordId(RecordId::new("test_table", RecordIdKey::Range(Box::new(RecordIdKeyRange { start: Bound::Included(RecordIdKey::String("a".to_string())), end: Bound::Unbounded })))))]
#[case::object(Value::Object(Object(BTreeMap::from([("key".to_string(), Value::String("value".to_owned()))]))))]
#[case::array(Value::Array(Array::from(vec![Value::Number(Number::Int(1)), Value::Number(Number::Float(2.0))])))]
#[case::geometry::point(Value::Geometry(Geometry::Point(geo::Point::new(1.0, 2.0))))]
#[case::geometry::line(Value::Geometry(Geometry::Line(geo::LineString(vec![geo::Coord { x: 1.0, y: 2.0 }, geo::Coord { x: 3.0, y: 4.0 }]))))]
#[case::geometry::polygon(Value::Geometry(Geometry::Polygon(geo::Polygon::new(
geo::LineString(vec![geo::Coord { x: 0.0, y: 0.0 }, geo::Coord { x: 1.0, y: 1.0 }, geo::Coord { x: 0.0, y: 1.0 }]),
vec![geo::LineString(vec![geo::Coord { x: 0.5, y: 0.5 }, geo::Coord { x: 0.75, y: 0.75 }])]
))))]
#[case::geometry::multipoint(Value::Geometry(Geometry::MultiPoint(geo::MultiPoint(vec![geo::Point::new(1.0, 2.0), geo::Point::new(3.0, 4.0)]))))]
#[case::geometry::multiline(Value::Geometry(Geometry::MultiLine(geo::MultiLineString(vec![geo::LineString(vec![geo::Coord { x: 1.0, y: 2.0 }, geo::Coord { x: 3.0, y: 4.0 }])] ))))]
#[case::geometry::multipolygon(Value::Geometry(Geometry::MultiPolygon(geo::MultiPolygon(vec![geo::Polygon::new(
geo::LineString(vec![geo::Coord { x: 0.0, y: 0.0 }, geo::Coord { x: 1.0, y: 1.0 }, geo::Coord { x: 0.0, y: 1.0 }]),
vec![geo::LineString(vec![geo::Coord { x: 0.5, y: 0.5 }, geo::Coord { x: 0.75, y: 0.75 }])]
)]))))]
#[case::file(Value::File(File { bucket: "test_bucket".to_string(), key: "test_key".to_string() }))]
#[case::range(Value::Range(Box::new(Range { start: Bound::Included(Value::String("Hello, World!".to_string())), end: Bound::Excluded(Value::String("Hello, World!".to_string())) })))]
#[case::range(Value::Range(Box::new(Range { start: Bound::Unbounded, end: Bound::Excluded(Value::String("Hello, World!".to_string())) })))]
#[case::range(Value::Range(Box::new(Range { start: Bound::Included(Value::String("Hello, World!".to_string())), end: Bound::Unbounded })))]
#[case::regex(Value::Regex(Regex::from_str("/^[a-z]+$/").unwrap()))]
fn test_flatbuffers_roundtrip_value(#[case] input: Value) {
let mut builder = ::flatbuffers::FlatBufferBuilder::new();
let input_fb = input.to_fb(&mut builder).expect("Failed to convert to FlatBuffer");
builder.finish_minimal(input_fb);
let buf = builder.finished_data();
let value_fb =
::flatbuffers::root::<proto_fb::Value>(buf).expect("Failed to read FlatBuffer");
let value = Value::from_fb(value_fb).expect("Failed to convert from FlatBuffer");
assert_eq!(input, value, "Roundtrip conversion failed for input: {:?}", input);
}
}