railway-api-derive 0.2.0

Derive macro for railway-api
Documentation
#![doc = include_str!("../README.md")]

use proc_macro::TokenStream as PmTokenStream;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
    parse_macro_input, punctuated::Punctuated, spanned::Spanned, Attribute, Data, DeriveInput,
    Error, Ident, Meta, MetaNameValue, Token, Variant,
};

struct ApiType {
    name: Ident,
    cfgs: Vec<Attribute>,
    constructor: TokenStream,
}

impl ApiType {
    fn feature(&self) -> TokenStream {
        let cfgs = &self.cfgs;
        quote! { #(#cfgs)* }
    }
}

#[proc_macro_derive(ProviderApi, attributes(provider))]
pub fn provider_api_derive(input: PmTokenStream) -> PmTokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    match provider_api_derive_error(&ast) {
        Ok(ts) => ts.into(),
        Err(e) => e.to_compile_error().into(),
    }
}

fn provider_api_derive_error(ast: &DeriveInput) -> Result<TokenStream, Error> {
    let variants = extract_variants(ast)?;

    if variants.is_empty() {
        panic!("Provider API needs at least one variant active");
    }

    let type_list = impl_type_list(&variants);
    let type_list_variants = impl_type_list_variants(&variants);
    let type_list_from_str = impl_type_list_from_str(&variants);
    let type_list_to_str = impl_type_list_to_str(&variants);
    let constructor = impl_constructor(&variants);
    let provider_impl = impl_provider_impl(&variants);

    let result = quote! {
        #type_list

        #type_list_variants

        #type_list_from_str

        #type_list_to_str

        #constructor

        #provider_impl
    };

    Ok(result)
}

fn extract_variants(ast: &DeriveInput) -> Result<Vec<ApiType>, Error> {
    let Data::Enum(data) = &ast.data else {
        return Err(Error::new(
            ast.span(),
            "ProviderApi-macro only applicable to enums",
        ));
    };
    data.variants.iter().map(variant_to_api_variant).collect()
}

fn variant_to_api_variant(var: &Variant) -> Result<ApiType, Error> {
    let name = var.ident.clone();
    let my_attrs = var
        .attrs
        .iter()
        .flat_map(|a| match &a.meta {
            Meta::List(l)
                if l.path.get_ident().map(|i| i.to_string()) == Some("provider".to_string()) =>
            {
                l.parse_args_with(Punctuated::<MetaNameValue, Token![,]>::parse_terminated)
                    .unwrap_or_default()
                    .into_iter()
                    .collect()
            }
            _ => vec![],
        })
        .collect::<Vec<_>>();
    let Some(constructor) = my_attrs
        .iter()
        .filter(|a| a.path.get_ident().map(|i| i.to_string()) == Some("constructor".to_string()))
        .map(|n| n.value.to_token_stream())
        .next()
        .or_else(|| {
            my_attrs
                .iter()
                .filter(|a| a.path.get_ident().map(|i| i.to_string()) == Some("hafas".to_string()))
                .map(|n| &n.value)
                .map(|v| quote! { |r| rhafas::client::HafasClient::new(#v, r) })
                .next()
        })
    else {
        return Err(Error::new(
            var.span(),
            "Provider does not have a constructor or hafas",
        ));
    };

    let cfg_attrs = var
        .attrs
        .iter()
        .filter(|a| {
            matches!(&a.meta,
            Meta::List(l)
                if l.path.get_ident().map(|i| i.to_string()) == Some("cfg".to_string()))
        })
        .cloned()
        .collect::<Vec<_>>();
    Ok(ApiType {
        name,
        constructor: constructor.clone(),
        cfgs: cfg_attrs,
    })
}

fn impl_type_list(types: &[ApiType]) -> TokenStream {
    let parts = types.iter().map(impl_type_list_single);
    quote! {
        #[automatically_derived]
        #[derive(Debug, Clone, PartialEq, Eq)]
        /// An enumeration listing all available [`Provider`s](rcore::Provider) in the [`RailwayProvider`].
        pub enum RailwayProviderType {
            #(#parts),*
        }
    }
}

fn impl_type_list_single(t: &ApiType) -> TokenStream {
    let name = &t.name;
    let feature = t.feature();
    quote! {
        #feature
        #name
    }
}

fn impl_type_list_variants(types: &[ApiType]) -> TokenStream {
    let parts = types.iter().map(impl_type_list_single_variant);
    quote! {
        #[automatically_derived]
        impl RailwayProviderType {
            pub fn variants() -> &'static [Self] {
                &[
                    #(#parts),*
                ]
            }
        }
    }
}

fn impl_type_list_single_variant(t: &ApiType) -> TokenStream {
    let name = &t.name;
    let feature = t.feature();
    quote! {
        #feature
        Self::#name
    }
}

fn impl_type_list_from_str(types: &[ApiType]) -> TokenStream {
    let parts = types.iter().map(impl_type_list_from_str_single);
    quote! {
        #[automatically_derived]
        impl std::str::FromStr for RailwayProviderType {
            type Err = ();
            fn from_str(input: &str) -> Result<Self, Self::Err> {
                let input = input.to_lowercase().replace(&['-', '_'][..], "");
                match &input[..] {
                    #(#parts)*
                    _ => Err(()),
                }
            }
        }
    }
}

fn impl_type_list_from_str_single(t: &ApiType) -> TokenStream {
    let name = &t.name;
    let string = t.name.to_string().to_lowercase();
    let feature = t.feature();
    quote! {
        #feature
        #string => Ok(Self::#name),
    }
}

fn impl_type_list_to_str(types: &[ApiType]) -> TokenStream {
    let parts = types.iter().map(impl_type_list_to_str_single);
    quote! {
        #[automatically_derived]
        impl std::fmt::Display for RailwayProviderType {

            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                let s = match self {
                    #(#parts)*
                };
                write!(f, "{}", s)
            }
        }
    }
}

fn impl_type_list_to_str_single(t: &ApiType) -> TokenStream {
    use convert_case::{Case, Casing};

    let name = &t.name;
    let string = t.name.to_string().to_case(Case::Kebab);
    let feature = t.feature();
    quote! {
        #feature
        Self::#name => #string,
    }
}

fn impl_constructor(types: &[ApiType]) -> TokenStream {
    let parts = types.iter().map(impl_constructor_single);
    quote! {
        #[automatically_derived]
        impl RailwayProvider {
            pub fn new(r#type: RailwayProviderType, builder: rcore::ReqwestRequesterBuilder) -> Self {
                match r#type {
                    #(#parts),*
                }
            }
        }
    }
}

