use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, format_ident, quote};
use syn::{Fields, Path, Token, parse::Parse, spanned::Spanned};
const ONE_MODE: &str = "choose one of `strict`, `unhandled = fatal`, `fatal(..)`";
pub(crate) enum Partial {
No,
CatchAll,
Cases { span: Span, cases: Vec<Case> },
}
impl Partial {
pub(crate) fn is_partial(&self) -> bool {
!matches!(self, Self::No)
}
fn parse_cases(keyword: syn::Ident, input: syn::parse::ParseStream) -> syn::Result<Self> {
let content;
let paren = syn::parenthesized!(content in input);
let cases = content.parse_terminated(Case::parse, Token![,])?;
if cases.is_empty() {
return Err(syn::Error::new(
paren.span.join(),
"`fatal(..)` names at least one case; an empty list is `strict`",
));
}
for case in &cases {
let top = &case.path;
if top.leading_colon.is_some()
|| top.segments.len() != 1
|| !top.segments[0].arguments.is_none()
{
return Err(syn::Error::new_spanned(
top,
"a `fatal(..)` case names a variant of the source; the source path is not repeated",
));
}
}
Ok(Self::Cases {
span: keyword.span(),
cases: cases.into_iter().collect(),
})
}
fn parse_unhandled(input: syn::parse::ParseStream) -> syn::Result<Self> {
input.parse::<Token![=]>()?;
let fatal: syn::Ident = input.parse()?;
if fatal != "fatal" {
return Err(syn::Error::new_spanned(fatal, "expected fatal"));
}
Ok(Self::CatchAll)
}
}
pub(crate) struct Registration {
pub(crate) source: Path,
pub(crate) partial: Partial,
}
pub(crate) struct Via {
pub(crate) via: Path,
pub(crate) id: u64,
}
impl Parse for Via {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key: syn::Ident = input.parse()?;
if key != "via" {
return Err(syn::Error::new_spanned(key, "expected `via = Family`"));
}
input.parse::<Token![=]>()?;
let via = input.parse()?;
input.parse::<Token![,]>()?;
let key: syn::Ident = input.parse()?;
if key != "id" {
return Err(syn::Error::new_spanned(key, "expected `id = N`"));
}
input.parse::<Token![=]>()?;
let id = input.parse::<syn::LitInt>()?.base10_parse()?;
Ok(Self { via, id })
}
}
impl ToTokens for Via {
fn to_tokens(&self, tokens: &mut TokenStream) {
let (via, id) = (&self.via, self.id);
tokens.extend(quote!(via = #via, id = #id));
}
}
pub(crate) fn is_via(attr: &syn::Attribute) -> syn::Result<bool> {
attr.parse_args_with(|input: syn::parse::ParseStream| {
let via = input.peek(syn::Ident) && input.peek2(Token![=]);
input.parse::<TokenStream>()?;
Ok(via)
})
}
impl Parse for Registration {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let source = input.parse()?;
let mut partial = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
let mode: syn::Ident = input.parse()?;
if partial.is_some() {
return Err(syn::Error::new_spanned(mode, ONE_MODE));
}
partial = Some(if mode == "unhandled" {
Partial::parse_unhandled(input)?
} else if mode == "fatal" {
Partial::parse_cases(mode, input)?
} else if mode == "strict" {
Partial::No
} else {
return Err(syn::Error::new_spanned(
mode,
"expected strict, unhandled = fatal, or fatal(..)",
));
});
}
Ok(Self {
source,
partial: partial.unwrap_or(Partial::No),
})
}
}
#[derive(Clone)]
pub(crate) struct Case {
path: Path,
inner: Option<Box<Case>>,
}
impl Case {
pub(crate) fn path(&self) -> &Path {
&self.path
}
pub(crate) fn leaf_path(&self) -> &Path {
match &self.inner {
Some(inner) => inner.leaf_path(),
None => &self.path,
}
}
fn is_nested(&self) -> bool {
self.inner.is_some()
}
fn key(&self) -> String {
let path = self.path.to_token_stream().to_string();
match &self.inner {
Some(inner) => format!("{path}({})", inner.key()),
None => path,
}
}
fn ancestor_keys(&self) -> Vec<String> {
let mut paths = Vec::new();
let mut level = self;
loop {
paths.push(level.path.to_token_stream().to_string());
match &level.inner {
Some(inner) => level = inner,
None => break,
}
}
(1..paths.len())
.map(|depth| {
paths[..depth - 1]
.iter()
.rev()
.fold(paths[depth - 1].clone(), |key, path| {
format!("{path}({key})")
})
})
.collect()
}
fn under(&self, source: &Path) -> Case {
let mut path = source.clone();
path.segments.last_mut().expect("source path").arguments = syn::PathArguments::None;
path.segments.push(self.path.segments[0].clone());
Case {
path,
inner: self.inner.clone(),
}
}
fn nest(&self, leaf: &dyn Fn(&Path) -> TokenStream) -> TokenStream {
match &self.inner {
Some(inner) => {
let path = &self.path;
let inner = inner.nest(leaf);
quote! { #path(#inner) }
}
None => leaf(&self.path),
}
}
}
impl Parse for Case {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let path = input.parse()?;
let inner = if input.peek(syn::token::Paren) {
let content;
let paren = syn::parenthesized!(content in input);
let inner: Case = content.parse()?;
if !content.is_empty() {
return Err(syn::Error::new(
paren.span.join(),
"a nested lift case takes exactly one inner case",
));
}
Some(Box::new(inner))
} else {
None
};
Ok(Self { path, inner })
}
}
pub(crate) struct Mapping {
pub case: Case,
pub with: Option<Path>,
pub field: Option<syn::Ident>,
pub into: bool,
pub when: Option<Path>,
pub to: Option<syn::Ident>,
}
const ARM_KEYS: [&str; 5] = ["with", "field", "into", "when", "to"];
pub(crate) fn is_arm(attr: &syn::Attribute) -> syn::Result<bool> {
attr.parse_args_with(|input: syn::parse::ParseStream| {
let fork = input.fork();
let arm = fork.parse::<Case>().is_ok()
&& fork.parse::<Token![,]>().is_ok()
&& fork
.parse::<syn::Ident>()
.is_ok_and(|key| ARM_KEYS.iter().any(|arm| key == arm));
input.parse::<TokenStream>()?;
Ok(arm)
})
}
pub(crate) fn registration(attr: &syn::Attribute) -> syn::Result<Option<Registration>> {
if is_arm(attr)? || is_via(attr)? {
return Ok(None);
}
attr.parse_args().map(Some)
}
impl Parse for Mapping {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let case = input.parse()?;
let mut mapping = Self {
case,
with: None,
field: None,
into: false,
when: None,
to: None,
};
let mut shape: Option<String> = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
let key: syn::Ident = input.parse()?;
let name = key.to_string();
if name == "to" {
if mapping.to.is_some() {
return Err(syn::Error::new_spanned(key, "duplicate `to`"));
}
input.parse::<Token![=]>()?;
mapping.to = Some(input.parse()?);
continue;
}
if name == "when" {
if mapping.when.is_some() {
return Err(syn::Error::new_spanned(key, "duplicate `when`"));
}
input.parse::<Token![=]>()?;
mapping.when = Some(input.parse()?);
continue;
}
if !matches!(name.as_str(), "with" | "field" | "into") {
return Err(syn::Error::new_spanned(
key,
"expected `with = mapper`, `field = name`, `into`, \
`when = predicate`, or `to = Variant`",
));
}
if let Some(previous) = &shape {
return Err(syn::Error::new_spanned(
key,
if *previous == name {
format!("duplicate `{name}`")
} else {
format!("`{name}` cannot be combined with `{previous}`")
},
));
}
match name.as_str() {
"into" => mapping.into = true,
_ => {
input.parse::<Token![=]>()?;
match name.as_str() {
"with" => mapping.with = Some(input.parse()?),
_ => mapping.field = Some(input.parse()?),
}
}
}
shape = Some(name);
}
Ok(mapping)
}
}
struct Arm<'a> {
owner: Path,
whole_value: bool,
dest: Option<&'a syn::Variant>,
mapping: Mapping,
attr: &'a syn::Attribute,
}
fn admit(
registrations: &[Registration],
unguarded: &mut Vec<String>,
attr: &syn::Attribute,
mapping: &Mapping,
enum_level: bool,
) -> syn::Result<(Path, bool)> {
let key = mapping.case.key();
let must_follow = |unguarded_key: &str| {
syn::Error::new_spanned(
attr,
format!(
"unguarded arm for `{}` must come after its guarded arms",
unguarded_key.replace(' ', "")
),
)
};
if unguarded.contains(&key) {
return Err(if mapping.when.is_some() {
must_follow(&key)
} else {
syn::Error::new_spanned(attr, "source variant mapped more than once")
});
}
if let Some(ancestor) = mapping
.case
.ancestor_keys()
.into_iter()
.find(|ancestor| unguarded.contains(ancestor))
{
return Err(must_follow(&ancestor));
}
if mapping.when.is_none() {
unguarded.push(key);
}
let case_key = mapping.case.path().to_token_stream().to_string();
let whole_value = !enum_level
&& registrations
.iter()
.any(|r| r.source.to_token_stream().to_string() == case_key);
if whole_value && mapping.case.is_nested() {
return Err(syn::Error::new_spanned(
attr,
"a whole-value lift cannot be nested",
));
}
let owner = if whole_value {
mapping.case.path().clone()
} else {
let mut owner = mapping.case.path().clone();
owner.segments.pop();
owner.segments.pop_punct();
owner
};
if !registrations
.iter()
.any(|r| r.source.to_token_stream().to_string() == owner.to_token_stream().to_string())
{
return Err(syn::Error::new_spanned(
attr,
"source family requires enum-level #[lift(Source)] registration",
));
}
Ok((owner, whole_value))
}
pub fn derive(input: &syn::DeriveInput) -> syn::Result<TokenStream> {
if let syn::Data::Struct(data) = &input.data {
return derive_struct(input, data);
}
let syn::Data::Enum(data) = &input.data else {
return Err(syn::Error::new_spanned(
input,
"Lift can only be derived for enums or structs",
));
};
let mut registrations = Vec::new();
let mut enum_arms = Vec::new();
let mut vias = Vec::new();
for attr in &input.attrs {
if !attr.path().is_ident("lift") {
continue;
}
if is_via(attr)? {
vias.push(attr.parse_args::<Via>()?);
continue;
}
match registration(attr)? {
Some(registration) => registrations.push(registration),
None => enum_arms.push(attr),
}
}
for registration in ®istrations {
let mut owner = registration.source.clone();
owner.segments.pop();
owner.segments.pop_punct();
let owner = owner.to_token_stream().to_string();
if registrations
.iter()
.any(|other| other.source.to_token_stream().to_string() == owner)
{
return Err(syn::Error::new_spanned(
®istration.source,
"an enum-level arm needs `to = Variant`, `into` (`Self: From<Payload>`) or \
`with` (`fn(Source) -> Self`)",
));
}
}
let mut arms: Vec<Arm> = Vec::new();
let mut unguarded: Vec<String> = Vec::new();
for attr in enum_arms {
let mapping = attr.parse_args::<Mapping>()?;
let dest = match &mapping.to {
Some(to) => Some(
data.variants
.iter()
.find(|variant| variant.ident == *to)
.ok_or_else(|| {
syn::Error::new_spanned(to, format!("no variant `{to}` to lift into"))
})?,
),
None => {
if mapping.field.is_some() {
return Err(syn::Error::new_spanned(
attr,
"an enum-level arm without `to` forwards the whole source case; \
use `into` or `with`",
));
}
if !mapping.into && mapping.with.is_none() {
return Err(syn::Error::new_spanned(
attr,
"an enum-level arm needs `to = Variant`, `into` (`Self: From<Payload>`) \
or `with` (`fn(Source) -> Self`)",
));
}
None
}
};
let (owner, whole_value) = admit(®istrations, &mut unguarded, attr, &mapping, true)?;
arms.push(Arm {
owner,
whole_value,
dest,
mapping,
attr,
});
}
for variant in &data.variants {
for field in &variant.fields {
if let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("lift")) {
return Err(syn::Error::new_spanned(
attr,
"field lift mappings are only supported on struct destinations",
));
}
}
for attr in &variant.attrs {
if !attr.path().is_ident("lift") {
continue;
}
let mapping = attr.parse_args::<Mapping>()?;
if let Some(to) = &mapping.to {
return Err(syn::Error::new_spanned(
to,
"`to` belongs on an enum-level arm; a variant arm already names its destination",
));
}
let (owner, whole_value) =
admit(®istrations, &mut unguarded, attr, &mapping, false)?;
arms.push(Arm {
owner,
whole_value,
dest: Some(variant),
mapping,
attr,
});
}
}
let mut out = TokenStream::new();
let mut owners = std::collections::HashSet::new();
for registration in registrations {
let source = registration.source;
let key = source.to_token_stream().to_string();
if !owners.insert(key.clone()) {
return Err(syn::Error::new_spanned(source, "duplicate lift source"));
}
let own: Vec<_> = arms
.iter()
.filter(|arm| arm.owner.to_token_stream().to_string() == key)
.collect();
for arm in &own {
let Arm { mapping, attr, .. } = arm;
if mapping.case.is_nested() && !registration.partial.is_partial() {
return Err(syn::Error::new_spanned(
attr,
format!(
"a nested lift case leaves `{}` partially mapped; register \
`#[lift({}, unhandled = fatal)]` or list the remaining cases \
in `fatal(..)`",
mapping
.case
.path()
.to_token_stream()
.to_string()
.replace(' ', ""),
key.replace(' ', ""),
),
));
}
if let Some(when) = &mapping.when {
let case_key = mapping.case.key();
let caught = arms.iter().any(|other| {
other.mapping.when.is_none() && other.mapping.case.key() == case_key
});
if !registration.partial.is_partial() && !caught {
return Err(syn::Error::new_spanned(
when,
format!(
"guards leave `{0}` partially mapped; add an unguarded arm for it, \
register `#[lift({1}, unhandled = fatal)]`, or list the remaining \
cases in `fatal(..)`",
case_key.replace(' ', ""),
key.replace(' ', ""),
),
));
}
}
}
if let Some(whole) = own
.iter()
.find(|arm| arm.whole_value && arm.mapping.when.is_none() && arm.mapping.into)
&& (registration.partial.is_partial()
|| own.iter().any(|arm| {
let guarded_whole = arm.whole_value && arm.mapping.when.is_some();
!(guarded_whole || std::ptr::eq(*arm, *whole))
}))
{
return Err(syn::Error::new_spanned(
whole.attr,
"a whole-value `into` over an enum source is the entire conversion; \
it cannot be combined with other arms, `unhandled = fatal` or `fatal(..)`",
));
}
if let Partial::Cases { cases, .. } = ®istration.partial {
let mapped: Vec<String> = own
.iter()
.filter(|arm| arm.mapping.when.is_none())
.map(|arm| arm.mapping.case.key())
.collect();
check_listed(&source, cases, &mapped)?;
}
let mut tokens = Vec::new();
for arm in own {
let (cfg, arm_tokens) = match arm.dest {
Some(variant) => (
variant
.attrs
.iter()
.filter(|a| a.path().is_ident("cfg"))
.collect(),
variant_arm(&arm.mapping, arm.whole_value, variant)?,
),
None => (Vec::new(), forwarding_arm(&arm.mapping)),
};
tokens.push(quote! { #(#cfg)* #arm_tokens });
}
out.extend(conversion_impl(
input,
&source,
®istration.partial,
&tokens,
));
}
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
for Via { via, id } in vias {
out.extend(quote! {
impl #impl_generics
From<<#via as errlanes::RejectionSource<#name #ty_generics, #id>>::Type>
for #name #ty_generics #where_clause
{
fn from(
source: <#via as errlanes::RejectionSource<#name #ty_generics, #id>>::Type,
) -> Self {
<Self as From<#via>>::from(<#via as From<
<#via as errlanes::RejectionSource<#name #ty_generics, #id>>::Type,
>>::from(source))
}
}
});
}
Ok(out)
}
fn forwarding_arm(mapping: &Mapping) -> TokenStream {
let case = &mapping.case;
let payload_guard = mapping.when.as_ref().map(|pred| quote!(if #pred(&payload)));
let bound = case.nest(&|leaf| quote!(#leaf(payload)));
if let Some(mapper) = &mapping.with {
let whole = case.nest(&|leaf| quote!(#leaf { .. }));
return match &mapping.when {
Some(pred) => quote! { #bound if #pred(&payload) => Ok(#mapper(#bound)) },
None => quote! { value @ #whole => Ok(#mapper(value)) },
};
}
quote! { #bound #payload_guard => Ok(::core::convert::Into::into(payload)) }
}
fn single_field(
variant: &syn::Variant,
value: TokenStream,
what: &str,
) -> syn::Result<TokenStream> {
let dest = &variant.ident;
match &variant.fields {
Fields::Unnamed(fields) if fields.unnamed.len() == 1 => Ok(quote! { Self::#dest(#value) }),
Fields::Named(fields) if fields.named.len() == 1 => {
let field = &fields.named[0].ident;
Ok(quote! { Self::#dest { #field: #value } })
}
_ => Err(syn::Error::new_spanned(
&variant.ident,
format!("{what} needs exactly one destination field"),
)),
}
}
fn variant_arm(
mapping: &Mapping,
whole_value: bool,
variant: &syn::Variant,
) -> syn::Result<TokenStream> {
let case = &mapping.case;
let dest = &variant.ident;
let guard = mapping.when.as_ref().map(|pred| quote!(if #pred(&payload)));
let bound = case.nest(&|leaf| quote!(#leaf(payload)));
if let Some(mapper) = &mapping.with {
let whole = case.nest(&|leaf| quote!(#leaf { .. }));
return Ok(match &mapping.when {
Some(pred) if whole_value => {
quote! { value @ #whole if #pred(&value) => Ok(#mapper(value)) }
}
Some(pred) => {
quote! { #bound if #pred(&payload) => Ok(#mapper(#bound)) }
}
None => quote! { value @ #whole => Ok(#mapper(value)) },
});
}
if mapping.into {
if whole_value {
let guard = mapping.when.as_ref().map(|pred| quote!(if #pred(&value)));
let converted = single_field(
variant,
quote!(::core::convert::Into::into(value)),
"an `into` lift",
)?;
return Ok(quote! { value #guard => Ok(#converted) });
}
let converted = single_field(
variant,
quote!(::core::convert::Into::into(payload)),
"an `into` lift",
)?;
return Ok(quote! { #bound #guard => Ok(#converted) });
}
if let Some(member) = &mapping.field {
if whole_value {
return Err(syn::Error::new_spanned(
case.path(),
"a `field` lift requires a source variant (`Source::Variant`)",
));
}
let projected = single_field(variant, quote!(payload.#member), "a `field` lift")?;
return Ok(quote! { #bound #guard => Ok(#projected) });
}
if whole_value {
let case = case.path();
let guard = mapping.when.as_ref().map(|pred| quote!(if #pred(&value)));
let held = single_field(
variant,
quote!(value),
"a whole-value lift (`#[lift(Source)]` with no variant)",
)
.map_err(|_| {
syn::Error::new_spanned(
&variant.ident,
"a whole-value lift (`#[lift(Source)]` with no variant) needs \
exactly one destination field to hold the source value",
)
})?;
return Ok(quote! { value @ #case { .. } #guard => Ok(#held) });
}
if mapping.when.is_some() {
let kept = single_field(variant, quote!(payload), "a guarded plain arm")?;
return Ok(quote! { #bound #guard => Ok(#kept) });
}
Ok(match &variant.fields {
Fields::Unit => {
let pattern = case.nest(&|leaf| quote!(#leaf));
quote! { #pattern => Ok(Self::#dest) }
}
Fields::Unnamed(fields) => {
let args: Vec<_> = (0..fields.unnamed.len())
.map(|i| format_ident!("field_{i}"))
.collect();
let pattern = case.nest(&|leaf| quote!(#leaf(#(#args),*)));
quote! { #pattern => Ok(Self::#dest(#(#args),*)) }
}
Fields::Named(fields) => {
let args: Vec<_> = fields.named.iter().map(|f| &f.ident).collect();
let pattern = case.nest(&|leaf| quote!(#leaf { #(#args),* }));
quote! { #pattern => Ok(Self::#dest { #(#args),* }) }
}
})
}
fn check_listed(source: &Path, cases: &[Case], mapped: &[String]) -> syn::Result<()> {
let display = |key: &str| key.replace(' ', "");
let mut seen: Vec<String> = Vec::new();
for case in cases {
let qualified = case.under(source);
let key = qualified.key();
let covered_by = |keys: &[String]| {
keys.contains(&key)
|| qualified
.ancestor_keys()
.iter()
.any(|ancestor| keys.contains(ancestor))
};
if covered_by(mapped) {
return Err(syn::Error::new_spanned(
&case.path,
format!("`{}` is mapped and fatal", display(&key)),
));
}
if covered_by(&seen) {
return Err(syn::Error::new_spanned(
&case.path,
format!(
"`{}` is listed in `fatal(..)` more than once",
display(&key)
),
));
}
seen.push(key);
}
Ok(())
}
fn conversion_impl(
input: &syn::DeriveInput,
source: &Path,
partial: &Partial,
arms: &[TokenStream],
) -> TokenStream {
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
if let Partial::CatchAll = partial {
let unhandled = syn::Ident::new("unhandled", source.span());
quote! {
impl #impl_generics errlanes::Lift<#source> for #name #ty_generics #where_clause {
type Unmapped = #source;
fn lift(source: #source) -> Result<Self, Self::Unmapped> {
match source { #(#arms,)* #unhandled => Err(#unhandled) }
}
}
}
} else if let Partial::Cases { span, cases } = partial {
let fatal = cases.iter().map(|case| {
let pattern = case.under(source).nest(&|leaf| quote!(#leaf { .. }));
quote! { unhandled @ #pattern => Err(unhandled) }
});
let scrutinee = syn::Ident::new("source", *span);
quote! {
impl #impl_generics errlanes::Lift<#source> for #name #ty_generics #where_clause {
type Unmapped = #source;
fn lift(source: #source) -> Result<Self, Self::Unmapped> {
match #scrutinee { #(#arms,)* #(#fatal,)* }
}
}
}
} else if arms.is_empty() {
quote! {
impl #impl_generics From<#source> for #name #ty_generics #where_clause {
fn from(source: #source) -> Self {
match source {}
}
}
}
} else {
quote! {
impl #impl_generics From<#source> for #name #ty_generics #where_clause {
fn from(source: #source) -> Self {
let mapped: Result<Self, core::convert::Infallible> =
match source { #(#arms,)* };
match mapped {
Ok(mapped) => mapped, Err(never) => match never {},
}
}
}
}
}
}
struct StructRegistration {
source: Path,
variant: syn::Ident,
partial: Partial,
}
impl Parse for StructRegistration {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let source: Path = input.parse()?;
let mut variant = None;
let mut partial = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let key: syn::Ident = input.parse()?;
match key.to_string().as_str() {
"variant" => {
if variant.is_some() {
return Err(syn::Error::new_spanned(key, "duplicate `variant`"));
}
input.parse::<Token![=]>()?;
variant = Some(input.parse()?);
}
"strict" | "unhandled" | "fatal" => {
if partial.is_some() {
return Err(syn::Error::new_spanned(key, ONE_MODE));
}
partial = Some(if key == "unhandled" {
Partial::parse_unhandled(input)?
} else if key == "fatal" {
Partial::parse_cases(key, input)?
} else {
Partial::No
});
}
_ => {
return Err(syn::Error::new_spanned(
key,
"expected `variant = Case`, `strict`, `unhandled = fatal`, or `fatal(..)`",
));
}
}
}
let variant = variant.ok_or_else(|| {
syn::Error::new_spanned(&source, "a struct lift requires `variant = Case`")
})?;
Ok(Self {
source,
variant,
partial: partial.unwrap_or(Partial::No),
})
}
}
struct SourceField(syn::Ident);
impl Parse for SourceField {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let key: syn::Ident = input.parse()?;
if key != "from" {
return Err(syn::Error::new_spanned(
key,
"expected `from = source_field`",
));
}
input.parse::<Token![=]>()?;
let field = input.parse()?;
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
Ok(Self(field))
}
}
fn derive_struct(input: &syn::DeriveInput, data: &syn::DataStruct) -> syn::Result<TokenStream> {
if let Fields::Unnamed(fields) = &data.fields {
return derive_newtype(input, fields);
}
if matches!(data.fields, Fields::Unit) {
return derive_unit(input);
}
let Fields::Named(fields) = &data.fields else {
return Err(syn::Error::new_spanned(
input,
"a struct lift requires named fields or a single-field tuple struct",
));
};
let mut attrs = input.attrs.iter().filter(|a| a.path().is_ident("lift"));
let attr = attrs.next().ok_or_else(|| {
syn::Error::new_spanned(
&input.ident,
"a struct lift requires #[lift(Source, variant = Case)]",
)
})?;
if let Some(extra) = attrs.next() {
return Err(syn::Error::new_spanned(
extra,
"a struct lift supports exactly one source registration",
));
}
let StructRegistration {
source,
variant,
partial,
} = attr.parse_args()?;
let mut case = source.clone();
case.segments.last_mut().expect("source path").arguments = syn::PathArguments::None;
case.segments.push(variant.into());
if let Partial::Cases { cases, .. } = &partial {
check_listed(&source, cases, &[case.to_token_stream().to_string()])?;
}
let mut bindings = Vec::new();
let mut assignments = Vec::new();
let mut sources = std::collections::HashSet::new();
for (i, field) in fields.named.iter().enumerate() {
let dest = field.ident.as_ref().expect("named field");
let mut attrs = field.attrs.iter().filter(|a| a.path().is_ident("lift"));
let from = match attrs.next() {
Some(attr) => attr.parse_args::<SourceField>()?.0,
None => dest.clone(),
};
if let Some(extra) = attrs.next() {
return Err(syn::Error::new_spanned(
extra,
"duplicate field lift mapping",
));
}
if !sources.insert(from.to_string().trim_start_matches("r#").to_owned()) {
return Err(syn::Error::new_spanned(
from,
"source field mapped more than once",
));
}
let binding = format_ident!("__lift_field_{i}");
bindings.push(quote! { #from: #binding });
assignments.push(quote! { #dest: ::core::convert::Into::into(#binding) });
}
let arm = quote! { #case { #(#bindings),* } => Ok(Self { #(#assignments),* }) };
Ok(conversion_impl(input, &source, &partial, &[arm]))
}
struct UnitRegistration {
source: Path,
}
impl Parse for UnitRegistration {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let source: Path = input.parse()?;
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if let Some(key) = input.parse::<Option<syn::Ident>>()? {
return Err(syn::Error::new_spanned(
&key,
if key == "variant" {
"a unit struct lifts a whole value; a case of an enum source is routed by the family's arm, `with` if it has no payload"
} else {
"a unit struct lift takes only the source path"
},
));
}
}
Ok(Self { source })
}
}
fn derive_unit(input: &syn::DeriveInput) -> syn::Result<TokenStream> {
let mut attrs = input.attrs.iter().filter(|a| a.path().is_ident("lift"));
let attr = attrs.next().ok_or_else(|| {
syn::Error::new_spanned(&input.ident, "a unit struct lift requires #[lift(Source)]")
})?;
if let Some(extra) = attrs.next() {
return Err(syn::Error::new_spanned(
extra,
"a struct lift supports exactly one source registration",
));
}
let UnitRegistration { source } = attr.parse_args()?;
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! {
impl #impl_generics From<#source> for #name #ty_generics #where_clause {
fn from(_: #source) -> Self {
Self
}
}
})
}
struct NewtypeRegistration {
source: Path,
field: Option<syn::Member>,
}
impl Parse for NewtypeRegistration {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let source: Path = input.parse()?;
let mut field = None;
while input.peek(Token![,]) {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let key: syn::Ident = input.parse()?;
if key != "field" {
return Err(syn::Error::new_spanned(
key,
"a tuple struct lift supports only `field = name`",
));
}
if field.is_some() {
return Err(syn::Error::new_spanned(key, "duplicate `field`"));
}
input.parse::<Token![=]>()?;
field = Some(input.parse()?);
}
Ok(Self { source, field })
}
}
fn derive_newtype(
input: &syn::DeriveInput,
fields: &syn::FieldsUnnamed,
) -> syn::Result<TokenStream> {
if fields.unnamed.len() != 1 {
return Err(syn::Error::new_spanned(
fields,
"a tuple struct lift requires exactly one field",
));
}
for field in &fields.unnamed {
if let Some(attr) = field.attrs.iter().find(|a| a.path().is_ident("lift")) {
return Err(syn::Error::new_spanned(
attr,
"put the field projection on the struct: #[lift(Source, field = name)]",
));
}
}
let mut attrs = input.attrs.iter().filter(|a| a.path().is_ident("lift"));
let attr = attrs.next().ok_or_else(|| {
syn::Error::new_spanned(
&input.ident,
"a tuple struct lift requires #[lift(Source)] or #[lift(Source, field = name)]",
)
})?;
if let Some(extra) = attrs.next() {
return Err(syn::Error::new_spanned(
extra,
"a struct lift supports exactly one source registration",
));
}
let NewtypeRegistration { source, field } = attr.parse_args()?;
let name = &input.ident;
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
let value = match field {
Some(field) => quote!(::core::convert::Into::into(source.#field)),
None => quote!(source),
};
Ok(quote! {
impl #impl_generics From<#source> for #name #ty_generics #where_clause {
fn from(source: #source) -> Self {
Self(#value)
}
}
})
}