use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::parse::{Parse, ParseStream, Parser};
use syn::punctuated::Punctuated;
use syn::{braced, token, Error, Ident, ItemImpl, ItemTrait, Path, PathArguments, Result, Token};
use crate::options::TraitSpec;
use crate::subject::Subject;
const REQUEST_VERSION: &str = "v1";
const CARRIED_TRAIT_VERSION: &str = "v2";
const CARRIED_SUBJECT_VERSION: &str = "v1";
const READABLE_TRAIT_VERSIONS: &[&str] = &["v1", CARRIED_TRAIT_VERSION];
fn version(name: &str) -> Ident {
Ident::new(name, Span::call_site())
}
#[derive(Clone, Copy)]
pub(crate) enum Implementation {
Trait,
Block,
}
impl Implementation {
fn ident(self) -> Ident {
let name = match self {
Implementation::Trait => "implement",
Implementation::Block => "implement_block",
};
Ident::new(name, Span::call_site())
}
}
pub(crate) const CARRIER_IS_OLDER: &str = "this trait's carrier macro was written by an older `whichever` than the one dispatching it; update the trait's crate, or build both crates with the same `whichever` minor";
pub(crate) fn carrier_macro(
name: &Ident,
attributes: TokenStream2,
implementation: Implementation,
carried: impl ToTokens,
) -> TokenStream2 {
let version = version(REQUEST_VERSION);
let implementation = implementation.ident();
let older = CARRIER_IS_OLDER;
quote! {
#attributes
macro_rules! #name {
(@#version $facade:ident $($rest:tt)*) => {
::$facade::__private::#implementation! { #carried $($rest)* }
};
(@ $($rest:tt)*) => {
::core::compile_error!(#older);
};
}
}
}
pub(crate) fn carrier_path(path: &Path) -> Path {
let mut path = path.clone();
if let Some(last) = path.segments.last_mut() {
last.arguments = PathArguments::None;
}
path
}
pub(crate) struct TraitRequest<'a> {
pub(crate) facade: &'a Ident,
pub(crate) subject: &'a Subject,
pub(crate) spec: &'a TraitSpec,
pub(crate) remote: Option<&'a Path>,
pub(crate) partial: Option<&'a ItemImpl>,
}
impl ToTokens for TraitRequest<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let TraitRequest {
facade,
subject,
spec,
remote,
partial,
} = self;
let version = version(REQUEST_VERSION);
let remote = remote.map(|path| quote!(remote { #path }));
let partial = partial.map(|block| quote!(impl { #block }));
tokens
.extend(quote! { @#version #facade enum { #subject } spec { #spec } #remote #partial });
}
}
pub(crate) struct CarriedTrait<'a> {
pub(crate) tokens: &'a TokenStream2,
pub(crate) remote: Option<&'a TokenStream2>,
pub(crate) provided: &'a [Ident],
}
impl ToTokens for CarriedTrait<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let CarriedTrait {
tokens: trait_tokens,
remote,
provided,
} = self;
let version = version(CARRIED_TRAIT_VERSION);
let remote = remote.map(|path| quote!(remote { #path }));
let provided = (!provided.is_empty()).then(|| quote!(provided { #(#provided),* }));
tokens.extend(quote! { @#version trait { #trait_tokens } #remote #provided });
}
}
pub(crate) struct ImplementInput {
pub(crate) item_trait: ItemTrait,
pub(crate) remote: Option<Path>,
pub(crate) provided: Vec<Ident>,
pub(crate) subject: Subject,
pub(crate) subject_remote: Option<Path>,
pub(crate) spec: TraitSpec,
pub(crate) partial: Option<ItemImpl>,
}
impl Parse for ImplementInput {
fn parse(input: ParseStream) -> Result<Self> {
parse_version(input, READABLE_TRAIT_VERSIONS)?;
input.parse::<Token![trait]>()?;
let item_trait = parse_braced(input)?;
let mut remote = None;
let mut provided = vec![];
segments(
input,
|input| input.peek(Token![enum]),
|tag, content| {
match (tag.to_string().as_str(), content) {
("remote", Some(tokens)) => remote = Some(syn::parse2(tokens)?),
("provided", Some(tokens)) => {
let names = Punctuated::<Ident, Token![,]>::parse_terminated;
provided = names.parse2(tokens)?.into_iter().collect();
}
_ => {}
}
Ok(())
},
)?;
input.parse::<Token![enum]>()?;
let subject = parse_braced(input)?;
let mut spec = None;
let mut subject_remote = None;
segments(
input,
|input| input.peek(Token![impl]),
|tag, content| {
match (tag.to_string().as_str(), content) {
("spec", Some(tokens)) => spec = Some(syn::parse2(tokens)?),
("remote", Some(tokens)) => subject_remote = Some(syn::parse2(tokens)?),
_ => {}
}
Ok(())
},
)?;
let spec = spec.ok_or_else(|| missing("spec"))?;
let partial = if input.peek(Token![impl]) {
input.parse::<Token![impl]>()?;
Some(parse_braced(input)?)
} else {
None
};
Ok(Self {
item_trait,
remote,
provided,
subject,
subject_remote,
spec,
partial,
})
}
}
pub(crate) struct BlockRequest<'a> {
pub(crate) facade: &'a Ident,
pub(crate) partial: Option<&'a TraitSpec>,
pub(crate) block: &'a ItemImpl,
}
impl ToTokens for BlockRequest<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let BlockRequest {
facade,
partial,
block,
} = self;
let version = version(REQUEST_VERSION);
let partial = partial.map(|spec| quote!(partial { #spec }));
tokens.extend(quote! { @#version #facade #partial impl { #block } });
}
}
pub(crate) struct CarriedSubject<'a> {
pub(crate) rejected: bool,
pub(crate) dispatched: &'a [&'a Path],
pub(crate) remote: Option<&'a Path>,
pub(crate) subject: &'a Subject,
}
impl ToTokens for CarriedSubject<'_> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let CarriedSubject {
rejected,
dispatched,
remote,
subject,
} = self;
let version = version(CARRIED_SUBJECT_VERSION);
let rejected = rejected.then(|| quote!(rejected));
let dispatched = (!dispatched.is_empty()).then(|| quote!(traits { #(#dispatched),* }));
let remote = remote.map(|path| quote!(remote { #path }));
tokens.extend(
quote! { @#version #rejected facade { $facade } #dispatched #remote enum { #subject } },
);
}
}
pub(crate) struct BlockInput {
pub(crate) rejected: bool,
pub(crate) facade: Ident,
pub(crate) dispatched: Vec<Path>,
pub(crate) remote: Option<Path>,
pub(crate) subject: Subject,
pub(crate) partial: Option<TraitSpec>,
pub(crate) item_impl: ItemImpl,
}
impl Parse for BlockInput {
fn parse(input: ParseStream) -> Result<Self> {
parse_version(input, &[CARRIED_SUBJECT_VERSION])?;
let mut rejected = false;
let mut facade = None;
let mut dispatched = vec![];
let mut remote = None;
segments(
input,
|input| input.peek(Token![enum]),
|tag, content| {
match (tag.to_string().as_str(), content) {
("rejected", None) => rejected = true,
("facade", Some(tokens)) => facade = Some(syn::parse2(tokens)?),
("traits", Some(tokens)) => {
let paths = Punctuated::<Path, Token![,]>::parse_terminated;
dispatched = paths.parse2(tokens)?.into_iter().collect();
}
("remote", Some(tokens)) => remote = Some(syn::parse2(tokens)?),
_ => {}
}
Ok(())
},
)?;
let facade = facade.ok_or_else(|| missing("facade"))?;
input.parse::<Token![enum]>()?;
let subject = parse_braced(input)?;
let mut partial = None;
segments(
input,
|input| input.peek(Token![impl]),
|tag, content| {
if let ("partial", Some(tokens)) = (tag.to_string().as_str(), content) {
partial = Some(syn::parse2(tokens)?);
}
Ok(())
},
)?;
input.parse::<Token![impl]>()?;
let item_impl = parse_braced(input)?;
Ok(Self {
rejected,
facade,
dispatched,
remote,
subject,
partial,
item_impl,
})
}
}
fn parse_version(input: ParseStream, readable: &[&str]) -> Result<()> {
input.parse::<Token![@]>()?;
let version: Ident = input.parse()?;
if !readable.iter().any(|known| version == known) {
return Err(Error::new_spanned(
version,
"this trait's carrier macro was written by a newer `whichever` than the one dispatching it; update `whichever` in this crate",
));
}
Ok(())
}
fn parse_braced<T: Parse>(input: ParseStream) -> Result<T> {
let content;
braced!(content in input);
content.parse()
}
fn segments(
input: ParseStream,
stop: impl Fn(ParseStream) -> bool,
mut on: impl FnMut(&Ident, Option<TokenStream2>) -> Result<()>,
) -> Result<()> {
while !input.is_empty() && !stop(input) {
let tag: Ident = input.parse()?;
let content = if input.peek(token::Brace) {
Some(parse_braced::<TokenStream2>(input)?)
} else {
None
};
on(&tag, content)?;
}
Ok(())
}
fn missing(segment: &str) -> Error {
Error::new(
Span::call_site(),
format!("the request has no `{segment}` segment"),
)
}
#[cfg(test)]
mod tests {
use quote::quote;
use syn::parse_quote;
use super::*;
fn subject() -> Subject {
Subject::Enum(parse_quote!(
enum Any {
A(A),
}
))
}
fn rest_of(request: impl ToTokens) -> TokenStream2 {
request.to_token_stream().into_iter().skip(3).collect()
}
#[test]
fn a_trait_request_reads_back_behind_its_carried_trait() {
let facade: Ident = parse_quote!(whichever);
let subject = subject();
let spec: TraitSpec = syn::parse_str("Trait, Error = X").unwrap();
let block: ItemImpl = parse_quote!(
impl Trait for Any {
fn make() -> Self {
todo!()
}
}
);
let subject_remote: Path = parse_quote!(::other::Any);
let request = TraitRequest {
facade: &facade,
subject: &subject,
spec: &spec,
remote: Some(&subject_remote),
partial: Some(&block),
};
let trait_tokens = quote!(
pub trait Trait {
type Error;
fn make() -> Self;
}
);
let remote = quote!(::other::Trait);
let provided: Vec<Ident> = vec![parse_quote!(make)];
let carried = CarriedTrait {
tokens: &trait_tokens,
remote: Some(&remote),
provided: &provided,
};
let rest = rest_of(request);
let input: ImplementInput = syn::parse2(quote!(#carried #rest)).unwrap();
assert_eq!(input.item_trait.ident, "Trait");
assert_eq!(
input.remote.map(|path| path.to_token_stream().to_string()),
Some(remote.to_string())
);
assert_eq!(input.subject.ident(), "Any");
assert_eq!(input.provided, vec![parse_quote!(make)] as Vec<Ident>);
assert!(input.subject_remote.is_some());
assert!(input.spec.is_overridden(&parse_quote!(Error)));
assert_eq!(input.partial.map(|block| block.items.len()), Some(1));
}
#[test]
fn a_block_request_reads_back_behind_its_carried_subject() {
let facade: Ident = parse_quote!(wh);
let block: ItemImpl = parse_quote!(
impl Any {
fn plain(&self) {}
}
);
let spec: TraitSpec = syn::parse_str("Error = X").unwrap();
let request = BlockRequest {
facade: &facade,
partial: Some(&spec),
block: &block,
};
let rest = rest_of(request);
let subject = subject();
let message = quote!(@v1 rejected facade { wh } traits { Shape, a::Encoder<X> } remote { other::Any } enum { #subject } #rest);
let input: BlockInput = syn::parse2(message).unwrap();
assert!(input.rejected);
assert!(input.remote.is_some());
assert_eq!(input.facade, "wh");
assert_eq!(input.dispatched.len(), 2);
assert!(input
.partial
.is_some_and(|spec| spec.is_overridden(&parse_quote!(Error))));
assert_eq!(input.item_impl.items.len(), 1);
}
#[test]
fn unknown_segments_are_skipped_and_a_wrong_version_is_not() {
let subject = subject();
let newer = quote! {
@v2 trait { trait T { fn f(&self); } } later { 1 } enum { #subject } spec { T } someday
};
assert!(syn::parse2::<ImplementInput>(newer).is_ok());
let of_0_6 = quote!(@v1 trait { trait T { fn f(&self) {} } } enum { #subject } spec { T });
assert!(
syn::parse2::<ImplementInput>(of_0_6).is_ok(),
"the prefix of the carriers before this release is still read"
);
let later_version = quote!(@v3 trait { trait T {} } enum { #subject } spec { T });
let error = syn::parse2::<ImplementInput>(later_version)
.err()
.expect("an error")
.to_string();
assert!(
error.starts_with("this trait's carrier macro was written by a newer"),
"{error}"
);
let without_spec = quote!(@v2 trait { trait T {} } enum { #subject });
assert!(syn::parse2::<ImplementInput>(without_spec).is_err());
}
#[test]
fn a_carrier_matches_the_request_and_reports_any_other() {
let name: Ident = parse_quote!(__whichever_T_1);
let carried = quote!(@v2 trait { trait T {} });
let carrier = carrier_macro(&name, quote!(), Implementation::Trait, carried).to_string();
assert!(
carrier.contains("@ v1 $ facade : ident $ ($ rest : tt) *"),
"{carrier}"
);
assert!(carrier.contains("(@ $ ($ rest : tt) *)"), "{carrier}");
assert!(carrier.contains(CARRIER_IS_OLDER), "{carrier}");
assert!(carrier.contains("@ v2 trait"), "{carrier}");
}
}