2025-05-13 00:02:33 -07:00
|
|
|
use std::fmt::Display;
|
|
|
|
|
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
|
use derive_builder::Builder;
|
2025-05-26 22:08:21 -07:00
|
|
|
use sqlx::{
|
|
|
|
|
error::BoxDynError,
|
|
|
|
|
postgres::{PgRow, PgTypeInfo, PgValueRef},
|
|
|
|
|
ColumnIndex, Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _,
|
|
|
|
|
};
|
2025-05-13 00:02:33 -07:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
2025-05-26 22:08:21 -07:00
|
|
|
pub enum Value {
|
|
|
|
|
Text(Option<String>),
|
|
|
|
|
Integer(Option<i32>),
|
|
|
|
|
Timestamptz(Option<DateTime<Utc>>),
|
|
|
|
|
Uuid(Option<Uuid>),
|
2025-05-13 00:02:33 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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<Vec<String>>,
|
|
|
|
|
|
|
|
|
|
/// Text to display in place of actual column name
|
|
|
|
|
#[builder(default)]
|
|
|
|
|
pub label: Option<String>,
|
|
|
|
|
|
|
|
|
|
#[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 {
|
2025-05-26 22:08:21 -07:00
|
|
|
pub fn get_from_row<I: ColumnIndex<PgRow> + Display>(
|
|
|
|
|
row: &PgRow,
|
|
|
|
|
idx: I,
|
|
|
|
|
) -> Result<Self, BoxDynError> {
|
|
|
|
|
let value_ref = row.try_get_raw(idx)?;
|
|
|
|
|
Self::decode(value_ref)
|
2025-05-13 00:02:33 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ToHtmlString for Value {
|
|
|
|
|
fn to_html_string(&self, options: &FieldOptions) -> String {
|
2025-05-26 22:08:21 -07:00
|
|
|
macro_rules! cell_html {
|
|
|
|
|
($component:expr, $value:expr$(, $attr_name:expr => $attr_val:expr)*) => {
|
|
|
|
|
{
|
|
|
|
|
let value = $value.clone();
|
|
|
|
|
let attrs: Vec<String> = 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),
|
2025-05-13 00:02:33 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 22:08:21 -07:00
|
|
|
impl sqlx::Type<Postgres> for Value {
|
|
|
|
|
fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
|
|
|
|
|
PgTypeInfo::with_name("XXX");
|
|
|
|
|
todo!()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> Decode<'a, Postgres> for Value {
|
|
|
|
|
fn decode(value: PgValueRef<'a>) -> Result<Self, BoxDynError> {
|
|
|
|
|
let type_info = value.type_info();
|
|
|
|
|
let ty = type_info.name();
|
2025-05-13 00:02:33 -07:00
|
|
|
match ty {
|
2025-05-26 22:08:21 -07:00
|
|
|
"INT" | "INT4" => Ok(Self::Integer(if value.is_null() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(<i32 as Decode<Postgres>>::decode(value)?)
|
|
|
|
|
})),
|
|
|
|
|
"TEXT" | "VARCHAR" => Ok(Self::Text(if value.is_null() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(<String as Decode<Postgres>>::decode(value)?)
|
|
|
|
|
})),
|
|
|
|
|
"TIMESTAMPTZ" => Ok(Self::Timestamptz(if value.is_null() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(<DateTime<Utc> as Decode<Postgres>>::decode(value)?)
|
|
|
|
|
})),
|
|
|
|
|
"UUID" => Ok(Self::Uuid(if value.is_null() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(<Uuid as Decode<Postgres>>::decode(value)?)
|
|
|
|
|
})),
|
2025-05-13 00:02:33 -07:00
|
|
|
_ => Err(Box::new(FromSqlError::new(
|
|
|
|
|
"unsupported pg type for interim Value",
|
|
|
|
|
))),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Lens {
|
|
|
|
|
pub fields: Vec<Field>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Field {
|
|
|
|
|
pub options: FieldOptions,
|
|
|
|
|
pub name: String,
|
|
|
|
|
}
|