pub const TYPES_MODULE: &str = "types";
pub const INTERVAL: &str = "sqlx::postgres::types::PgInterval";
pub const BYTES: &str = "Vec<u8>";
pub const TIME_TZ: &str = "sqlx::postgres::types::PgTimeTz";
pub const MONEY: &str = "sqlx::postgres::types::PgMoney";
pub const RANGE: &str = "sqlx::postgres::types::PgRange";
pub const IP_NETWORK: &str = "ipnetwork::IpNetwork";
pub const MAC_ADDRESS: &str = "mac_address::MacAddress";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Shape<'a> {
Optional(&'a str),
List(&'a str),
Value(&'a str),
}
impl<'a> Shape<'a> {
pub fn of(rust_type: &'a str) -> Self {
if let Some(inner) = generic_argument(rust_type, "Option") {
Shape::Optional(inner)
} else if let Some(item) = generic_argument(rust_type, "Vec") {
Shape::List(item)
} else {
Shape::Value(rust_type)
}
}
}
fn generic_argument<'a>(rust_type: &'a str, wrapper: &str) -> Option<&'a str> {
rust_type
.strip_prefix(wrapper)?
.strip_prefix('<')?
.strip_suffix('>')
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BaseType {
Integer,
Float,
Decimal,
Bool,
Text,
Uuid,
Date,
Time,
Timestamp,
TimestampTz,
Interval,
Bytes,
TimeTz,
Money,
Range,
IpNetwork,
MacAddress,
Enum,
Array,
Other,
}
impl BaseType {
pub fn of(sqlx_type: &str) -> Self {
match sqlx_type {
"i16" | "i32" | "i64" => BaseType::Integer,
"f32" | "f64" => BaseType::Float,
"rust_decimal::Decimal" => BaseType::Decimal,
"bool" => BaseType::Bool,
"String" => BaseType::Text,
"uuid::Uuid" => BaseType::Uuid,
"chrono::NaiveDate" => BaseType::Date,
"chrono::NaiveTime" => BaseType::Time,
"chrono::NaiveDateTime" => BaseType::Timestamp,
"chrono::DateTime<chrono::Utc>" => BaseType::TimestampTz,
INTERVAL => BaseType::Interval,
BYTES => BaseType::Bytes,
TIME_TZ => BaseType::TimeTz,
MONEY => BaseType::Money,
IP_NETWORK => BaseType::IpNetwork,
MAC_ADDRESS => BaseType::MacAddress,
other if generic_argument(other, RANGE).is_some() => BaseType::Range,
other if is_enum_path(other) => BaseType::Enum,
other => match Shape::of(other) {
Shape::List(_) => BaseType::Array,
Shape::Optional(_) | Shape::Value(_) => BaseType::Other,
},
}
}
pub fn is_timestamp(self) -> bool {
matches!(self, BaseType::Timestamp | BaseType::TimestampTz)
}
pub fn supports_typed_id(self) -> bool {
matches!(
self,
BaseType::Integer
| BaseType::Bool
| BaseType::Text
| BaseType::Uuid
| BaseType::Decimal
| BaseType::Date
| BaseType::Time
| BaseType::Timestamp
| BaseType::TimestampTz
)
}
pub fn integer_bounds(sqlx_type: &str) -> Option<(i128, i128)> {
match sqlx_type {
"i16" => Some((i128::from(i16::MIN), i128::from(i16::MAX))),
"i32" => Some((i128::from(i32::MIN), i128::from(i32::MAX))),
"i64" => Some((i128::from(i64::MIN), i128::from(i64::MAX))),
_ => None,
}
}
pub fn float_mantissa_digits(sqlx_type: &str) -> Option<u32> {
match sqlx_type {
"f32" => Some(f32::MANTISSA_DIGITS),
"f64" => Some(f64::MANTISSA_DIGITS),
_ => None,
}
}
}
fn is_enum_path(rust_type: &str) -> bool {
rust_type
.strip_prefix(TYPES_MODULE)
.and_then(|rest| rest.strip_prefix("::"))
.is_some_and(|name| !name.is_empty() && !name.contains([':', '<', '>']))
}
pub fn lacks_serde(rust_type: &str) -> bool {
[INTERVAL, TIME_TZ, MONEY, RANGE]
.iter()
.any(|sqlx_type| rust_type.contains(sqlx_type))
}
pub fn mentions_generated_type(rust_type: &str) -> bool {
let needle = format!("{TYPES_MODULE}::");
rust_type.match_indices(&needle).any(|(index, _)| {
!rust_type[..index]
.ends_with(|character: char| character.is_alphanumeric() || "_:".contains(character))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shapes_and_base_types() {
assert_eq!(Shape::of("Option<Vec<i32>>"), Shape::Optional("Vec<i32>"));
assert_eq!(Shape::of("Vec<types::Mood>"), Shape::List("types::Mood"));
assert_eq!(Shape::of("Optional<i32>"), Shape::Value("Optional<i32>"));
assert_eq!(BaseType::of("types::Mood"), BaseType::Enum);
assert_eq!(BaseType::of("Vec<types::Mood>"), BaseType::Array);
assert_eq!(BaseType::of("serde_json::Value"), BaseType::Other);
assert_eq!(
BaseType::of("chrono::DateTime<chrono::Utc>"),
BaseType::TimestampTz
);
assert_eq!(
BaseType::of("sqlx::postgres::types::PgRange<chrono::DateTime<chrono::Utc>>"),
BaseType::Range
);
assert_eq!(BaseType::of("ipnetwork::IpNetwork"), BaseType::IpNetwork);
assert!(lacks_serde("Option<sqlx::postgres::types::PgRange<i32>>"));
assert!(!lacks_serde("ipnetwork::IpNetwork"));
}
}