1
0
Fork 0
forked from 2sys/phonograph
phonograph/interim-server/src/data_layer.rs
2025-07-08 14:37:03 -07:00

128 lines
3.8 KiB
Rust

use std::fmt::Display;
use anyhow::Result;
use chrono::{DateTime, Utc};
use interim_models::selection::SelectionDisplayType;
use sqlx::{
ColumnIndex, Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _,
error::BoxDynError,
postgres::{PgRow, PgTypeInfo, PgValueRef},
};
use uuid::Uuid;
const DEFAULT_TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z";
pub enum Value {
Text(Option<String>),
Integer(Option<i32>),
Timestamptz(Option<DateTime<Utc>>),
Uuid(Option<Uuid>),
}
pub trait ToHtmlString {
fn to_html_string(&self, display_type: &Option<SelectionDisplayType>) -> String;
}
// TODO rewrite with thiserror
#[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<I: ColumnIndex<PgRow> + Display>(
row: &PgRow,
idx: I,
) -> Result<Self, BoxDynError> {
let value_ref = row.try_get_raw(idx)?;
Self::decode(value_ref)
}
}
impl ToHtmlString for Value {
fn to_html_string(&self, display_type: &Option<SelectionDisplayType>) -> String {
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" => DEFAULT_TIMESTAMP_FORMAT
),
Self::Uuid(value) => cell_html!("cell-uuid", value),
}
}
}
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();
match ty {
"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)?)
})),
_ => Err(Box::new(FromSqlError::new(
"unsupported pg type for interim Value",
))),
}
}
}