use proc_macro2::TokenTree;
use syn::ext::IdentExt;
use syn::parse::ParseStream;
use syn::{Attribute, Ident, LitStr, Token, Type};
use crate::naming::RenameAll;
pub(crate) fn rename_all(attrs: &[Attribute]) -> syn::Result<Option<RenameAll>> {
let mut casing = None;
for attr in attrs.iter().filter(|attr| attr.path().is_ident("sqlx")) {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename_all") {
casing = Some(RenameAll::parse(&meta.value()?.parse()?)?);
} else {
skip_value(meta.input)?;
}
Ok(())
})?;
}
Ok(casing)
}
#[derive(Default)]
pub(crate) struct SqlxField {
pub(crate) rename: Option<LitStr>,
pub(crate) skip: bool,
pub(crate) flatten: bool,
pub(crate) json: bool,
pub(crate) try_from: Option<Box<Type>>,
}
pub(crate) fn column_name(
ident: &Ident,
rename: Option<&LitStr>,
rename_all: Option<RenameAll>,
) -> syn::Result<String> {
if let Some(rename) = rename {
let name = rename.value();
if name.is_empty() {
return Err(syn::Error::new(rename.span(), "the column name is empty"));
}
return Ok(name);
}
let name = ident.unraw().to_string();
Ok(match rename_all {
Some(casing) => casing.apply(&name),
None => name,
})
}
pub(crate) fn sqlx_field(attrs: &[Attribute]) -> syn::Result<SqlxField> {
let mut sqlx = SqlxField::default();
for attr in attrs.iter().filter(|attr| attr.path().is_ident("sqlx")) {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
sqlx.rename = Some(meta.value()?.parse()?);
} else if meta.path.is_ident("skip") {
sqlx.skip = true;
} else if meta.path.is_ident("flatten") {
sqlx.flatten = true;
} else if meta.path.is_ident("json") {
sqlx.json = true;
skip_value(meta.input)?;
} else if meta.path.is_ident("try_from") {
let decoded: LitStr = meta.value()?.parse()?;
sqlx.try_from = Some(Box::new(decoded.parse()?));
} else {
skip_value(meta.input)?;
}
Ok(())
})?;
}
Ok(sqlx)
}
fn skip_value(input: ParseStream<'_>) -> syn::Result<()> {
while !input.is_empty() && !input.peek(Token![,]) {
input.parse::<TokenTree>()?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use quote::ToTokens;
use syn::{DeriveInput, parse_quote};
use crate::parse::{Inbox, Storage, inbox};
fn columns(inbox: &Inbox<'_>) -> Vec<Option<String>> {
inbox
.fields
.iter()
.map(|field| field.column().map(|column| column.name.clone()))
.collect()
}
#[test]
fn column_names_follow_sqlx() -> syn::Result<()> {
let input: DeriveInput = parse_quote! {
#[inbox(table = "email_jobs")]
#[sqlx(rename_all = "camelCase")]
struct SendEmail {
#[field(id)]
job_id: i64,
#[sqlx(rename = "queue_name")]
#[field(group)]
group_name: String,
r#type: String,
#[sqlx(skip)]
cache: Vec<u8>,
#[sqlx(flatten)]
extra: Extra,
#[sqlx(json(nullable), try_from = "i64", default)]
attachments: Vec<String>,
}
};
assert_eq!(
columns(&inbox(&input)?),
[
Some("jobId".to_owned()),
Some("queue_name".to_owned()),
Some("type".to_owned()),
None,
None,
Some("attachments".to_owned()),
]
);
let inbox = inbox(&input)?;
assert!(matches!(inbox.fields[3].storage, Storage::Skipped));
assert!(matches!(inbox.fields[4].storage, Storage::Flattened));
assert!(inbox.flattens());
let decoded = inbox.fields[5]
.column()
.and_then(|column| column.try_from.as_ref())
.map(|ty| ty.to_token_stream().to_string());
assert_eq!(decoded.as_deref(), Some("i64"));
Ok(())
}
#[test]
fn a_json_field_is_marked_for_the_insert() -> syn::Result<()> {
let input: DeriveInput = parse_quote! {
#[inbox(table = "jobs")]
struct Job { #[field(id)] id: i64, #[sqlx(json(nullable))] body: Option<Body>, other: String }
};
let json: Vec<_> = inbox(&input)?
.columns()
.map(|(_, column)| column.json)
.collect();
assert_eq!(json, [false, true, false]);
Ok(())
}
}