use std::fmt::Display; use anyhow::Result; use chrono::{DateTime, Utc}; use deadpool_postgres::tokio_postgres::{ row::RowIndex, types::{FromSql, Type}, Row, }; use derive_builder::Builder; use uuid::Uuid; pub enum Contents { Text(String), Integer(i32), Timestamptz(DateTime), Uuid(Uuid), } pub struct Value(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(row: &Row, idx: I) -> Result { Ok(Self(row.try_get::<_, Option>(idx)?)) } } impl ToHtmlString for Value { fn to_html_string(&self, options: &FieldOptions) -> String { if let Self(Some(contents)) = self { match contents { Contents::Text(value) => value.clone(), &Contents::Integer(value) => value.to_string(), &Contents::Timestamptz(value) => value.format(&options.date_format).to_string(), &Contents::Uuid(value) => format!( "{}", value.hyphenated() ), } } else { "NULL".to_owned() } } } impl<'a> FromSql<'a> for Contents { fn from_sql( ty: &Type, raw: &'a [u8], ) -> Result> { match ty { &Type::INT4 => Ok(Self::Integer(i32::from_sql(ty, raw)?)), &Type::TEXT | &Type::VARCHAR => Ok(Self::Text(String::from_sql(ty, raw)?)), &Type::TIMESTAMPTZ => Ok(Self::Timestamptz(DateTime::::from_sql(ty, raw)?)), &Type::UUID => Ok(Self::Uuid(::from_sql(ty, raw)?)), _ => Err(Box::new(FromSqlError::new( "unsupported pg type for interim Value", ))), } } fn accepts(ty: &Type) -> bool { matches!(ty, &Type::TEXT | &Type::VARCHAR | &Type::UUID) } } pub struct Lens { pub fields: Vec, } pub struct Field { pub options: FieldOptions, pub name: String, }