use anyhow::{anyhow, bail, Result};
use quote::ToTokens;
use syn::{GenericArgument, PathArguments, Type, TypePath};
use super::ir::ColumnType;
pub struct TypeInfo {
pub column_type: ColumnType,
pub nullable: bool,
}
pub fn map_field_type(ty: &Type) -> Result<TypeInfo> {
if let Some(inner) = peel_generic(ty, "Option") {
let column_type = map_inner(&inner)?;
return Ok(TypeInfo {
column_type,
nullable: true,
});
}
Ok(TypeInfo {
column_type: map_inner(ty)?,
nullable: false,
})
}
fn map_inner(ty: &Type) -> Result<ColumnType> {
if let Some(inner) = peel_generic(ty, "Vec") {
if type_is_named(&inner, "u8") {
return Ok(ColumnType::Blob);
}
return Err(unwrapped_collection_error(ty, "Vec"));
}
if let Some(inner) = peel_generic(ty, "Json") {
return Ok(ColumnType::Json(type_to_string(&inner)));
}
if let Some(inner) = peel_generic(ty, "Jsonb") {
return Ok(ColumnType::Jsonb(type_to_string(&inner)));
}
if let Type::Path(TypePath { path, .. }) = ty {
if let Some(seg) = path.segments.last() {
let name = seg.ident.to_string();
if let Some(t) = primitive_for(&name) {
return Ok(t);
}
if matches!(
name.as_str(),
"HashMap" | "BTreeMap" | "HashSet" | "BTreeSet"
) {
return Err(unwrapped_collection_error(ty, &name));
}
}
}
Err(anyhow!(
"unsupported field type `{}`. Wrap in `reef::Json<>` (TEXT) or \
`reef::Jsonb<>` (BLOB) to store as JSON, or use a recognized \
primitive (i64, i32, u64, u32, f64, f32, bool, String, Vec<u8>).",
type_to_string(ty)
))
}
fn primitive_for(name: &str) -> Option<ColumnType> {
Some(match name {
"i64" | "i32" | "i16" | "i8" | "u64" | "u32" | "u16" | "u8" | "isize" | "usize"
| "bool" => ColumnType::Integer,
"f64" | "f32" => ColumnType::Real,
"String" | "str" => ColumnType::Text,
_ => return None,
})
}
fn peel_generic(ty: &Type, wrapper_name: &str) -> Option<Type> {
let Type::Path(TypePath { path, .. }) = ty else {
return None;
};
let seg = path.segments.last()?;
if seg.ident != wrapper_name {
return None;
}
let PathArguments::AngleBracketed(args) = &seg.arguments else {
return None;
};
args.args.iter().find_map(|a| match a {
GenericArgument::Type(t) => Some(t.clone()),
_ => None,
})
}
fn type_is_named(ty: &Type, name: &str) -> bool {
matches!(
ty,
Type::Path(TypePath { path, .. })
if path.segments.last().is_some_and(|s| s.ident == name)
)
}
fn type_to_string(ty: &Type) -> String {
ty.to_token_stream()
.to_string()
.replace(" < ", "<")
.replace(" >", ">")
.replace(" , ", ", ")
}
fn unwrapped_collection_error(ty: &Type, kind: &str) -> anyhow::Error {
anyhow!(
"field type `{}` — `{kind}<T>` cannot be stored directly. Wrap in \
`reef::Json<>` (TEXT) or `reef::Jsonb<>` (BLOB), e.g. `Json<{}>`.",
type_to_string(ty),
type_to_string(ty)
)
}
#[allow(dead_code)]
fn _ensure_imports() {
let _: fn(_) -> Result<()> = |s: &str| bail!("{s}");
}