use proc_macro2::Span;
use syn::spanned::Spanned as _;
use syn::{Attribute, Error, Lit, LitStr, Meta, Result, Token};
const KEELSON: &str = "keelson";
#[derive(Debug, Default)]
pub(crate) struct FieldOptions {
pub(crate) rename: Option<(String, Span)>,
pub(crate) flatten: Option<Span>,
}
pub(crate) fn field_options(attrs: &[Attribute]) -> Result<FieldOptions> {
let mut opts = FieldOptions::default();
let mut errors: Option<Error> = None;
for attr in attrs.iter().filter(|a| a.path().is_ident(KEELSON)) {
if let Err(e) = one_attr(attr, &mut opts) {
combine(&mut errors, e);
}
}
match errors {
Some(e) => Err(e),
None => Ok(opts),
}
}
fn one_attr(attr: &Attribute, opts: &mut FieldOptions) -> Result<()> {
require_list(attr)?;
attr.parse_nested_meta(|meta| {
let span = meta.path.span();
if meta.path.is_ident("rename") {
if !meta.input.peek(Token![=]) {
return Err(Error::new(
span,
"`rename` needs the column name to read: `#[keelson(rename = \"user_id\")]`",
));
}
let name = match meta.value()?.parse::<Lit>()? {
Lit::Str(s) => s,
other => {
return Err(Error::new(
other.span(),
"`rename` takes a string literal — the column name as the database \
spells it, e.g. `#[keelson(rename = \"user_id\")]`",
));
}
};
check_rename(&name)?;
if let Some((_, first)) = opts.rename.replace((name.value(), span)) {
let mut e = Error::new(span, "`rename` is given twice on this field; keep one");
e.combine(Error::new(first, "the first `rename` is here"));
return Err(e);
}
} else if meta.path.is_ident("flatten") {
if meta.input.peek(Token![=]) {
return Err(Error::new(
span,
"`flatten` takes no value — it reads the field's own type out of the same \
row. Write `#[keelson(flatten)]`",
));
}
opts.flatten = Some(span);
} else if meta.path.is_ident("prefix") {
return Err(Error::new(span, PREFIX));
} else {
let key = quote_path(&meta.path);
return Err(Error::new(
span,
format!(
"unknown keelson option `{key}`. `#[derive(FromRow)]` understands \
`rename = \"column\"` and `flatten` on a field, and nothing on the struct \
itself"
),
));
}
Ok(())
})
}
const PREFIX: &str = "`prefix` is not supported. Stripping a prefix means rebuilding the row \
under different column names, and the failure a user then sees names the stripped column \
(\"no column \\\"id\\\"\") rather than the real one (\"author_id\") — a worse error than \
the one it saves. Use `#[keelson(flatten)]` with a nested struct whose fields carry \
`#[keelson(rename = \"author_id\")]`, which reads the same row and reports real column \
names";
pub(crate) fn reject_options(attrs: &[Attribute], what: &str) -> Result<()> {
let mut errors: Option<Error> = None;
for attr in attrs.iter().filter(|a| a.path().is_ident(KEELSON)) {
let e = match require_list(attr) {
Err(e) => e,
Ok(()) => {
let mut found: Option<Error> = None;
let _ = attr.parse_nested_meta(|meta| {
let key = quote_path(&meta.path);
combine(
&mut found,
Error::new(
meta.path.span(),
format!(
"`#[derive(Bind)]` takes no options, so `{key}` on {what} does \
nothing. A newtype is a single column: `rename` and `flatten` \
are `#[derive(FromRow)]` options and belong on a struct that \
maps a whole row"
),
),
);
if meta.input.peek(Token![=]) {
let _ = meta.value().and_then(|v| v.parse::<Lit>());
}
Ok(())
});
match found {
Some(e) => e,
None => Error::new_spanned(attr, "`#[derive(Bind)]` takes no options"),
}
}
};
combine(&mut errors, e);
}
match errors {
Some(e) => Err(e),
None => Ok(()),
}
}
pub(crate) fn reject_lifetimes(
generics: &syn::Generics,
message: impl Fn(&syn::LifetimeParam) -> String,
) -> Result<()> {
let mut errors: Option<Error> = None;
for lt in generics.lifetimes() {
combine(&mut errors, Error::new(lt.lifetime.span(), message(lt)));
}
match errors {
Some(e) => Err(e),
None => Ok(()),
}
}
fn require_list(attr: &Attribute) -> Result<()> {
match attr.meta {
Meta::List(_) => Ok(()),
_ => Err(Error::new_spanned(
attr,
"expected a list of options: `#[keelson(...)]`, e.g. \
`#[keelson(rename = \"user_id\")]`",
)),
}
}
fn check_rename(name: &LitStr) -> Result<()> {
if name.value().is_empty() {
return Err(Error::new(
name.span(),
"`rename` cannot be empty — no result set has a column with no name. Drop the \
attribute to read the column named after the field",
));
}
Ok(())
}
fn quote_path(path: &syn::Path) -> String {
path.segments
.iter()
.map(|s| s.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
pub(crate) fn combine(slot: &mut Option<Error>, e: Error) {
match slot {
Some(existing) => existing.combine(e),
None => *slot = Some(e),
}
}