nutype_test_util 0.1.3

Ergonomically create newtypes in tests
Documentation
use pretty_assertions::assert_str_eq;
use quote::{ToTokens as _, format_ident};

use super::*;

macro_rules! before {
    ($($input:tt)*) => {{
        // NOTE: using `From` is required, e.g. `nutype_test_util::derive(From)`
        let (input, errors) = do_derive(quote!(From), quote!($($input)*)).unwrap();
        assert!(errors.is_empty(), "proc macro outputted compile errors: {errors:#?}");

        prettyplease::unparse(&syn::parse_file(&input.to_string()).unwrap())
    }}
}

macro_rules! after {
    ($($expected:tt)*) => {
        prettyplease::unparse(&syn::parse_file(&quote!($($expected)*).to_string()).unwrap())
    }
}

#[test]
fn demo() {
    assert_str_eq!(
        before! {
            #[nutype(validate(non_empty))]
            struct Repo(String);
        },
        after! {
            #[nutype(validate(non_empty))]
            struct Repo(String);

            #[cfg(test)]
            impl<T: Into<String>> ::core::convert::From<T> for Repo {
                fn from(value: T) -> Self {
                    let value: String = value.into();
                    Self::try_new(value).unwrap()
                }
            }
        }
    );
    assert_str_eq!(
        before! {
            #[nutype]
            struct PrNumber(NonZeroU32);
        },
        after! {
            #[nutype]
            struct PrNumber(NonZeroU32);

            #[cfg(test)]
            impl<T: Into<u32>> ::core::convert::From<T> for PrNumber {
                fn from(value: T) -> Self {
                    let value: u32 = value.into();
                    Self::new(::core::num::NonZeroU32::new(value).unwrap())
                }
            }
        }
    );
    assert_str_eq!(
        before! {
            #[nutype(validate(non_empty, predicate = is_valid_commit_hash))]
            struct Commit(String);
        },
        after! {
            #[nutype(validate(non_empty, predicate = is_valid_commit_hash))]
            struct Commit(String);

            #[cfg(test)]
            impl<T: Into<String>> ::core::convert::From<T> for Commit {
                fn from(value: T) -> Self {
                    let value: String = value.into();
                    Self::try_new(value).unwrap()
                }
            }
        }
    );
}

/// `NonZero` types require special handling
#[test]
fn non_zero() {
    #[rustfmt::skip]
    let non_zeroable_primitives = &[
        "i8", "i16", "i32", "i64", "i128", "isize",
        "u8", "u16", "u32", "u64", "u128", "usize",
    ];

    for primitive in non_zeroable_primitives {
        /// Type of the constructor used for creating the newtype
        enum ConstructorKind {
            /// Requires to maintain some kind of invariant,
            /// so constructor is fallible
            ///
            /// `Self::try_new(...).unwrap()`
            New,
            /// `Self::new()`
            TryNew,
        }

        /// Type of the `NonZero`
        enum NonZeroTy {
            /// e.g. `NonZero<u8>`
            Generic,
            /// e.g. `NonZeroU8`
            Concrete,
        }

        let check_nonzero_transformation =
            |non_zero_ty: NonZeroTy, constructor_kind: ConstructorKind| {
                let nutype_attr = match constructor_kind {
                    ConstructorKind::New => quote! { #[nutype] },
                    ConstructorKind::TryNew => quote! { #[nutype(validate(predicate = |_| true))] },
                };

                let constructor_fn = match constructor_kind {
                    ConstructorKind::New => quote!(new),
                    ConstructorKind::TryNew => quote!(try_new),
                };

                let primitive_ident = format_ident!("{primitive}");

                let nonzero_type = match non_zero_ty {
                    NonZeroTy::Generic => quote!(NonZero<#primitive_ident>),
                    NonZeroTy::Concrete => {
                        let capitalized = format!(
                            "{}{}",
                            primitive.get(0..1).unwrap().to_ascii_uppercase(),
                            primitive.get(1..).unwrap()
                        );
                        format_ident!("NonZero{capitalized}").to_token_stream()
                    }
                };

                let inner_ctor = match non_zero_ty {
                    NonZeroTy::Generic => quote!(NonZero::<#primitive_ident>::new(value).unwrap()),
                    NonZeroTy::Concrete => quote!(#nonzero_type::new(value).unwrap()),
                };

                let before = before!(
                    #nutype_attr
                    struct A(num::#nonzero_type);
                );

                let unwrap_if_try_new = if let ConstructorKind::TryNew = constructor_kind {
                    quote! { .unwrap() }
                } else {
                    quote!()
                };

                let after = after! {
                    #nutype_attr
                    struct A(num::#nonzero_type);

                    #[cfg(test)]
                    impl<T: Into<#primitive_ident>> ::core::convert::From<T> for A {
                        fn from(value: T) -> Self {
                            let value: #primitive_ident = value.into();
                            Self::#constructor_fn(::core::num::#inner_ctor)#unwrap_if_try_new
                        }
                    }
                };

                eprintln!("{before}");

                assert_str_eq!(before, after);
            };

        check_nonzero_transformation(NonZeroTy::Generic, ConstructorKind::New);
        check_nonzero_transformation(NonZeroTy::Generic, ConstructorKind::TryNew);
        check_nonzero_transformation(NonZeroTy::Concrete, ConstructorKind::New);
        check_nonzero_transformation(NonZeroTy::Concrete, ConstructorKind::TryNew);
    }
}