use std::fmt::Write as _;
use crate::core::{FieldSchema, FieldType};
use crate::sql::sqlx::{self, postgres::PgRow, Postgres, Row};
pub(crate) fn escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}
pub(crate) fn render_value(row: &PgRow, field: &FieldSchema) -> String {
match field.ty {
FieldType::I32 => format_display::<i32>(row, field),
FieldType::I64 => format_display::<i64>(row, field),
FieldType::F32 => format_display::<f32>(row, field),
FieldType::F64 => format_display::<f64>(row, field),
FieldType::Bool => format_display::<bool>(row, field),
FieldType::String => format_display::<String>(row, field),
FieldType::DateTime => format_display::<chrono::DateTime<chrono::Utc>>(row, field),
FieldType::Date => format_display::<chrono::NaiveDate>(row, field),
FieldType::Uuid => format_display::<uuid::Uuid>(row, field),
FieldType::Json => format_json(row, field),
}
}
fn format_display<T>(row: &PgRow, field: &FieldSchema) -> String
where
T: std::fmt::Display + for<'r> sqlx::Decode<'r, Postgres> + sqlx::Type<Postgres>,
{
match row.try_get::<Option<T>, _>(field.column) {
Ok(Some(v)) => escape(&v.to_string()),
Ok(None) => "<em>NULL</em>".to_owned(),
Err(e) => format!("<em><error: {}></em>", escape(&e.to_string())),
}
}
fn format_json(row: &PgRow, field: &FieldSchema) -> String {
let _ = (row, field);
"<em>JSON (rendering disabled)</em>".to_owned()
}
pub(crate) fn read_value_as_json(row: &PgRow, field: &FieldSchema) -> serde_json::Value {
use serde_json::Value;
match field.ty {
FieldType::I32 => row
.try_get::<Option<i32>, _>(field.column)
.ok()
.flatten()
.map(|v| Value::from(v))
.unwrap_or(Value::Null),
FieldType::I64 => row
.try_get::<Option<i64>, _>(field.column)
.ok()
.flatten()
.map(|v| Value::from(v))
.unwrap_or(Value::Null),
FieldType::F32 => row
.try_get::<Option<f32>, _>(field.column)
.ok()
.flatten()
.map(|v| Value::from(v))
.unwrap_or(Value::Null),
FieldType::F64 => row
.try_get::<Option<f64>, _>(field.column)
.ok()
.flatten()
.map(|v| Value::from(v))
.unwrap_or(Value::Null),
FieldType::Bool => row
.try_get::<Option<bool>, _>(field.column)
.ok()
.flatten()
.map(Value::from)
.unwrap_or(Value::Null),
FieldType::String => row
.try_get::<Option<String>, _>(field.column)
.ok()
.flatten()
.map(Value::from)
.unwrap_or(Value::Null),
FieldType::Uuid => row
.try_get::<Option<uuid::Uuid>, _>(field.column)
.ok()
.flatten()
.map(|v| Value::from(v.to_string()))
.unwrap_or(Value::Null),
FieldType::Date => row
.try_get::<Option<chrono::NaiveDate>, _>(field.column)
.ok()
.flatten()
.map(|d| Value::from(d.format("%Y-%m-%d").to_string()))
.unwrap_or(Value::Null),
FieldType::DateTime => row
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(field.column)
.ok()
.flatten()
.map(|d| Value::from(d.to_rfc3339()))
.unwrap_or(Value::Null),
FieldType::Json => row
.try_get::<Option<Value>, _>(field.column)
.ok()
.flatten()
.unwrap_or(Value::Null),
}
}
pub(crate) fn coerce_form_to_json(field: &FieldSchema, raw: &str) -> serde_json::Value {
use serde_json::Value;
if raw.is_empty() && field.nullable {
return Value::Null;
}
match field.ty {
FieldType::I32 => raw.parse::<i32>().map(Value::from).unwrap_or_else(|_| Value::String(raw.to_owned())),
FieldType::I64 => raw.parse::<i64>().map(Value::from).unwrap_or_else(|_| Value::String(raw.to_owned())),
FieldType::F32 => raw.parse::<f32>().map(Value::from).unwrap_or_else(|_| Value::String(raw.to_owned())),
FieldType::F64 => raw.parse::<f64>().map(Value::from).unwrap_or_else(|_| Value::String(raw.to_owned())),
FieldType::Bool => match raw.to_ascii_lowercase().as_str() {
"true" | "on" | "1" | "yes" => Value::Bool(true),
"false" | "off" | "0" | "no" | "" => Value::Bool(false),
_ => Value::String(raw.to_owned()),
},
_ => Value::String(raw.to_owned()),
}
}
pub(crate) fn render_value_for_input(row: &PgRow, field: &FieldSchema) -> String {
fn opt_to_string<T: std::fmt::Display>(v: Option<T>) -> String {
v.map(|x| x.to_string()).unwrap_or_default()
}
match field.ty {
FieldType::I32 => opt_to_string(row.try_get::<Option<i32>, _>(field.column).ok().flatten()),
FieldType::I64 => opt_to_string(row.try_get::<Option<i64>, _>(field.column).ok().flatten()),
FieldType::F32 => opt_to_string(row.try_get::<Option<f32>, _>(field.column).ok().flatten()),
FieldType::F64 => opt_to_string(row.try_get::<Option<f64>, _>(field.column).ok().flatten()),
FieldType::Bool => row
.try_get::<Option<bool>, _>(field.column)
.ok()
.flatten()
.map(|b| b.to_string())
.unwrap_or_default(),
FieldType::String => row
.try_get::<Option<String>, _>(field.column)
.ok()
.flatten()
.unwrap_or_default(),
FieldType::Uuid => opt_to_string(
row.try_get::<Option<uuid::Uuid>, _>(field.column)
.ok()
.flatten(),
),
FieldType::Date => row
.try_get::<Option<chrono::NaiveDate>, _>(field.column)
.ok()
.flatten()
.map(|d| d.format("%Y-%m-%d").to_string())
.unwrap_or_default(),
FieldType::DateTime => row
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(field.column)
.ok()
.flatten()
.map(|d| d.format("%Y-%m-%dT%H:%M:%S").to_string())
.unwrap_or_default(),
FieldType::Json => String::new(),
}
}
pub(crate) fn render_input(field: &FieldSchema, value: &str, pk_locked: bool) -> String {
let name = escape(field.name);
let val = escape(value);
let required = if field.nullable || field.ty == FieldType::Bool {
""
} else {
" required"
};
let readonly = if pk_locked { " readonly" } else { "" };
match field.ty {
FieldType::Bool => {
let checked = if value == "true" { " checked" } else { "" };
format!(
r#"<input type="checkbox" name="{name}" id="{name}" value="true"{checked}{readonly}>"#
)
}
FieldType::I32 | FieldType::I64 => {
let mut attrs = String::new();
if let Some(min) = field.min {
let _ = write!(attrs, r#" min="{min}""#);
}
if let Some(max) = field.max {
let _ = write!(attrs, r#" max="{max}""#);
}
format!(
r#"<input type="number" step="1" name="{name}" id="{name}" value="{val}"{attrs}{required}{readonly}>"#
)
}
FieldType::F32 | FieldType::F64 => {
format!(
r#"<input type="number" step="any" name="{name}" id="{name}" value="{val}"{required}{readonly}>"#
)
}
FieldType::String => match field.max_length {
Some(n) if n <= 80 => format!(
r#"<input type="text" name="{name}" id="{name}" value="{val}" maxlength="{n}"{required}{readonly}>"#
),
Some(n) => format!(
r#"<textarea name="{name}" id="{name}" maxlength="{n}"{required}{readonly}>{val}</textarea>"#
),
None => format!(
r#"<textarea name="{name}" id="{name}"{required}{readonly}>{val}</textarea>"#
),
},
FieldType::Date => format!(
r#"<input type="date" name="{name}" id="{name}" value="{val}"{required}{readonly}>"#
),
FieldType::DateTime => format!(
r#"<input type="datetime-local" name="{name}" id="{name}" value="{val}"{required}{readonly}>"#
),
FieldType::Uuid => format!(
r#"<input type="text" name="{name}" id="{name}" value="{val}" pattern="[0-9a-fA-F\-]+"{required}{readonly}>"#
),
FieldType::Json => format!(
r#"<textarea name="{name}" id="{name}" disabled>JSON editing disabled in v0.1</textarea>"#
),
}
}
pub(crate) fn read_value_as_string(row: &PgRow, field: &FieldSchema) -> Option<String> {
read_value_as_string_at(row, field, field.column)
}
pub(crate) fn read_value_as_string_at(
row: &PgRow,
field: &FieldSchema,
column_alias: &str,
) -> Option<String> {
match field.ty {
FieldType::I32 => row
.try_get::<Option<i32>, _>(column_alias)
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::I64 => row
.try_get::<Option<i64>, _>(column_alias)
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::String => row
.try_get::<Option<String>, _>(column_alias)
.ok()
.flatten(),
FieldType::Uuid => row
.try_get::<Option<uuid::Uuid>, _>(column_alias)
.ok()
.flatten()
.map(|v| v.to_string()),
_ => None,
}
}
pub(crate) fn read_joined_value_as_html(
row: &PgRow,
alias: &str,
field: &FieldSchema,
) -> Option<String> {
let prefixed = format!("{}__{}", alias, field.column);
let text: Option<String> = match field.ty {
FieldType::I32 => row
.try_get::<Option<i32>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::I64 => row
.try_get::<Option<i64>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::F32 => row
.try_get::<Option<f32>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::F64 => row
.try_get::<Option<f64>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::Bool => row
.try_get::<Option<bool>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::String => row
.try_get::<Option<String>, _>(prefixed.as_str())
.ok()
.flatten(),
FieldType::Uuid => row
.try_get::<Option<uuid::Uuid>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::Date => row
.try_get::<Option<chrono::NaiveDate>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::DateTime => row
.try_get::<Option<chrono::DateTime<chrono::Utc>>, _>(prefixed.as_str())
.ok()
.flatten()
.map(|v| v.to_string()),
FieldType::Json => None,
};
text.map(|s| escape(&s))
}