proc-macro-assertions 0.1.5

Easily create asserts on proc macro inputs
Documentation
use better_any::{Tid, TidAble};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Ident, TraitBound, Type};

use crate::{
    context::Context,
    into_template::IntoTemplate,
    maybe_borrowed::{FromMaybeBorrowed, MaybeBorrowed},
    passed_data::{PassedData, WithData},
    token_cmp_wrapper::TokenCmpWrapper,
};

use super::{Generatable, StaticTid};

/// A trait Generatable / Template. This asserts that some type implements some trait.
#[derive(Tid)]
pub struct Trait<'a> {
    /// The trait bound to check the type implements
    trait_bound: MaybeBorrowed<'a, TraitBound>,
}

impl<'a> Trait<'a> {
    /// Creates a new trait template from some trait bound.
    /// Takes any T that can be turned into a `MaybeBorrowed` type, to support
    /// references as well as owned types
    pub fn new<T>(trait_bound: T) -> Self
    where
        T: Into<MaybeBorrowed<'a, TraitBound>>,
    {
        Self {
            trait_bound: trait_bound.into(),
        }
    }
}

impl<'a> FromMaybeBorrowed<'a, TraitBound> for Trait<'a> {
    fn from_maybe_borrowed(from: MaybeBorrowed<'a, TraitBound>) -> Self {
        Self::new(from)
    }
}

impl<'a> IntoTemplate<'a> for TraitBound {
    type Template = Trait<'a>;

    fn into_template<T: crate::into_template::TypeEq<This = Self::Template>>(
        self,
    ) -> Self::Template {
        Trait::new(self)
    }
}

impl<'a> IntoTemplate<'a> for &'a TraitBound {
    type Template = Trait<'a>;

    fn into_template<T: crate::into_template::TypeEq<This = Self::Template>>(
        self,
    ) -> Self::Template {
        Trait::new(self)
    }
}

impl<'a> PartialOrd for Trait<'a> {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(Ord::cmp(self, other))
    }
}

impl<'a> Ord for Trait<'a> {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        Ord::cmp(
            &self.trait_bound.to_token_stream().to_string(),
            &other.trait_bound.to_token_stream().to_string(),
        )
    }
}

impl<'a> PartialEq for Trait<'a> {
    fn eq(&self, other: &Self) -> bool {
        PartialEq::eq(
            &self.trait_bound.to_token_stream().to_string(),
            &other.trait_bound.to_token_stream().to_string(),
        )
    }
}

impl<'a> Eq for Trait<'a> {}

impl<'a> Generatable<'a, TokenCmpWrapper<Type>> for Trait<'a> {
    type GeneratableData = ();
    type TemplateData = Ident;

    const EMITS_NON_CONSTANT_CODE: bool = false;

    fn template(
        &self,
        Context {
            ident_generator, ..
        }: &mut Context,
        _passed: &Self::GeneratableData,
    ) -> PassedData<Self::TemplateData> {
        let fn_ident = ident_generator.prefixed("assert_trait_bound");
        let trait_bound = &*self.trait_bound;

        quote! {
            fn #fn_ident<T: #trait_bound>() {}
        }
        .with_data(fn_ident)
    }

    fn assert(
        &self,
        _context: &mut Context,
        (_, assert_trait_bound): (&Self::GeneratableData, &Self::TemplateData),
        to_assert: &TokenCmpWrapper<syn::Type>,
    ) -> Option<TokenStream> {
        Some(quote! {
            #assert_trait_bound::<#to_assert>();
        })
    }

    fn generatable(_context: &mut Context) -> PassedData<Self::GeneratableData>
    where
        Self: Sized,
    {
        PassedData::default()
    }
}

impl<'a> Generatable<'a, StaticTid<Ident>> for Trait<'a> {
    type GeneratableData = ();
    type TemplateData = Ident;

    const EMITS_NON_CONSTANT_CODE: bool = true;

    fn template(
        &self,
        Context {
            ident_generator, ..
        }: &mut Context,
        _passed: &Self::GeneratableData,
    ) -> PassedData<Self::TemplateData> {
        let fn_ident = ident_generator.prefixed("assert_trait_bound");
        let trait_bound = &*self.trait_bound;

        quote! {
            fn #fn_ident<T: #trait_bound>(_v: T) {}
        }
        .with_data(fn_ident)
    }

    fn assert(
        &self,
        _context: &mut Context,
        (_, assert_trait_bound): (&Self::GeneratableData, &Self::TemplateData),
        to_assert: &StaticTid<proc_macro2::Ident>,
    ) -> Option<TokenStream> {
        Some(quote! {
            #assert_trait_bound(#to_assert);
        })
    }

    fn generatable(_context: &mut Context) -> PassedData<Self::GeneratableData>
    where
        Self: Sized,
    {
        PassedData::default()
    }
}

#[cfg(test)]
mod test {
    use quote::quote;
    use syn::parse_quote;

    use crate::{
        context::Context,
        generatable::Generatable,
        ident_generator::MockIdentGenerator,
        maybe_borrowed::MaybeBorrowed,
        passed_data::{PassedData, WithData},
        token_cmp_wrapper::TokenCmpWrapper,
    };

    use super::Trait;

    #[test]
    fn generate_generatable() {
        let mut mock_ident_gen = MockIdentGenerator::new();
        let mut context = Context::new(&mut mock_ident_gen);

        assert_eq!(
            <Trait as Generatable<TokenCmpWrapper<syn::Type>>>::generatable(&mut context),
            PassedData::default()
        );
        assert!(mock_ident_gen.was_not_called());
    }

    #[test]
    fn generate_template() {
        let mut mock_ident_gen = MockIdentGenerator::new();

        mock_ident_gen.push_ident("mock_1");

        let mut context = Context::new(&mut mock_ident_gen);
        assert_eq!(
            <Trait as Generatable<TokenCmpWrapper<syn::Type>>>::template(
                &Trait {
                    trait_bound: MaybeBorrowed::Owned(parse_quote!(::test_mod::Test))
                },
                &mut context,
                &()
            ),
            quote! {
                fn assert_trait_bound_mock_1<T: ::test_mod::Test>() {}
            }
            .with_data(parse_quote!(assert_trait_bound_mock_1))
        );
        assert!(mock_ident_gen.has_no_idents_remaining());
    }

    #[test]
    fn generate_assert() {
        let mut mock_ident_gen = MockIdentGenerator::new();
        let mut context = Context::new(&mut mock_ident_gen);

        let test_type: syn::Type = parse_quote!(TestType);

        assert_eq!(
            <Trait as Generatable<TokenCmpWrapper<syn::Type>>>::assert(
                &Trait {
                    trait_bound: MaybeBorrowed::Owned(parse_quote!(::test_mod::Test))
                },
                &mut context,
                (&(), &parse_quote!(assert_trait_bound_mock_1)),
                &test_type.into()
            )
            .as_ref()
            .map(ToString::to_string),
            Some(quote! {
                assert_trait_bound_mock_1::<TestType>();
            })
            .as_ref()
            .map(ToString::to_string)
        );
        assert!(mock_ident_gen.was_not_called());
    }
}