use crate::value::{FromValue, Value};
use rustlavel_core::{Error, Json, Result};
use std::sync::Arc;
pub type Columns = Arc<Vec<String>>;
#[derive(Debug, Clone)]
pub struct Row {
columns: Columns,
values: Vec<Value>,
}
impl Row {
pub fn new(columns: Columns, values: Vec<Value>) -> Self {
Row { columns, values }
}
pub fn columns(&self) -> &[String] {
&self.columns
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn get<T: FromValue>(&self, column: &str) -> Result<T> {
let value = self.value(column)?;
T::from_value(value).map_err(|e| Error::msg(format!("column `{column}`: {e}")))
}
pub fn get_at<T: FromValue>(&self, index: usize) -> Result<T> {
let value = self
.values
.get(index)
.ok_or_else(|| Error::msg(format!("no column at index {index}")))?;
T::from_value(value)
}
pub fn get_or<T: FromValue>(&self, column: &str, default: T) -> T {
self.value(column).ok().and_then(|v| T::from_value(v).ok()).unwrap_or(default)
}
pub fn value(&self, column: &str) -> Result<&Value> {
let index = self
.columns
.iter()
.position(|name| name == column)
.ok_or_else(|| self.unknown_column(column))?;
Ok(&self.values[index])
}
pub fn value_at(&self, index: usize) -> Result<&Value> {
self.values
.get(index)
.ok_or_else(|| Error::msg(format!("no column at index {index}")))
}
pub fn has(&self, column: &str) -> bool {
self.columns.iter().any(|name| name == column)
}
fn unknown_column(&self, column: &str) -> Error {
Error::msg(format!(
"no column `{column}` in this result. Available: {}",
self.columns.join(", ")
))
}
pub fn to_json(&self) -> Json {
Json::object(
self.columns
.iter()
.cloned()
.zip(self.values.iter().cloned().map(Json::from))
.collect::<Vec<_>>(),
)
}
}
pub fn rows_to_json(rows: &[Row]) -> Json {
Json::Array(rows.iter().map(Row::to_json).collect())
}
#[cfg(test)]
mod tests {
use super::*;
fn row() -> Row {
let columns = Arc::new(vec!["id".to_string(), "name".to_string(), "note".to_string()]);
Row::new(columns, vec![Value::Int(7), Value::Text("Ada".into()), Value::Null])
}
#[test]
fn reads_columns_by_name_and_position() {
let row = row();
assert_eq!(row.get::<i64>("id").unwrap(), 7);
assert_eq!(row.get::<String>("name").unwrap(), "Ada");
assert_eq!(row.get::<Option<String>>("note").unwrap(), None);
assert_eq!(row.get_at::<i64>(0).unwrap(), 7);
}
#[test]
fn an_unknown_column_lists_what_is_available() {
let error = row().get::<i64>("nope").unwrap_err().to_string();
assert!(error.contains("no column `nope`"));
assert!(error.contains("id, name, note"));
}
#[test]
fn a_type_error_names_the_column() {
let error = row().get::<i64>("name").unwrap_err().to_string();
assert!(error.contains("column `name`"));
}
#[test]
fn converts_to_json() {
assert_eq!(
row().to_json().to_string(),
r#"{"id":7,"name":"Ada","note":null}"#
);
}
}