split-async 0.1.1

A procedural macro to generate sync and async versions of a function
Documentation
use derive_more::From;
use proc_macro2::Span;
use syn::{
    Error, Ident, LitStr, Path, PathSegment, Token,
    parse::{Parse, ParseStream},
};

pub struct TwoIdents {
    pub sync_ident: Ident,
    pub async_ident: Ident,
}

pub fn split_idents(name: &Ident) -> TwoIdents {
    TwoIdents {
        sync_ident: proc_macro2::Ident::new(&format!("sync_{name}"), Span::call_site()),
        async_ident: proc_macro2::Ident::new(&format!("async_{name}"), Span::call_site()),
    }
}

#[derive(From)]
pub struct SplitArgs(pub TwoIdents);

fn parse_ident_or_litstr(input: &ParseStream) -> syn::Result<Ident> {
    let input_span = input.span();
    if input.peek(Ident) {
        input.parse()
    } else if input.peek(LitStr) {
        let s: LitStr = input.parse()?;
        Ok(Ident::new(&s.value(), s.span()))
    } else {
        Err(syn::Error::new(
            input_span,
            "expected an identifier or a string literal",
        ))
    }
}

impl Parse for SplitArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let ident1 = parse_ident_or_litstr(&input)?;

        if input.peek(Token![,]) {
            let _: Token![,] = input.parse()?;
            if !input.is_empty() {
                let ident2 = parse_ident_or_litstr(&input)?;

                if ident1 == ident2 {
                    return Err(Error::new_spanned(ident2, "repeated identifier"));
                }

                let _: Option<Token![,]> = input.parse()?;

                if !input.is_empty() {
                    return Err(input.error("unexpected token"));
                }

                return Ok(Self(TwoIdents {
                    sync_ident: ident1,
                    async_ident: ident2,
                }));
            }
        }

        if !input.is_empty() {
            return Err(input.error("unexpected token"));
        }

        Ok(Self(split_idents(&ident1)))
    }
}

pub struct ChooseArgs {
    pub sync_path: Path,
    pub async_path: Path,
}

fn parse_path_or_litstr(input: &ParseStream) -> syn::Result<Path> {
    let input_span = input.span();
    if input.peek(Token![::]) || input.peek(Ident) {
        input.parse()
    } else if input.peek(LitStr) {
        let s: LitStr = input.parse()?;
        Ok(Ident::new(&s.value(), s.span()).into())
    } else {
        Err(syn::Error::new(
            input_span,
            "expected an identifier or a string literal",
        ))
    }
}

fn split_path(path: &Path) -> syn::Result<ChooseArgs> {
    let tail_segment = path
        .segments
        .last()
        .ok_or_else(|| Error::new_spanned(path, "bad path"))?;
    if !tail_segment.arguments.is_empty() {
        return Err(Error::new_spanned(tail_segment, "must be an identifier"));
    }
    let TwoIdents {
        sync_ident,
        async_ident,
    } = split_idents(&tail_segment.ident);
    let mut sync_path = path.clone();
    *sync_path.segments.last_mut().unwrap() = PathSegment::from(sync_ident);
    let mut async_path = path.clone();
    *async_path.segments.last_mut().unwrap() = PathSegment::from(async_ident);
    Ok(ChooseArgs {
        sync_path,
        async_path,
    })
}

impl Parse for ChooseArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let path1 = parse_path_or_litstr(&input)?;

        if input.peek(Token![,]) {
            let _: Token![,] = input.parse()?;
            if !input.is_empty() {
                let path2 = parse_path_or_litstr(&input)?;

                if path1 == path2 {
                    return Err(Error::new_spanned(path2, "repeated identifier"));
                }

                let _: Option<Token![,]> = input.parse()?;

                if !input.is_empty() {
                    return Err(input.error("unexpected token"));
                }

                return Ok(Self {
                    sync_path: path1,
                    async_path: path2,
                });
            }
        }

        if !input.is_empty() {
            return Err(input.error("unexpected token"));
        }

        split_path(&path1)
    }
}