use std::fmt::Display; use anyhow::Result; use chrono::{DateTime, Utc}; use derive_builder::Builder; use sqlx::{ error::BoxDynError, postgres::{PgRow, PgTypeInfo, PgValueRef}, ColumnIndex, Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, }; use uuid::Uuid; pub enum Value { Text(Option), Integer(Option), Timestamptz(Option>), Uuid(Option), } #[derive(Builder)] #[builder(pattern = "owned", setter(prefix = "with"))] pub struct FieldOptions { /// Format with which to render timestamptz values #[builder(default = "\"%Y-%m-%dT%H:%M:%S%.f%:z\".to_owned()")] pub date_format: String, /// If some, treat text column like an enum #[builder(default)] pub select_options: Option>, /// Text to display in place of actual column name #[builder(default)] pub label: Option, #[builder(default = "true")] pub editable: bool, } pub trait ToHtmlString { fn to_html_string(&self, options: &FieldOptions) -> String; } #[derive(Clone, Debug)] pub struct FromSqlError { message: String, } impl std::error::Error for FromSqlError {} impl Display for FromSqlError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl FromSqlError { fn new(message: &str) -> Self { Self { message: message.to_owned(), } } } impl Value { pub fn get_from_row + Display>( row: &PgRow, idx: I, ) -> Result { let value_ref = row.try_get_raw(idx)?; Self::decode(value_ref) } } impl ToHtmlString for Value { fn to_html_string(&self, options: &FieldOptions) -> String { macro_rules! cell_html { ($component:expr, $value:expr$(, $attr_name:expr => $attr_val:expr)*) => { { let value = $value.clone(); let attrs: Vec = vec![ format!("value=\"{}\"", serde_json::to_string(&value).unwrap().replace('"', "\\\"")), $(format!("{}=\"{}\"", $attr_name, $attr_val.replace('"', "\\\"")),)* ]; format!( "<{} {}>{}", $component, attrs.join(" "), value.map(|value| value.to_string()).unwrap_or("-".to_owned()), $component, ) } }; } match self { Self::Text(value) => cell_html!("cell-text", value), Self::Integer(value) => cell_html!("cell-integer", value), Self::Timestamptz(value) => cell_html!( "cell-timestamptz", value, "format" => options.date_format ), Self::Uuid(value) => cell_html!("cell-uuid", value), } } } impl sqlx::Type for Value { fn type_info() -> ::TypeInfo { PgTypeInfo::with_name("XXX"); todo!() } } impl<'a> Decode<'a, Postgres> for Value { fn decode(value: PgValueRef<'a>) -> Result { let type_info = value.type_info(); let ty = type_info.name(); match ty { "INT" | "INT4" => Ok(Self::Integer(if value.is_null() { None } else { Some(>::decode(value)?) })), "TEXT" | "VARCHAR" => Ok(Self::Text(if value.is_null() { None } else { Some(>::decode(value)?) })), "TIMESTAMPTZ" => Ok(Self::Timestamptz(if value.is_null() { None } else { Some( as Decode>::decode(value)?) })), "UUID" => Ok(Self::Uuid(if value.is_null() { None } else { Some(>::decode(value)?) })), _ => Err(Box::new(FromSqlError::new( "unsupported pg type for interim Value", ))), } } } pub struct Lens { pub fields: Vec, } pub struct Field { pub options: FieldOptions, pub name: String, }