fn impl_constructor_single(t: &ApiType) -> TokenStream {
    let name = &t.name;
    let constructor = &t.constructor;
    let feature = t.feature();
    quote! {
        #feature
        RailwayProviderType::#name => Self::#name((#constructor)(builder))
    }
}

fn impl_provider_impl(types: &[ApiType]) -> TokenStream {
    let journeys = impl_provider_impl_journeys(types);
    let locations = impl_provider_impl_locations(types);
    let station_board = impl_provider_impl_station_board(types);
    let refresh = impl_provider_impl_refresh(types);

    quote! {
        #[automatically_derived]
        #[cfg_attr(feature = "rt-multi-thread", async_trait::async_trait)]
        #[cfg_attr(not(feature = "rt-multi-thread"), async_trait::async_trait(?Send))]
        impl rcore::Provider<rcore::ReqwestRequester> for RailwayProvider {
            type Error = BoxedError;

            #journeys

            #locations

            #station_board

            #refresh
        }
    }
}

fn impl_provider_impl_journeys(types: &[ApiType]) -> TokenStream {
    let parts = types
        .iter()
        .map(|t| impl_provider_impl_single(t, quote! {journeys(from, to, opts)}));
    quote! {
        async fn journeys(
            &self,
            from: rcore::Place,
            to: rcore::Place,
            opts: rcore::JourneysOptions,
        ) -> Result<rcore::JourneysResponse, rcore::Error<<R as Requester>::Error, Self::Error>> {
            use rcore::Provider;
            match self {
                #(#parts),*
            }
        }
    }
}

fn impl_provider_impl_locations(types: &[ApiType]) -> TokenStream {
    let parts = types
        .iter()
        .map(|t| impl_provider_impl_single(t, quote! {refresh_journey(journey, opts)}));
    quote! {
        async fn refresh_journey(
            &self,
            journey: &rcore::Journey,
            opts: rcore::RefreshJourneyOptions,
        ) -> Result<rcore::RefreshJourneyResponse, rcore::Error<<R as Requester>::Error, Self::Error>>
        {
            use rcore::Provider;
            match self {
                #(#parts),*
            }
        }
    }
}

fn impl_provider_impl_station_board(types: &[ApiType]) -> TokenStream {
    let parts = types
        .iter()
        .map(|t| impl_provider_impl_single(t, quote! {station_board(place, kind, opts)}));
    quote! {
        async fn station_board(
            &self,
            place: rcore::Place,
            kind: rcore::StationBoardKind,
            opts: rcore::StationBoardOptions,
        ) -> Result<rcore::StationBoardResponse, rcore::Error<<R as Requester>::Error, Self::Error>> {
            use rcore::Provider;
            match self {
                #(#parts),*
            }
        }
    }
}

fn impl_provider_impl_refresh(types: &[ApiType]) -> TokenStream {
    let parts = types
        .iter()
        .map(|t| impl_provider_impl_single(t, quote! {locations(opts)}));
    quote! {
        async fn locations(
            &self,
            opts: rcore::LocationsOptions,
        ) -> Result<rcore::LocationsResponse, rcore::Error<<R as Requester>::Error, Self::Error>> {
            use rcore::Provider;
            match self {
                #(#parts),*
            }
        }
    }
}

fn impl_provider_impl_single(t: &ApiType, code: TokenStream) -> TokenStream {
    let name = &t.name;
    let feature = t.feature();
    quote! {
        #feature
        RailwayProvider::#name(p) => p.#code.await.map_err(transform_error)
    }
}