fluid_attributes 0.4.0

Proc macro attributes for the fluid crate.
Documentation
extern crate proc_macro;

#[macro_use]
mod debug;

mod fact;
mod session;
mod theory;

mod body;
mod helpers;

mod assertion_derive;

use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse::Error, parse_macro_input, ItemFn, ItemImpl, ItemStruct};

const DOC_MSG: &str = "Read the documentation at: https://docs.rs/fluid/wiki";

/// The `fact` attribute.
/// A fact is a simple concrete fact to be verified.
///
/// See the [wiki] for more information.
///
/// # Example
///
/// ```
/// #[fact]
/// fn cerberus_has_3_heads() {
///     number_of_faces("Cerberus").should().be_equal_to(3)
///         .because("that's how Cerberus is described");
/// }
/// ```
///
/// [wiki]: https://docs.rs/fluid/wiki/fact
#[proc_macro_attribute]
pub fn fact(args: TokenStream, input: TokenStream) -> TokenStream {
    if let Some(tok) = args.into_iter().next() {
        let msg = format!(
            "The macro `fact` must have no arguments. \
             If you want to add some test cases, please use a `theory` with `case`s. {}",
            DOC_MSG
        );
        Error::new(tok.span().into(), msg).to_compile_error().into()
    } else {
        let mut output: TokenStream = quote!(#[cfg(test)] #[fact_inner]).into();

        output.extend(input);
        output
    }
}

/// To not doing expensive treatments when not launching the tests, the `fact` attribute expends
/// to a conditional `fact_inner` attribute.
#[doc(hidden)]
#[proc_macro_attribute]
pub fn fact_inner(_args: TokenStream, input: TokenStream) -> TokenStream {
    debug!(input);
    let output = fact::generate(parse_macro_input!(input as ItemFn))
        .unwrap_or_else(|e| e.to_compile_error());

    debug!(output);
    output.into()
}

/// The `theory` attribute.
/// A theory is a test more complex than a fact, that must be proven or inferred with multiple cases.
/// Therefore, a theory must come with its multiple test `case`s.
///
/// See the [wiki] for more information.
///
/// # Example
///
/// ```
/// #[theory]
/// #[case("Cerberus", 3)]
/// #[case("Hydra", 7)]
/// #[case("Janus", 2)]
/// #[case("Normal guy", 1)]
/// fn each_cerature_has_a_correct_number_of_faces(name: &str, nbr_faces: u8) {
///     number_of_faces(name).should().be_equal_to(nbr_faces);
/// }
/// ```
///
/// [wiki]: https://docs.rs/fluid/wiki/theory
#[proc_macro_attribute]
pub fn theory(args: TokenStream, input: TokenStream) -> TokenStream {
    if let Some(tok) = args.into_iter().next() {
        let msg = format!(
            "The macro `theory` must have no arguments. \
             If you want to add some test cases, please use the `case` attribute. {}",
            DOC_MSG
        );
        Error::new(tok.span().into(), msg).to_compile_error().into()
    } else {
        let mut output: TokenStream = quote!(#[cfg(test)] #[theory_inner]).into();

        output.extend(input);
        output
    }
}

/// To not doing expensive treatments when not launching the tests, the `theory` attribute expends
/// to a conditional `theory_inner` attribute.
#[doc(hidden)]
#[proc_macro_attribute]
pub fn theory_inner(_args: TokenStream, input: TokenStream) -> TokenStream {
    debug!(input);
    let mut input: ItemFn = parse_macro_input!(input as ItemFn);
    let module = theory::generate(
        input.ident.clone(),
        &mut input.decl,
        &mut input.block,
        &mut input.attrs,
        None,
    )
    .map(ToTokens::into_token_stream)
    .unwrap_or_else(|e| e.to_compile_error());
    let output = {
        let mut module = module;
        module.extend(input.into_token_stream());

        module
    };

    debug!(output);
    output.into()
}

/// A test session with a setup. Must decorate an inherent implementation
/// of a data structure that implements `Default`.
#[proc_macro_attribute]
pub fn session(args: TokenStream, input: TokenStream) -> TokenStream {
    if let Some(tok) = args.into_iter().next() {
        let msg = format!("The macro `session` must have no arguments. {}", DOC_MSG);
        Error::new(tok.span().into(), msg).to_compile_error().into()
    } else {
        let mut output: TokenStream = quote!(#[cfg(test)] #[session_inner]).into();

        output.extend(input);
        output
    }
}

/// To not doing expensive treatments when not launching the tests, the `session` attribute expends
/// to a conditional `session_inner` attribute.
#[doc(hidden)]
#[proc_macro_attribute]
pub fn session_inner(_args: TokenStream, input: TokenStream) -> TokenStream {
    debug!(input);
    let item = parse_macro_input!(input as ItemImpl);
    let output = session::generate(item).unwrap_or_else(|e| e.to_compile_error());

    debug!(output);
    output.into()
}

/// Implement the `Drop` trait for the assertion type. This is needed, because
/// in most cases, the assertion will be checked from within the destructor.
#[proc_macro_derive(Drop)]
pub fn drop_derive(input: TokenStream) -> TokenStream {
    use assertion_derive::*;

    debug!(input);
    let item = parse_macro_input!(input as ItemStruct);
    let output = generate(item).unwrap_or_else(|e| e.to_compile_error());

    debug!(output);
    output.into()
}