use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
use diesel::deserialize::{self, FromSql};
use diesel::serialize::{self, IsNull, Output, ToSql};
use diesel::sql_types::Text;
use diesel::sqlite::Sqlite;
use serde::{Deserialize, Serialize};
macro_rules! text_backed_newtype_impls {
(
$name:ident($inner:ty), encode = |$value:ident| $encode:expr $(,)?
) => {
impl $name {
#[must_use]
pub const fn into_inner(self) -> $inner {
self.0
}
}
impl From<$inner> for $name {
fn from(value: $inner) -> Self {
Self(value)
}
}
impl From<$name> for $inner {
fn from(value: $name) -> Self {
value.0
}
}
impl Deref for $name {
type Target = $inner;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<$inner> for $name {
fn as_ref(&self) -> &$inner {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl FromStr for $name {
type Err = <$inner as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
<$inner as FromStr>::from_str(s).map(Self)
}
}
impl ToSql<Text, Sqlite> for $name {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result {
let $value = &self.0;
out.set_value($encode);
Ok(IsNull::No)
}
}
impl FromSql<Text, Sqlite> for $name {
fn from_sql(
bytes: <Sqlite as diesel::backend::Backend>::RawValue<'_>,
) -> deserialize::Result<Self> {
let text = <String as FromSql<Text, Sqlite>>::from_sql(bytes)?;
text.parse().map_err(Into::into)
}
}
};
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Default,
Serialize,
Deserialize,
diesel::AsExpression,
diesel::FromSqlRow,
)]
#[diesel(sql_type = Text)]
#[serde(transparent)]
pub struct SqliteUuid(pub uuid::Uuid);
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Default,
Serialize,
Deserialize,
diesel::AsExpression,
diesel::FromSqlRow,
)]
#[diesel(sql_type = Text)]
#[serde(transparent)]
pub struct SqliteDecimal(pub rust_decimal::Decimal);
text_backed_newtype_impls!(SqliteUuid(uuid::Uuid), encode = |value| value.to_string());
text_backed_newtype_impls!(
SqliteDecimal(rust_decimal::Decimal),
encode = |value| value.normalize().to_string(),
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn uuid_newtype_round_trips_through_text() {
let id = uuid::Uuid::from_u128(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
let wrapped = SqliteUuid::from(id);
assert_eq!(wrapped.to_string(), id.to_string());
assert_eq!(wrapped.to_string().parse::<SqliteUuid>().unwrap(), wrapped);
assert_eq!(uuid::Uuid::from(wrapped), id);
assert_eq!(*wrapped, id);
}
#[test]
fn decimal_newtype_round_trips_through_text() {
let value = SqliteDecimal::from(rust_decimal::Decimal::from_str("-1234.5678").unwrap());
assert_eq!(value.to_string().parse::<SqliteDecimal>().unwrap(), value);
}
#[test]
fn decimal_encoding_is_canonical_per_value() {
let written = rust_decimal::Decimal::from_str("19.990").unwrap();
let typed = rust_decimal::Decimal::from_str("19.99").unwrap();
assert_eq!(written, typed, "the two spellings are one value");
assert_ne!(
written.to_string(),
typed.to_string(),
"…but Decimal's own text differs, which SQLite would treat as two rows"
);
assert_eq!(
written.normalize().to_string(),
typed.normalize().to_string()
);
assert_eq!(
rust_decimal::Decimal::from_str("0.10")
.unwrap()
.normalize()
.to_string(),
"0.1",
);
}
#[test]
fn newtypes_serialize_transparently() {
let id = uuid::Uuid::from_u128(9);
assert_eq!(
serde_json::to_string(&SqliteUuid::from(id)).unwrap(),
serde_json::to_string(&id).unwrap(),
);
let dec = rust_decimal::Decimal::from_str("-3.25").unwrap();
assert_eq!(
serde_json::to_string(&SqliteDecimal::from(dec)).unwrap(),
serde_json::to_string(&dec).unwrap(),
);
}
}