use {
proc_macro::TokenStream,
proc_macro_crate::{FoundCrate, crate_name},
quote::quote,
syn::{
Attribute, Data, DeriveInput, Expr, ExprLit, Fields, Lit, MetaNameValue, Type,
parse_macro_input, spanned::Spanned,
},
};
fn resolve_gluesql_crate() -> Result<syn::Path, syn::Error> {
if std::env::var("CARGO_PKG_NAME")
.map(|name| name == "gluesql")
.unwrap_or(false)
{
return Ok(syn::parse_quote!(::gluesql::core));
}
if let Ok(found) = crate_name("gluesql") {
let path = match found {
FoundCrate::Itself => syn::parse_quote!(crate::core),
FoundCrate::Name(name) => {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
syn::parse_quote!(::#ident::core)
}
};
return Ok(path);
}
let found = crate_name("gluesql_core")
.or_else(|_| crate_name("gluesql-core"))
.map_err(|_| {
syn::Error::new(
proc_macro2::Span::call_site(),
"failed to locate `gluesql` crate; add a dependency on `gluesql` or `gluesql-core`",
)
})?;
let path = match found {
FoundCrate::Itself => syn::parse_quote!(crate),
FoundCrate::Name(name) => {
let ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
syn::parse_quote!(::#ident)
}
};
Ok(path)
}
fn expand_from_glue_row(input: DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
let input_span = input.span();
let gluesql_crate_path = resolve_gluesql_crate()?;
let gluesql_crate = quote! { #gluesql_crate_path };
let ident = input.ident.clone();
let data = match input.data {
Data::Struct(s) => s,
_ => {
return Err(syn::Error::new(
input_span,
"FromGlueRow can only be derived for structs",
));
}
};
let fields = match data.fields {
Fields::Named(f) => f.named,
_ => {
return Err(syn::Error::new(
input_span,
"FromGlueRow supports only named fields",
));
}
};
let mut field_inits = Vec::new();
let mut field_inits_with_idx = Vec::new();
let mut field_idents = Vec::new();
let mut fields_meta_pairs = Vec::new();
for field in fields.iter() {
let field_ident = field.ident.clone().expect("named field");
field_idents.push(field_ident.clone());
let field_name_literal = field_ident.to_string();
let mut rename: Option<String> = None;
for attr in &field.attrs {
if let Some(res) = parse_glue_rename(attr) {
match res {
Ok(Some(name)) => rename = Some(name),
Ok(None) => {}
Err(e) => return Err(e),
}
}
}
let column_name = rename.unwrap_or_else(|| field_name_literal.clone());
fields_meta_pairs.push(quote! { (#field_name_literal, #column_name) });
let (is_option, base_ty) = match get_option_inner_type(&field.ty) {
Some(inner) => (true, inner.clone()),
None => (false, field.ty.clone()),
};
let (_expected_str, value_match_ref, value_match_idx) =
match_expected(&gluesql_crate, &base_ty, &field_name_literal, &column_name);
let init_expr = if is_option {
quote! {
let __idx = __labels.iter().position(|l| l == #column_name)
.ok_or(#gluesql_crate::row_conversion::RowConversionError::MissingColumn { field: #field_name_literal, column: #column_name })?;
let __v = &__row[__idx];
let #field_ident = match __v {
#gluesql_crate::data::Value::Null => None,
__v => Some({ #value_match_ref }),
};
}
} else {
quote! {
let __idx = __labels.iter().position(|l| l == #column_name)
.ok_or(#gluesql_crate::row_conversion::RowConversionError::MissingColumn { field: #field_name_literal, column: #column_name })?;
let __v = &__row[__idx];
if matches!(__v, #gluesql_crate::data::Value::Null) {
return Err(#gluesql_crate::row_conversion::RowConversionError::NullNotAllowed { field: #field_name_literal, column: __labels[__idx].clone() });
}
let #field_ident = { #value_match_ref };
}
};
field_inits.push(init_expr);
let idx_pos = field_inits_with_idx.len();
let init_expr_with_idx = if is_option {
quote! {
let __v = &__row[__idx[#idx_pos]];
let #field_ident = match __v {
#gluesql_crate::data::Value::Null => None,
__v => Some({ #value_match_idx }),
};
}
} else {
quote! {
let __v = &__row[__idx[#idx_pos]];
if matches!(__v, #gluesql_crate::data::Value::Null) {
return Err(#gluesql_crate::row_conversion::RowConversionError::NullNotAllowed { field: #field_name_literal, column: __labels[__idx[#idx_pos]].clone() });
}
let #field_ident = { #value_match_idx };
}
};
field_inits_with_idx.push(init_expr_with_idx);
}
let fields_len = fields_meta_pairs.len();
let expanded = quote! {
impl #gluesql_crate::row_conversion::FromGlueRow for #ident {
fn __glue_fields() -> &'static [(&'static str, &'static str)] {
static FIELDS: [(&str, &str); #fields_len] = [ #(#fields_meta_pairs),* ];
&FIELDS
}
fn from_glue_row(
__labels: &[::std::string::String],
__row: &[#gluesql_crate::data::Value],
) -> ::core::result::Result<Self, #gluesql_crate::row_conversion::RowConversionError> {
#(#field_inits)*
Ok(Self { #(#field_idents),* })
}
fn from_glue_row_with_idx(
__idx: &[usize],
__labels: &[::std::string::String],
__row: &[#gluesql_crate::data::Value],
) -> ::core::result::Result<Self, #gluesql_crate::row_conversion::RowConversionError> {
#(#field_inits_with_idx)*
Ok(Self { #(#field_idents),* })
}
}
};
Ok(expanded)
}
#[proc_macro_derive(FromGlueRow, attributes(glue))]
pub fn derive_from_glue_row(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match expand_from_glue_row(input) {
Ok(ts) => TokenStream::from(ts),
Err(e) => e.to_compile_error().into(),
}
}
fn parse_glue_rename(attr: &Attribute) -> Option<Result<Option<String>, syn::Error>> {
if !attr.path().is_ident("glue") {
return None;
}
match attr.parse_args::<MetaNameValue>() {
Ok(MetaNameValue { path, value, .. }) if path.is_ident("rename") => match value {
Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) => Some(Ok(Some(s.value()))),
other => Some(Err(syn::Error::new(
other.span(),
"expected string literal for rename",
))),
},
Ok(_) => Some(Ok(None)),
Err(e) => Some(Err(e)),
}
}
fn get_option_inner_type(ty: &Type) -> Option<&Type> {
if let Type::Path(tp) = ty
&& tp.path.segments.len() == 1
&& tp.path.segments[0].ident == "Option"
&& let syn::PathArguments::AngleBracketed(args) = &tp.path.segments[0].arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner);
}
None
}
fn is_type(ty: &Type, name: &str) -> bool {
if let Type::Path(tp) = ty
&& tp.path.segments.len() == 1
{
return tp.path.segments[0].ident == name;
}
false
}
fn is_string_type(ty: &Type) -> bool {
if let Type::Path(tp) = ty
&& tp.path.segments.len() == 1
{
return tp.path.segments[0].ident == "String";
}
false
}
fn last_ident_is(ty: &Type, name: &str) -> bool {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
{
return seg.ident == name;
}
false
}
fn is_vec_of_u8(ty: &Type) -> bool {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "Vec"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return is_type(inner, "u8");
}
false
}
fn is_vec_of_value(ty: &Type) -> bool {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "Vec"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return last_ident_is(inner, "Value");
}
false
}
fn get_vec_inner_type(ty: &Type) -> Option<&Type> {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "Vec"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
&& let Some(syn::GenericArgument::Type(inner)) = args.args.first()
{
return Some(inner);
}
None
}
fn is_btreemap_string_value(ty: &Type) -> bool {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "BTreeMap"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
{
let mut it = args.args.iter();
let first = it.next();
let second = it.next();
if let (Some(syn::GenericArgument::Type(t1)), Some(syn::GenericArgument::Type(t2))) =
(first, second)
{
return is_string_type(t1) && last_ident_is(t2, "Value");
}
}
false
}
fn get_btreemap_types(ty: &Type) -> Option<(&Type, &Type)> {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "BTreeMap"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
{
let mut it = args.args.iter();
if let (Some(syn::GenericArgument::Type(k)), Some(syn::GenericArgument::Type(v))) =
(it.next(), it.next())
{
return Some((k, v));
}
}
None
}
fn get_hashmap_types(ty: &Type) -> Option<(&Type, &Type)> {
if let Type::Path(tp) = ty
&& let Some(seg) = tp.path.segments.last()
&& seg.ident == "HashMap"
&& let syn::PathArguments::AngleBracketed(args) = &seg.arguments
{
let mut it = args.args.iter();
if let (Some(syn::GenericArgument::Type(k)), Some(syn::GenericArgument::Type(v))) =
(it.next(), it.next())
{
return Some((k, v));
}
}
None
}
fn match_expected(
gluesql_crate: &proc_macro2::TokenStream,
base_ty: &Type,
field_name_literal: &str,
column_name: &str,
) -> (
&'static str,
proc_macro2::TokenStream,
proc_macro2::TokenStream,
) {
let got_str = quote! { match __v {
#gluesql_crate::data::Value::I8(_) => "I8",
#gluesql_crate::data::Value::I16(_) => "I16",
#gluesql_crate::data::Value::I32(_) => "I32",
#gluesql_crate::data::Value::I64(_) => "I64",
#gluesql_crate::data::Value::I128(_) => "I128",
#gluesql_crate::data::Value::U8(_) => "U8",
#gluesql_crate::data::Value::U16(_) => "U16",
#gluesql_crate::data::Value::U32(_) => "U32",
#gluesql_crate::data::Value::U64(_) => "U64",
#gluesql_crate::data::Value::U128(_) => "U128",
#gluesql_crate::data::Value::F32(_) => "F32",
#gluesql_crate::data::Value::F64(_) => "F64",
#gluesql_crate::data::Value::Decimal(_) => "Decimal",
#gluesql_crate::data::Value::Bool(_) => "Bool",
#gluesql_crate::data::Value::Str(_) => "Str",
#gluesql_crate::data::Value::Bytea(_) => "Bytea",
#gluesql_crate::data::Value::Inet(_) => "Inet",
#gluesql_crate::data::Value::Date(_) => "Date",
#gluesql_crate::data::Value::Timestamp(_) => "Timestamp",
#gluesql_crate::data::Value::Time(_) => "Time",
#gluesql_crate::data::Value::Interval(_) => "Interval",
#gluesql_crate::data::Value::Uuid(_) => "Uuid",
#gluesql_crate::data::Value::Map(_) => "Map",
#gluesql_crate::data::Value::List(_) => "List",
#gluesql_crate::data::Value::Point(_) => "Point",
#gluesql_crate::data::Value::Null => "Null",
}};
macro_rules! arms_copy {
($variant:ident, $expected:expr) => {{
let by_ref = quote! { if let #gluesql_crate::data::Value::$variant(v) = __v { *v } else { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(__labels[__idx].clone()), expected: $expected, got: __got }) } };
let by_idx = quote! { if let #gluesql_crate::data::Value::$variant(v) = __v { *v } else { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(#column_name.to_string()), expected: $expected, got: __got }) } };
($expected, by_ref, by_idx)
}}
}
macro_rules! arms_copy_either {
($v1:ident, $v2:ident, $expected:expr) => {{
let by_ref = quote! {
if let #gluesql_crate::data::Value::$v1(v) = __v { *v }
else if let #gluesql_crate::data::Value::$v2(v) = __v { *v }
else { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(__labels[__idx].clone()), expected: $expected, got: __got }) }
};
let by_idx = quote! {
if let #gluesql_crate::data::Value::$v1(v) = __v { *v }
else if let #gluesql_crate::data::Value::$v2(v) = __v { *v }
else { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(#column_name.to_string()), expected: $expected, got: __got }) }
};
($expected, by_ref, by_idx)
}}
}
macro_rules! arms_clone {
($variant:ident, $expected:expr) => {{
let by_ref = quote! { if let #gluesql_crate::data::Value::$variant(v) = __v { v.clone() } else { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(__labels[__idx].clone()), expected: $expected, got: __got }) } };
let by_idx = quote! { if let #gluesql_crate::data::Value::$variant(v) = __v { v.clone() } else { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(#column_name.to_string()), expected: $expected, got: __got }) } };
($expected, by_ref, by_idx)
}}
}
if is_type(base_ty, "i8") {
return arms_copy!(I8, "i8");
}
if is_type(base_ty, "i16") {
return arms_copy!(I16, "i16");
}
if is_type(base_ty, "i32") {
return arms_copy!(I32, "i32");
}
if is_type(base_ty, "i64") {
return arms_copy!(I64, "i64");
}
if is_type(base_ty, "i128") {
return arms_copy!(I128, "i128");
}
if is_type(base_ty, "u8") {
return arms_copy!(U8, "u8");
}
if is_type(base_ty, "u16") {
return arms_copy!(U16, "u16");
}
if is_type(base_ty, "u32") {
return arms_copy!(U32, "u32");
}
if is_type(base_ty, "u64") {
return arms_copy!(U64, "u64");
}
if is_type(base_ty, "u128") {
return arms_copy_either!(U128, Uuid, "u128");
}
if is_type(base_ty, "f32") {
return arms_copy!(F32, "f32");
}
if is_type(base_ty, "f64") {
return arms_copy!(F64, "f64");
}
if is_type(base_ty, "bool") {
return arms_copy!(Bool, "bool");
}
if is_string_type(base_ty) {
let by_ref = quote! {
match __v {
#gluesql_crate::data::Value::Str(v) => v.clone(),
#gluesql_crate::data::Value::Date(v) => v.to_string(),
#gluesql_crate::data::Value::Time(v) => v.to_string(),
#gluesql_crate::data::Value::Uuid(v) => #gluesql_crate::row_conversion::uuid_to_string(*v),
#gluesql_crate::data::Value::Timestamp(v) => v.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string(),
_ => { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(__labels[__idx].clone()), expected: "String", got: __got }) }
}
};
let by_idx = quote! {
match __v {
#gluesql_crate::data::Value::Str(v) => v.clone(),
#gluesql_crate::data::Value::Date(v) => v.to_string(),
#gluesql_crate::data::Value::Time(v) => v.to_string(),
#gluesql_crate::data::Value::Uuid(v) => #gluesql_crate::row_conversion::uuid_to_string(*v),
#gluesql_crate::data::Value::Timestamp(v) => v.format("%Y-%m-%dT%H:%M:%S%.fZ").to_string(),
_ => { let __got: &str = #got_str; return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch { field: Some(#field_name_literal), column: Some(#column_name.to_string()), expected: "String", got: __got }) }
}
};
return ("String", by_ref, by_idx);
}
if last_ident_is(base_ty, "Decimal") {
return arms_clone!(Decimal, "Decimal");
}
if is_vec_of_u8(base_ty) {
return arms_clone!(Bytea, "Vec<u8>");
}
if last_ident_is(base_ty, "IpAddr") {
return arms_clone!(Inet, "IpAddr");
}
if last_ident_is(base_ty, "NaiveDate") {
return arms_clone!(Date, "NaiveDate");
}
if last_ident_is(base_ty, "NaiveDateTime") {
return arms_clone!(Timestamp, "NaiveDateTime");
}
if last_ident_is(base_ty, "NaiveTime") {
return arms_clone!(Time, "NaiveTime");
}
if last_ident_is(base_ty, "Interval") {
return arms_clone!(Interval, "Interval");
}
if is_btreemap_string_value(base_ty) {
return arms_clone!(Map, "BTreeMap<String, Value>");
}
if let Some((k_ty, v_ty)) = get_btreemap_types(base_ty)
&& is_string_type(k_ty)
&& !last_ident_is(v_ty, "Value")
{
let expected_str = format!("BTreeMap<String, {}>", quote! { #v_ty });
let expected_lit = syn::LitStr::new(&expected_str, base_ty.span());
let (is_opt_val, val_base_ty) = match get_option_inner_type(v_ty) {
Some(inner) => (true, inner),
None => (false, v_ty),
};
let (_inner_exp_s, inner_by_ref, inner_by_idx) =
match_expected(gluesql_crate, val_base_ty, field_name_literal, column_name);
let by_ref = if is_opt_val {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::BTreeMap<::std::string::String, #v_ty> = ::std::collections::BTreeMap::new();
for (__k, __item) in __map.iter() {
let __val: #v_ty = match __item {
#gluesql_crate::data::Value::Null => None,
__item => {
let __v = __item;
Some({ #inner_by_ref })
}
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: #expected_lit,
got: __got,
});
}
}
} else {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::BTreeMap<::std::string::String, #v_ty> = ::std::collections::BTreeMap::new();
for (__k, __item) in __map.iter() {
let __val: #v_ty = {
let __v = __item;
{ #inner_by_ref }
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: #expected_lit,
got: __got,
});
}
}
};
let by_idx = if is_opt_val {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::BTreeMap<::std::string::String, #v_ty> = ::std::collections::BTreeMap::new();
for (__k, __item) in __map.iter() {
let __val: #v_ty = match __item {
#gluesql_crate::data::Value::Null => None,
__item => {
let __v = __item;
Some({ #inner_by_idx })
}
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: #expected_lit,
got: __got,
});
}
}
} else {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::BTreeMap<::std::string::String, #v_ty> = ::std::collections::BTreeMap::new();
for (__k, __item) in __map.iter() {
let __val: #v_ty = {
let __v = __item;
{ #inner_by_idx }
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: #expected_lit,
got: __got,
});
}
}
};
return ("BTreeMap<_, _>", by_ref, by_idx);
}
if let Some((k_ty, v_ty)) = get_hashmap_types(base_ty)
&& is_string_type(k_ty)
{
if last_ident_is(v_ty, "Value") {
let by_ref = quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::HashMap<::std::string::String, #gluesql_crate::data::Value> = ::std::collections::HashMap::with_capacity(__map.len());
for (__k, __item) in __map.iter() {
__out.insert(__k.clone(), __item.clone());
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: "HashMap<String, Value>",
got: __got,
});
}
};
let by_idx = quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::HashMap<::std::string::String, #gluesql_crate::data::Value> = ::std::collections::HashMap::with_capacity(__map.len());
for (__k, __item) in __map.iter() {
__out.insert(__k.clone(), __item.clone());
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: "HashMap<String, Value>",
got: __got,
});
}
};
return ("HashMap<_, _>", by_ref, by_idx);
}
let expected_str = format!("HashMap<String, {}>", quote! { #v_ty });
let expected_lit = syn::LitStr::new(&expected_str, base_ty.span());
let (is_opt_val, val_base_ty) = match get_option_inner_type(v_ty) {
Some(inner) => (true, inner),
None => (false, v_ty),
};
let (_inner_exp_s, inner_by_ref, inner_by_idx) =
match_expected(gluesql_crate, val_base_ty, field_name_literal, column_name);
let by_ref = if is_opt_val {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::HashMap<::std::string::String, #v_ty> = ::std::collections::HashMap::with_capacity(__map.len());
for (__k, __item) in __map.iter() {
let __val: #v_ty = match __item {
#gluesql_crate::data::Value::Null => None,
__item => {
let __v = __item;
Some({ #inner_by_ref })
}
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: #expected_lit,
got: __got,
});
}
}
} else {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::HashMap<::std::string::String, #v_ty> = ::std::collections::HashMap::with_capacity(__map.len());
for (__k, __item) in __map.iter() {
let __val: #v_ty = {
let __v = __item;
{ #inner_by_ref }
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: #expected_lit,
got: __got,
});
}
}
};
let by_idx = if is_opt_val {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::HashMap<::std::string::String, #v_ty> = ::std::collections::HashMap::with_capacity(__map.len());
for (__k, __item) in __map.iter() {
let __val: #v_ty = match __item {
#gluesql_crate::data::Value::Null => None,
__item => {
let __v = __item;
Some({ #inner_by_idx })
}
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: #expected_lit,
got: __got,
});
}
}
} else {
quote! {
if let #gluesql_crate::data::Value::Map(__map) = __v {
let mut __out: ::std::collections::HashMap<::std::string::String, #v_ty> = ::std::collections::HashMap::with_capacity(__map.len());
for (__k, __item) in __map.iter() {
let __val: #v_ty = {
let __v = __item;
{ #inner_by_idx }
};
__out.insert(__k.clone(), __val);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: #expected_lit,
got: __got,
});
}
}
};
return ("HashMap<_, _>", by_ref, by_idx);
}
if is_vec_of_value(base_ty) {
return arms_clone!(List, "Vec<Value>");
}
if let Some(inner) = get_vec_inner_type(base_ty)
&& !is_type(inner, "u8")
&& !last_ident_is(inner, "Value")
{
let expected_str = format!("Vec<{}>", quote! { #inner });
let expected_lit = syn::LitStr::new(&expected_str, base_ty.span());
let (is_opt_elem, elem_base_ty) = match get_option_inner_type(inner) {
Some(inner2) => (true, inner2),
None => (false, inner),
};
let (_inner_exp_s, inner_by_ref, inner_by_idx) =
match_expected(gluesql_crate, elem_base_ty, field_name_literal, column_name);
let by_ref = if is_opt_elem {
quote! {
if let #gluesql_crate::data::Value::List(__list) = __v {
let mut __out: ::std::vec::Vec<#inner> = ::std::vec::Vec::with_capacity(__list.len());
for __item in __list.iter() {
let __elem: #inner = match __item {
#gluesql_crate::data::Value::Null => None,
__item => {
let __v = __item;
Some({ #inner_by_ref })
}
};
__out.push(__elem);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: #expected_lit,
got: __got,
});
}
}
} else {
quote! {
if let #gluesql_crate::data::Value::List(__list) = __v {
let mut __out: ::std::vec::Vec<#inner> = ::std::vec::Vec::with_capacity(__list.len());
for __item in __list.iter() {
let __elem: #inner = {
let __v = __item;
{ #inner_by_ref }
};
__out.push(__elem);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(__labels[__idx].clone()),
expected: #expected_lit,
got: __got,
});
}
}
};
let by_idx = if is_opt_elem {
quote! {
if let #gluesql_crate::data::Value::List(__list) = __v {
let mut __out: ::std::vec::Vec<#inner> = ::std::vec::Vec::with_capacity(__list.len());
for __item in __list.iter() {
let __elem: #inner = match __item {
#gluesql_crate::data::Value::Null => None,
__item => {
let __v = __item;
Some({ #inner_by_idx })
}
};
__out.push(__elem);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: #expected_lit,
got: __got,
});
}
}
} else {
quote! {
if let #gluesql_crate::data::Value::List(__list) = __v {
let mut __out: ::std::vec::Vec<#inner> = ::std::vec::Vec::with_capacity(__list.len());
for __item in __list.iter() {
let __elem: #inner = {
let __v = __item;
{ #inner_by_idx }
};
__out.push(__elem);
}
__out
} else {
let __got: &str = #got_str;
return Err(#gluesql_crate::row_conversion::RowConversionError::TypeMismatch {
field: Some(#field_name_literal),
column: Some(#column_name.to_string()),
expected: #expected_lit,
got: __got,
});
}
}
};
return ("Vec<_>", by_ref, by_idx);
}
if last_ident_is(base_ty, "Point") {
return arms_clone!(Point, "Point");
}
let msg = format!(
"Unsupported field type for FromGlueRow: `{}`. Supported: direct matches of Value variants (integers, floats, bool, String, Decimal, Vec<u8>, IpAddr, chrono NaiveDate/NaiveDateTime/NaiveTime, Interval, u128 for Uuid, BTreeMap<String, Value>, Vec<Value>, Point) and Option<T> of those.",
quote! { #base_ty }
);
(
"<unsupported>",
syn::Error::new(base_ty.span(), &msg).to_compile_error(),
syn::Error::new(base_ty.span(), &msg).to_compile_error(),
)
}
#[cfg(test)]
mod tests {
use super::expand_from_glue_row;
use syn::parse_quote;
#[test]
fn non_struct_input_returns_error() {
let di: syn::DeriveInput = parse_quote! {
enum E { A }
};
let err = expand_from_glue_row(di).unwrap_err();
assert!(
err.to_string()
.contains("FromGlueRow can only be derived for structs")
);
}
#[test]
fn non_named_fields_struct_returns_error() {
let di: syn::DeriveInput = parse_quote! {
struct T(i32);
};
let err = expand_from_glue_row(di).unwrap_err();
assert!(
err.to_string()
.contains("FromGlueRow supports only named fields")
);
}
#[test]
fn glue_rename_ok_some_and_ok_none() {
let di: syn::DeriveInput = parse_quote! {
struct S {
#[glue(rename = "col")] a: i64,
#[glue(other = "x")] b: String,
}
};
let _ = expand_from_glue_row(di).expect("expand ok");
}
#[test]
fn glue_rename_parse_error_missing_args() {
let di: syn::DeriveInput = parse_quote! {
struct S { #[glue] a: i64 }
};
let err = expand_from_glue_row(di).unwrap_err();
let s = err.to_string();
assert!(!s.is_empty(), "unexpected empty error message");
}
#[test]
fn glue_rename_wrong_literal_type() {
let di: syn::DeriveInput = parse_quote! {
struct S { #[glue(rename = 123)] a: i64 }
};
let err = expand_from_glue_row(di).unwrap_err();
assert!(
err.to_string()
.contains("expected string literal for rename")
);
}
#[test]
fn match_expected_all_types_and_options_expand() {
let di: syn::DeriveInput = parse_quote! {
struct All {
i8_: i8,
i16_: i16,
i32_: i32,
i64_: i64,
i128_: i128,
u8_: u8,
u16_: u16,
u32_: u32,
u64_: u64,
u128_: u128,
f32_: f32,
f64_: f64,
b_: bool,
s_: String,
dec_: Decimal,
bytes_: Vec<u8>,
ip_: IpAddr,
date_: NaiveDate,
ts_: NaiveDateTime,
time_: NaiveTime,
interval_: Interval,
map_: BTreeMap<String, Value>,
list_: Vec<Value>,
point_: Point,
opt_s: Option<String>,
opt_i64: Option<i64>,
}
};
let _ = expand_from_glue_row(di).expect("expand ok");
}
#[test]
fn match_expected_maps_and_nested_options_expand() {
let di: syn::DeriveInput = parse_quote! {
struct M {
hm_val: std::collections::HashMap<String, Value>,
hm_opt: std::collections::HashMap<String, Option<i64>>,
bm_opt: std::collections::BTreeMap<String, Option<String>>,
vv: Vec<Vec<i64>>,
}
};
let _ = expand_from_glue_row(di).expect("expand ok");
}
#[test]
fn unsupported_type_path_emits_compile_error_tokens() {
let di: syn::DeriveInput = parse_quote! {
struct S<'a> { v: &'a str }
};
let ts = expand_from_glue_row(di).expect("expand ok");
let s = ts.to_string();
assert!(s.contains("Unsupported field type for FromGlueRow"));
}
#[test]
fn non_glue_attribute_is_ignored() {
let di: syn::DeriveInput = parse_quote! {
struct S {
#[doc = "field doc"]
a: i64,
}
};
let _ = expand_from_glue_row(di).expect("expand ok");
}
#[test]
fn unsupported_btreemap_missing_generic_arg() {
let di: syn::DeriveInput = parse_quote! {
struct S { v: std::collections::BTreeMap<String> }
};
let ts = expand_from_glue_row(di).expect("expand ok");
let s = ts.to_string();
assert!(s.contains("Unsupported field type for FromGlueRow"));
}
#[test]
fn unsupported_hashmap_missing_generic_arg() {
let di: syn::DeriveInput = parse_quote! {
struct S { v: std::collections::HashMap<String> }
};
let ts = expand_from_glue_row(di).expect("expand ok");
let s = ts.to_string();
assert!(s.contains("Unsupported field type for FromGlueRow"));
}
}