use umbral::web::HeaderMap;
use crate::AdminError;
pub(crate) fn q(name: &str) -> String {
name.replace('"', "\"\"")
}
pub(crate) fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
pub(crate) fn urlencoding_simple(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
out
}
pub(crate) fn is_htmx(headers: &HeaderMap) -> bool {
headers
.get("hx-request")
.and_then(|v| v.to_str().ok())
.map(|v| v == "true")
.unwrap_or(false)
}
pub(crate) fn apply_write_error_to_fields(
we: &umbral::orm::write::WriteError,
fields: &mut [crate::view::FormField],
) -> String {
let by_col = we.field_errors();
let mut unmatched: Vec<String> = Vec::new();
for (col, messages) in &by_col {
if let Some(f) = fields.iter_mut().find(|f| &f.name == col) {
let msg = messages.join("; ");
if f.error.is_empty() {
f.error = msg;
} else {
f.error.push_str("; ");
f.error.push_str(&msg);
}
} else {
for m in messages {
unmatched.push(format!("`{col}`: {m}"));
}
}
}
let mut banner_parts: Vec<String> = we.non_field_errors();
banner_parts.extend(unmatched);
banner_parts.join("; ")
}
pub(crate) fn sanitise_form_error(e: &AdminError) -> String {
match e {
AdminError::Sqlx(sqlx_err) => {
tracing::error!(error = %sqlx_err, "admin: form submission database error");
let msg = sqlx_err.to_string();
if let Some(col) = parse_unique_violation_column(&msg) {
return format!("A record with this `{col}` already exists.");
}
if is_unique_violation(&msg) {
return "A record with one of these values already exists.".to_string();
}
if let Some(tail) = msg.strip_prefix("umbral::orm::write: ") {
let mut chars = tail.chars();
return match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => "database error".to_string(),
};
}
if msg.starts_with("error returned from database:") {
if let Some(tail) = msg.splitn(2, ':').nth(1) {
let trimmed = tail.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
}
"database error".to_string()
}
AdminError::Write(write_err) => {
tracing::error!(error = %write_err, "admin: form submission validator error");
let msg = write_err.to_string();
let tail = msg.strip_prefix("umbral::orm::write: ").unwrap_or(&msg);
let mut chars = tail.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => "validation failed".to_string(),
}
}
AdminError::NotFound(msg) | AdminError::Render(msg) | AdminError::BadInput(msg) => {
msg.clone()
}
}
}
fn parse_any_datetime(raw: &str) -> Option<chrono::DateTime<chrono::Utc>> {
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
let s = raw.trim();
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt.with_timezone(&Utc));
}
for fmt in &["%Y-%m-%d %H:%M:%S%.f", "%Y-%m-%d %H:%M:%S"] {
if let Ok(naive) = NaiveDateTime::parse_from_str(s, fmt) {
return Some(naive.and_utc());
}
}
if let Ok(naive) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
return Some(naive.and_utc());
}
if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
return d.and_hms_opt(0, 0, 0).map(|n| n.and_utc());
}
None
}
pub(crate) fn humanize_date(raw: &str) -> String {
match parse_any_datetime(raw) {
Some(dt) => dt.format("%b %-d, %Y at %-I:%M %p").to_string(),
None => raw.to_string(),
}
}
pub(crate) fn naturaltime(raw: &str) -> String {
let Some(dt) = parse_any_datetime(raw) else {
return raw.to_string();
};
let now = chrono::Utc::now();
let delta = now.signed_duration_since(dt);
let secs = delta.num_seconds();
let (n, unit, past) = match secs.abs() {
s if s < 5 => return "just now".to_string(),
s if s < 60 => (s, "second", secs >= 0),
s if s < 3600 => (s / 60, "minute", secs >= 0),
s if s < 86_400 => (s / 3600, "hour", secs >= 0),
s if s < 604_800 => (s / 86_400, "day", secs >= 0),
s if s < 2_592_000 => (s / 604_800, "week", secs >= 0),
s if s < 31_536_000 => (s / 2_592_000, "month", secs >= 0),
s => (s / 31_536_000, "year", secs >= 0),
};
let plural = if n == 1 { "" } else { "s" };
if past {
format!("{n} {unit}{plural} ago")
} else {
format!("in {n} {unit}{plural}")
}
}
pub(crate) fn parse_unique_violation_column(msg: &str) -> Option<String> {
if let Some(idx) = msg.find("UNIQUE constraint failed: ") {
let tail = &msg[idx + "UNIQUE constraint failed: ".len()..];
let first = tail
.split(',')
.next()
.unwrap_or(tail)
.trim()
.trim_end_matches(|c: char| c == ')' || c == '"' || c == '\'');
let bare = first.rsplit('.').next().unwrap_or(first);
if !bare.is_empty() {
return Some(bare.to_string());
}
}
if let Some(idx) = msg.find("Key (") {
let tail = &msg[idx + "Key (".len()..];
if let Some(end) = tail.find(')') {
let col = tail[..end].trim();
if !col.is_empty() {
return Some(col.to_string());
}
}
}
None
}
fn is_unique_violation(msg: &str) -> bool {
msg.contains("UNIQUE constraint failed")
|| msg.contains("duplicate key value violates unique constraint")
}
#[cfg(test)]
mod tests {
use super::{is_unique_violation, parse_unique_violation_column};
#[test]
fn sqlite_unique_violation_extracts_column_after_dot() {
let msg =
"error returned from database: (code: 2067) UNIQUE constraint failed: profile.user";
assert_eq!(parse_unique_violation_column(msg).as_deref(), Some("user"));
assert!(is_unique_violation(msg));
}
#[test]
fn sqlite_unique_violation_takes_first_column_of_compound_index() {
let msg = "UNIQUE constraint failed: post.slug, post.lang";
assert_eq!(parse_unique_violation_column(msg).as_deref(), Some("slug"));
}
#[test]
fn postgres_unique_violation_extracts_column_from_key_clause() {
let msg = "error returned from database: duplicate key value violates unique constraint \
\"profile_user_key\": Key (user)=(7) already exists.";
assert_eq!(parse_unique_violation_column(msg).as_deref(), Some("user"));
assert!(is_unique_violation(msg));
}
#[test]
fn non_unique_error_returns_none() {
let msg = "error returned from database: FOREIGN KEY constraint failed";
assert!(parse_unique_violation_column(msg).is_none());
assert!(!is_unique_violation(msg));
}
#[test]
fn fallback_detector_catches_unparseable_unique_errors() {
let msg = "UNIQUE constraint failed";
assert!(parse_unique_violation_column(msg).is_none());
assert!(is_unique_violation(msg));
}
}