use std::collections::BTreeMap;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Int64(i64),
Uint64(u64),
Double(f64),
Boolean(bool),
String(Vec<u8>),
Any(Vec<u8>),
}
impl Value {
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::String(bytes) | Self::Any(bytes) => Some(bytes),
_ => None,
}
}
pub fn as_str(&self) -> Option<&str> {
std::str::from_utf8(self.as_bytes()?).ok()
}
pub fn as_i64(&self) -> Option<i64> {
match self {
Self::Int64(value) => Some(*value),
_ => None,
}
}
pub fn as_u64(&self) -> Option<u64> {
match self {
Self::Uint64(value) => Some(*value),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Self::Double(value) => Some(*value),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Boolean(value) => Some(*value),
_ => None,
}
}
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
}
impl From<i64> for Value {
fn from(value: i64) -> Self {
Self::Int64(value)
}
}
impl From<u64> for Value {
fn from(value: u64) -> Self {
Self::Uint64(value)
}
}
impl From<f64> for Value {
fn from(value: f64) -> Self {
Self::Double(value)
}
}
impl From<bool> for Value {
fn from(value: bool) -> Self {
Self::Boolean(value)
}
}
impl From<&str> for Value {
fn from(value: &str) -> Self {
Self::String(value.as_bytes().to_vec())
}
}
impl From<String> for Value {
fn from(value: String) -> Self {
Self::String(value.into_bytes())
}
}
impl From<Vec<u8>> for Value {
fn from(value: Vec<u8>) -> Self {
Self::String(value)
}
}
impl<T: Into<Value>> From<Option<T>> for Value {
fn from(value: Option<T>) -> Self {
match value {
Some(value) => value.into(),
None => Self::Null,
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Row {
columns: Vec<(String, Value)>,
}
impl Row {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.columns.push((name.into(), value.into()));
self
}
pub fn set(&mut self, name: impl Into<String>, value: impl Into<Value>) {
self.columns.push((name.into(), value.into()));
}
pub fn get(&self, name: &str) -> Option<&Value> {
self.columns
.iter()
.find(|(column, _)| column == name)
.map(|(_, value)| value)
}
pub fn columns(&self) -> &[(String, Value)] {
&self.columns
}
pub fn names(&self) -> impl Iterator<Item = &str> {
self.columns.iter().map(|(name, _)| name.as_str())
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn is_empty(&self) -> bool {
self.columns.is_empty()
}
pub fn to_map(&self) -> BTreeMap<&str, &Value> {
self.columns
.iter()
.map(|(name, value)| (name.as_str(), value))
.collect()
}
}
impl FromIterator<(String, Value)> for Row {
fn from_iter<I: IntoIterator<Item = (String, Value)>>(iterator: I) -> Self {
Self {
columns: iterator.into_iter().collect(),
}
}
}
impl fmt::Display for Row {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("{")?;
for (index, (name, value)) in self.columns.iter().enumerate() {
if index > 0 {
formatter.write_str(", ")?;
}
match value {
Value::String(bytes) | Value::Any(bytes) => match std::str::from_utf8(bytes) {
Ok(text) => write!(formatter, "{name}={text:?}")?,
Err(_) => write!(formatter, "{name}=<{} bytes>", bytes.len())?,
},
other => write!(formatter, "{name}={other:?}")?,
}
}
formatter.write_str("}")
}
}
pub type MaybeRow = Option<Row>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_row_keeps_the_order_its_columns_were_added_in() {
let row = Row::new().with("b", 2i64).with("a", 1i64).with("c", 3i64);
assert_eq!(row.names().collect::<Vec<_>>(), ["b", "a", "c"]);
}
#[test]
fn columns_are_read_by_name() {
let row = Row::new().with("key", 42i64).with("value", "hello");
assert_eq!(row.get("key"), Some(&Value::Int64(42)));
assert_eq!(row.get("value").and_then(Value::as_str), Some("hello"));
assert_eq!(row.get("absent"), None);
}
#[test]
fn strings_are_bytes_and_need_not_be_utf8() {
let row = Row::new().with("raw", vec![0xff, 0xfe]);
assert_eq!(row.get("raw").unwrap().as_bytes(), Some(&[0xff, 0xfe][..]));
assert_eq!(
row.get("raw").unwrap().as_str(),
None,
"not UTF-8, and that is allowed"
);
}
#[test]
fn an_option_becomes_null() {
let row = Row::new()
.with("present", Some(1i64))
.with("absent", None::<i64>);
assert_eq!(row.get("present"), Some(&Value::Int64(1)));
assert!(row.get("absent").unwrap().is_null());
}
#[test]
fn display_is_readable_and_does_not_choke_on_binary() {
let row = Row::new().with("key", 1i64).with("blob", vec![0xff, 0x00]);
assert_eq!(row.to_string(), r#"{key=Int64(1), blob=<2 bytes>}"#);
}
#[test]
fn accessors_report_the_wrong_type_as_absent() {
let value = Value::Int64(1);
assert_eq!(value.as_i64(), Some(1));
assert_eq!(value.as_u64(), None);
assert_eq!(value.as_str(), None);
assert!(!value.is_null());
}
}