system2-macros 0.1.0

The macros of system2.
Documentation
use crate::part::Part;
use proc_macro2::{Span, TokenStream};
use quote::{ToTokens, quote};
use syn::Token;
use syn::parse::{Parse, ParseStream};

pub(crate) struct Bbox {
    instructions: Part,
}

impl Parse for Bbox {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.is_empty() {
            let message =
                "`bbox!` takes the instructions: a string literal, a constant, or a JSON literal";
            return Err(syn::Error::new(Span::call_site(), message));
        }
        let instructions = Part::parse(input, "the instructions", "Bbox::new")?;

        input.parse::<Option<Token![,]>>()?;
        if !input.is_empty() {
            let message = "`bbox!` takes the instructions, and nothing more: \
                           to fill in the instructions, name each value in a placeholder, \
                           as in `{name}`";
            return Err(input.error(message));
        }
        Ok(Bbox { instructions })
    }
}

impl ToTokens for Bbox {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let Bbox { instructions } = self;
        tokens.extend(quote! { ::system2::__private::bbox(#instructions) });
    }
}

#[cfg(test)]
mod tests {
    use super::Bbox;
    use quote::{ToTokens, quote};

    #[test]
    fn to_tokens_gives_the_instructions() {
        let bbox: Bbox = syn::parse2(quote!("The mug.",)).unwrap();

        let instructions = quote! {
            ::system2::__private::Json::String(::std::borrow::Cow::Borrowed(
                ::system2::__private::unescaped(::core::format_args!("The mug."))
            ))
        };
        assert_eq!(
            bbox.to_token_stream().to_string(),
            quote!(::system2::__private::bbox(#instructions)).to_string()
        );
    }

    #[test]
    fn parse_refuses_bad_input() {
        let cases = [
            (
                quote!(),
                "`bbox!` takes the instructions: a string literal, a constant, or a JSON literal",
            ),
            (quote!(""), "the instructions must not be empty"),
            (
                quote!("{thing}"),
                "the instructions must have text outside the placeholders: \
                 for text from data, use `Bbox::new`",
            ),
            (
                quote!("The {}.", thing),
                "`bbox!` takes the instructions, and nothing more: \
                 to fill in the instructions, name each value in a placeholder, as in `{name}`",
            ),
        ];
        for (tokens, message) in cases {
            let error = syn::parse2::<Bbox>(tokens.clone()).err().unwrap();
            assert_eq!(error.to_string(), message, "{tokens}");
        }
    }
}