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);
impl crate::openapi::OpenApiSchema for SqliteUuid {
fn schema_name() -> &'static str {
<uuid::Uuid as crate::openapi::OpenApiSchema>::schema_name()
}
fn schema() -> serde_json::Value {
<uuid::Uuid as crate::openapi::OpenApiSchema>::schema()
}
}
impl crate::openapi::OpenApiSchema for SqliteDecimal {
fn schema_name() -> &'static str {
<rust_decimal::Decimal as crate::openapi::OpenApiSchema>::schema_name()
}
fn schema() -> serde_json::Value {
<rust_decimal::Decimal as crate::openapi::OpenApiSchema>::schema()
}
}
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::*;
use crate::openapi::OpenApiSchema as _;
#[derive(crate::openapi::OpenApiSchema)]
#[allow(dead_code)]
struct Row {
id: SqliteUuid,
price: SqliteDecimal,
}
#[test]
fn wrappers_document_the_same_scalars_as_their_wrapped_types() {
let props = Row::schema()["properties"].clone();
assert!(props["id"].get("$ref").is_none(), "{props}");
assert!(props["price"].get("$ref").is_none(), "{props}");
assert_eq!(props["id"], uuid::Uuid::schema());
assert_eq!(props["price"], rust_decimal::Decimal::schema());
assert_eq!(props["id"]["format"], "uuid");
}
#[test]
fn macro_identity_literals_match_the_real_type_names() {
assert_eq!(
std::any::type_name::<SqliteUuid>(),
"autumn_web::db::sqlite_types::SqliteUuid"
);
assert_eq!(
std::any::type_name::<SqliteDecimal>(),
"autumn_web::db::sqlite_types::SqliteDecimal"
);
}
#[test]
fn wrappers_implement_the_schema_trait() {
assert_eq!(SqliteUuid::schema(), uuid::Uuid::schema());
assert_eq!(SqliteDecimal::schema(), rust_decimal::Decimal::schema());
}
#[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(),
);
}
}