use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, quote_spanned, ToTokens};
use syn::parse::{Parse, ParseStream};
use syn::{Ident, Index, LitFloat, LitInt, Member, Result, Token};
#[derive(Clone)]
pub(crate) enum Step {
Deref,
Field(Member),
Method(Ident),
}
#[derive(Clone, Default)]
pub(crate) struct Route {
pub(crate) steps: Vec<Step>,
}
impl Route {
pub(crate) fn deref() -> Self {
Route {
steps: vec![Step::Deref],
}
}
pub(crate) fn is_direct(&self) -> bool {
self.steps.is_empty()
}
pub(crate) fn is_deref(&self) -> bool {
matches!(self.steps.as_slice(), [Step::Deref])
}
pub(crate) fn starts_with_deref(&self) -> bool {
matches!(self.steps.first(), Some(Step::Deref))
}
pub(crate) fn names_the_target(&self) -> bool {
self.is_direct() || self.is_deref()
}
pub(crate) fn ends_in_a_method(&self) -> bool {
matches!(self.steps.last(), Some(Step::Method(_)))
}
pub(crate) fn ends_in_a_deref(&self) -> bool {
matches!(self.steps.last(), Some(Step::Deref))
}
pub(crate) fn reach(
&self,
binding: &Ident,
through_reference: bool,
site: Span,
) -> TokenStream2 {
let mut expression = quote_spanned!(site => #binding);
let mut derefs = 0usize;
for (index, step) in self.steps.iter().enumerate() {
match step {
Step::Deref => {
if index == 0 && through_reference {
expression = quote_spanned!(site => **#expression);
derefs = 2;
} else {
expression = quote_spanned!(site => *#expression);
derefs += 1;
}
}
Step::Field(member) => {
if derefs > 0 {
expression = quote_spanned!(site => (#expression).#member);
} else {
expression = quote_spanned!(site => #expression.#member);
}
derefs = 0;
}
Step::Method(method) => {
if derefs > 0 {
expression = quote_spanned!(site => (#expression).#method());
} else {
expression = quote_spanned!(site => #expression.#method());
}
derefs = 0;
}
}
}
expression
}
pub(crate) fn reach_for_postfix(
&self,
binding: &Ident,
through_reference: bool,
site: Span,
) -> TokenStream2 {
let expression = self.reach(binding, through_reference, site);
if self.ends_in_a_deref() {
return quote_spanned!(site => (#expression));
}
expression
}
pub(crate) fn spelled(&self) -> String {
self.to_token_stream().to_string().replace(' ', "")
}
}
impl Parse for Route {
fn parse(input: ParseStream) -> Result<Self> {
let mut steps = vec![];
while !input.is_empty() && !input.peek(Token![,]) {
if input.peek(Token![*]) {
input.parse::<Token![*]>()?;
steps.push(Step::Deref);
continue;
}
if input.peek(Token![.]) {
input.parse::<Token![.]>()?;
if input.peek(Ident) {
let ident: Ident = input.parse()?;
if input.peek(syn::token::Paren) {
let content;
syn::parenthesized!(content in input);
if !content.is_empty() {
return Err(content.error("a method on a route takes no arguments"));
}
steps.push(Step::Method(ident));
} else {
steps.push(Step::Field(Member::Named(ident)));
}
continue;
}
if input.peek(LitInt) {
let index: LitInt = input.parse()?;
steps.push(Step::Field(Member::Unnamed(Index {
index: index.base10_parse()?,
span: index.span(),
})));
continue;
}
if input.peek(LitFloat) {
let float: LitFloat = input.parse()?;
for part in float.base10_digits().split('.') {
steps.push(Step::Field(Member::Unnamed(Index {
index: part.parse().map_err(|_| {
syn::Error::new(float.span(), "expected a field index")
})?,
span: float.span(),
})));
}
continue;
}
return Err(
input.error("expected a field, `.name` or `.0`, or a method, `.name()`")
);
}
return Err(input.error(
"a route is `*` for what the payload dereferences to, `.name` or `.0` for a field, or `.name()` for what a method returns, in any sequence",
));
}
if steps.is_empty() {
return Err(
input.error("a route needs at least one step: `*`, `.name`, `.0` or `.name()`")
);
}
Ok(Route { steps })
}
}
impl ToTokens for Route {
fn to_tokens(&self, tokens: &mut TokenStream2) {
for step in &self.steps {
match step {
Step::Deref => tokens.extend(quote!(*)),
Step::Field(member) => tokens.extend(quote!(.#member)),
Step::Method(method) => tokens.extend(quote!(.#method())),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn route(tokens: &str) -> Route {
syn::parse_str(tokens).expect("a valid route")
}
fn spelled(tokens: TokenStream2) -> String {
tokens.to_string().replace(' ', "")
}
#[test]
fn a_route_is_read_and_written_back() {
for text in [
"*",
".base",
".1",
".get_ref()",
"*.inner",
".0.1",
".a.b()",
"*.a*",
] {
assert_eq!(route(text).spelled(), text, "{text}");
}
assert!(syn::parse_str::<Route>("").is_err());
assert!(syn::parse_str::<Route>("base").is_err());
assert!(syn::parse_str::<Route>(".get(1)").is_err());
assert!(route("*").is_deref() && route("*").names_the_target());
assert!(!route(".a").names_the_target());
assert!(route(".a.b()").ends_in_a_method());
}
#[test]
fn the_target_is_reached_through_the_reference_or_from_the_payload() {
let binding = Ident::new("inner", Span::call_site());
let reach = |text: &str, through_reference: bool| {
spelled(route(text).reach(&binding, through_reference, Span::call_site()))
};
assert_eq!(reach("*", true), "**inner");
assert_eq!(reach("*", false), "*inner");
assert_eq!(reach(".base", true), "inner.base");
assert_eq!(reach(".base", false), "inner.base");
assert_eq!(reach(".1", true), "inner.1");
assert_eq!(reach(".get_ref()", true), "inner.get_ref()");
assert_eq!(reach("*.inner", true), "(**inner).inner");
assert_eq!(reach("*.inner", false), "(*inner).inner");
assert_eq!(reach(".a*", true), "*inner.a");
}
#[test]
fn a_route_ends_in_a_deref_when_its_last_step_is_one() {
for text in ["*", ".boxed*", "*.a*", ".get_ref()*"] {
assert!(route(text).ends_in_a_deref(), "{text}");
}
for text in [".base", "*.inner", ".a.b()", ".0.1"] {
assert!(!route(text).ends_in_a_deref(), "{text}");
}
assert!(!Route::default().ends_in_a_deref());
}
#[test]
fn a_route_ending_in_a_deref_is_parenthesised_for_a_postfix() {
let binding = Ident::new("inner", Span::call_site());
let reach = |text: &str, through_reference: bool| {
spelled(route(text).reach_for_postfix(&binding, through_reference, Span::call_site()))
};
assert_eq!(reach("*", true), "(**inner)");
assert_eq!(reach("*", false), "(*inner)");
assert_eq!(reach(".boxed*", true), "(*inner.boxed)");
assert_eq!(reach(".boxed*", false), "(*inner.boxed)");
assert_eq!(reach("*.a*", true), "(*(**inner).a)");
assert_eq!(reach(".get_ref()*", true), "(*inner.get_ref())");
assert_eq!(reach(".base", true), "inner.base");
assert_eq!(reach("*.inner", true), "(**inner).inner");
assert_eq!(reach(".a.b()", false), "inner.a.b()");
}
}