use syn::{Attribute, Field, Ident, Type};
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum FieldRole {
Source,
Location,
Context,
}
impl FieldRole {
pub(crate) fn classify(field: &Field) -> syn::Result<FieldRole> {
let is_source = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("source") || attr.path().is_ident("from"));
let is_location = field
.attrs
.iter()
.any(|attr| attr.path().is_ident("location"));
if (is_source || is_location)
&& field
.attrs
.iter()
.any(|attr| attr.path().is_ident("context"))
{
return Err(syn::Error::new_spanned(
field,
"`#[context(...)]` cannot be combined with a `#[source]`/`#[from]` or \
`#[location]` marker",
));
}
match (is_source, is_location) {
(true, true) => Err(syn::Error::new_spanned(
field,
"a field cannot carry both a `#[source]`/`#[from]` marker and a `#[location]` marker",
)),
(true, false) => Ok(FieldRole::Source),
(false, true) => Ok(FieldRole::Location),
(false, false) => Ok(FieldRole::Context),
}
}
}
pub(crate) struct CtxField<'a> {
field: &'a Field,
pub(crate) borrow_target: Option<Type>,
}
impl<'a> CtxField<'a> {
pub(crate) fn parse_borrow(field: &'a Field) -> syn::Result<Self> {
let context_attrs: Vec<&Attribute> = field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("context"))
.collect();
if let Some(duplicate) = context_attrs.get(1) {
return Err(syn::Error::new_spanned(
duplicate,
"a field may carry at most one `#[context(...)]` attribute",
));
}
let mut borrow_target = None;
for attr in &context_attrs {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("borrow") {
if borrow_target.is_some() {
return Err(
meta.error("duplicate `#[context(borrow = ...)]` on the same field")
);
}
let Ok(value_stream) = meta.value() else {
return Err(meta.error(
"`#[context(borrow)]` requires an explicit target type, e.g. \
`#[context(borrow = Path)]`",
));
};
borrow_target = Some(value_stream.parse::<Type>()?);
} else {
return Err(meta.error("unknown `#[context(...)]` helper; expected `borrow`"));
}
Ok(())
})?;
}
Ok(Self {
field,
borrow_target,
})
}
pub(crate) fn ident(&self) -> Option<&Ident> {
self.field.ident.as_ref()
}
pub(crate) fn ty(&self) -> &Type {
&self.field.ty
}
pub(crate) fn vis(&self) -> &syn::Visibility {
&self.field.vis
}
}
pub(crate) struct ScopeFields<'a> {
pub(crate) source_field: Option<&'a Field>,
pub(crate) location_field: Option<&'a Field>,
pub(crate) context_fields: Vec<CtxField<'a>>,
}
impl<'a> ScopeFields<'a> {
pub(crate) fn classify(fields: impl IntoIterator<Item = &'a Field>) -> syn::Result<Self> {
let mut source_field: Option<&Field> = None;
let mut location_field: Option<&Field> = None;
let mut context_fields: Vec<CtxField> = Vec::new();
for field in fields {
match FieldRole::classify(field)? {
FieldRole::Source => {
if source_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one field may be marked `#[source]`/`#[from]`",
));
}
source_field = Some(field);
}
FieldRole::Location => {
if location_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one field may be marked `#[location]`",
));
}
location_field = Some(field);
}
FieldRole::Context => context_fields.push(CtxField::parse_borrow(field)?),
}
}
Ok(Self {
source_field,
location_field,
context_fields,
})
}
}