use syn::visit::Visit;
use syn::visit_mut::VisitMut;
use syn::{Attribute, Field, Ident, Lifetime, Type, parse_quote};
#[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"));
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) enum CtxTarget {
Field,
Borrow(Type),
From(Type),
}
pub(crate) struct CtxField<'a> {
field: &'a Field,
pub(crate) target: CtxTarget,
}
impl<'a> CtxField<'a> {
pub(crate) fn parse(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 target: Option<CtxTarget> = None;
for attr in &context_attrs {
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("borrow") || meta.path.is_ident("from") {
match (target.as_ref(), meta.path.is_ident("borrow")) {
(Some(CtxTarget::Borrow(_)), true) => {
Err(meta
.error("duplicate `#[context(borrow = ...)]` on the same field"))
}
(Some(CtxTarget::From(_)), false) => {
Err(meta.error("duplicate `#[context(from = ...)]` on the same field"))
}
(Some(_), _) => Err(meta.error(
"`#[context(borrow = ...)]` and `#[context(from = ...)]` cannot be \
combined on the same field",
)),
(None, is_borrow) => {
let value_stream = meta.value().map_err(|_| {
meta.error(if is_borrow {
"`#[context(borrow)]` requires an explicit target type, e.g. \
`#[context(borrow = Path)]`"
} else {
"`#[context(from)]` requires an explicit target type, e.g. \
`#[context(from = String)]`"
})
})?;
let ty: Type = value_stream.parse()?;
target = Some(if is_borrow {
CtxTarget::Borrow(ty)
} else {
CtxTarget::From(ty)
});
Ok(())
}
}
} else if meta.path.is_ident("option") {
Err(meta.error(
"`#[context(option)]` is only valid on the `#[source]`/`#[from]` field",
))
} else {
Err(meta.error("unknown `#[context(...)]` helper; expected `borrow` or `from`"))
}
})?;
}
let target = target.unwrap_or(CtxTarget::Field);
Ok(Self { field, 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) source_is_option: bool,
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 source_is_option = false;
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]`",
));
}
let explicit_option = parse_source_option(field)?;
let is_option_ty = is_option_type(&field.ty);
if explicit_option && !is_option_ty {
return Err(syn::Error::new_spanned(
field,
"`#[context(option)]` requires the `#[source]` field's type to be `Option<T>`",
));
}
source_field = Some(field);
source_is_option = explicit_option || is_option_ty;
}
FieldRole::Location => {
if location_field.is_some() {
return Err(syn::Error::new_spanned(
field,
"at most one field may be marked `#[location]`",
));
}
for attr in field
.attrs
.iter()
.filter(|attr| attr.path().is_ident("context"))
{
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("option") {
Err(meta.error(
"`#[context(option)]` is only valid on the `#[source]`/`#[from]` field",
))
} else {
Err(meta.error(
"`#[context(...)]` cannot be applied to a `#[location]` field",
))
}
})?;
}
location_field = Some(field);
}
FieldRole::Context => context_fields.push(CtxField::parse(field)?),
}
}
Ok(Self {
source_field,
source_is_option,
location_field,
context_fields,
})
}
}
fn parse_source_option(field: &Field) -> syn::Result<bool> {
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 option = false;
for attr in &context_attrs {
attr.parse_nested_meta(|meta| {
if !meta.path.is_ident("option") {
return Err(meta.error(
"`#[context(...)]` on a `#[source]`/`#[from]` field only supports the `option` helper",
));
}
if option {
return Err(meta.error("duplicate `#[context(option)]` on the same field"));
}
option = true;
Ok(())
})?;
}
Ok(option)
}
pub(crate) fn is_option_type(ty: &Type) -> bool {
let Type::Path(type_path) = ty else {
return false;
};
type_path
.path
.segments
.last()
.is_some_and(|segment| segment.ident == "Option")
}
pub(crate) fn contains_lifetime(ty: &Type) -> bool {
let mut checker = LifetimeChecker { found: false };
checker.visit_type(ty);
checker.found
}
struct LifetimeChecker {
found: bool,
}
impl Visit<'_> for LifetimeChecker {
fn visit_lifetime(&mut self, _lifetime: &Lifetime) {
self.found = true;
}
}
pub(crate) fn bind_lifetimes_to_ctx(ty: &Type) -> Type {
let mut bound = ty.clone();
BindLifetimesToCtx.visit_type_mut(&mut bound);
bound
}
struct BindLifetimesToCtx;
impl VisitMut for BindLifetimesToCtx {
fn visit_lifetime_mut(&mut self, lifetime: &mut Lifetime) {
*lifetime = parse_quote!('ctx);
}
}