diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 0000000..9fd45e0 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,22 @@ +name: Rust + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..95ceaf6 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,323 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "nested_enum_utils" +version = "0.2.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "trybuild", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.68", +] + +[[package]] +name = "serde_json" +version = "1.0.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "toml" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.14", +] + +[[package]] +name = "toml_datetime" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.13", +] + +[[package]] +name = "trybuild" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a5f13f11071020bb12de7a16b925d2d58636175c20c11dc5f96cb64bb6c9b3" +dependencies = [ + "glob", + "serde", + "serde_derive", + "serde_json", + "termcolor", + "toml", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "winapi-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..94631a5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nested_enum_utils" +version = "0.2.0" +edition = "2024" +readme = "README.md" +description = "Macros to provide conversions for nested enums" +license = "MIT OR Apache-2.0" +authors = ["RĂ¼diger Klaehn "] +repository = "https://github.com/n0-computer/nested-enum-utils" +keywords = ["enum", "conversion", "macros", "protocol"] + +[dependencies] +proc-macro-crate = "3.1.0" +proc-macro2 = "1.0" +quote = "1.0" +syn = { version = "1.0", features = ["full"] } + +[lib] +proc-macro = true + +[dev-dependencies] +trybuild = "1.0.96" diff --git a/README.md b/README.md new file mode 100644 index 0000000..30d8c03 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Nested enum utils + +This crate provides a single attribute macro to provide conversions from enum cases to the enum itself or to some other type. + +It only works with enums where each variant has a single unnamed element, and if each variant has a distinct type. + +The most basic use is to provide conversions between the enum cases and the enum type itself. You could achieve something similar with the popular [derive_more] crate. + +```rust +#[enum_conversions()] +enum Request { + Get(GetRequest), + Put(PutRequest), +} +``` + +A more advanced use, and the reason for this crate to exist, is to provide conversions between enum variants and any type that itself has a *conversion* to the enum. This allows to use nested enums, like you would in a complex protocol that has several subsystems. + +```rust +#[enum_conversions(Request)] +enum StoreRequest { + Get(GetRequest), + Put(PutRequest), +} + +#[enum_conversions(Request)] +enum NetworkRequest { + Ping(PingRequest), +} + +#[enum_conversions()] +enum Request { + Store(StoreRequest), + Network(NetworkRequest), +} +``` + +Here we define conversions from `GetRequest` to `StoreRequest`, from `StoreRequest` to `Request`, and then directly from `GetRequest` to `Request`, and corresponding [TryFrom] conversions in the other direction. + +## Generated conversions + +The generated [From] conversions are straightforward. Obviously it is always possible to convert from an enum case to the enum itself. + +We also generate [TryFrom] conversions from the enum to each variant, as well as from a reference to the enum to a reference to the variant. + +The conversions that take a value are different than the ones from [derive_more]: they return the unmodified input in the error case, allowing to chain conversion attempts. + +```rust +let request = ... +match GetRequest::try_from(request) { + Ok(get) => // handle get request + Err(request) => { + // I still got the request and can try something else + match PutRequest::try_from(request) { + ... + } + } +} +``` + +The conversions that take a reference just return a `&'static str` as the error type. References are `Copy`, so we can always retry anyway. + +[From]: https://doc.rust-lang.org/std/convert/trait.From.html +[TryFrom]: https://doc.rust-lang.org/std/convert/trait.TryFrom.html +[derive_more]: https://crates.io/crates/derive_more \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..02895fb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,345 @@ +use std::collections::BTreeSet; + +use proc_macro::TokenStream; +use proc_macro2::{Literal, TokenStream as TokenStream2}; +use quote::{ToTokens, quote}; +use syn::{ + Data, DeriveInput, Fields, Ident, ItemEnum, ItemStruct, Token, Type, Variant, braced, + parse::{Parse, ParseStream}, + parse_macro_input, + punctuated::Punctuated, +}; + +fn extract_enum_variants(input: &DeriveInput) -> syn::Result> { + let mut distinct_types = BTreeSet::new(); + let Data::Enum(data_enum) = &input.data else { + return Err(syn::Error::new_spanned( + input, + "EnumConversions can only be used with enums", + )); + }; + data_enum.variants.iter().map(|variant: &Variant| { + let variant_name = &variant.ident; + match &variant.fields { + Fields::Unnamed(fields) if fields.unnamed.len() == 1 => { + let field_type = &fields.unnamed.first().unwrap().ty; + if !distinct_types.insert(field_type.to_token_stream().to_string()) { + return Err(syn::Error::new_spanned( + field_type, + "EnumConversions only works with enums that have unnamed single fields of distinct types" + )); + } + Ok((variant_name, field_type)) + }, + _ => Err(syn::Error::new_spanned( + variant, + "EnumConversions only works with enums that have unnamed single fields" + )) + } + }).collect() +} + +fn generate_enum_self_conversions(enum_name: &Ident, variants: &[(&Ident, &Type)]) -> TokenStream2 { + let mut conversions = TokenStream2::new(); + + for (variant_name, field_type) in variants { + // Generate From for Enum + let from_impl = quote! { + impl From<#field_type> for #enum_name { + fn from(value: #field_type) -> Self { + #enum_name::#variant_name(value) + } + } + }; + conversions.extend(from_impl); + + // Generate TryFrom for FieldType + // + // This is a self conversion, so it case it does not work we want to return the original value + let try_from_impl = quote! { + impl TryFrom<#enum_name> for #field_type { + type Error = #enum_name; + + fn try_from(value: #enum_name) -> Result { + match value { + #enum_name::#variant_name(inner) => Ok(inner), + x => Err(x), + } + } + } + }; + conversions.extend(try_from_impl); + + // Generate TryFrom for &FieldType + let try_from_ref_impl = quote! { + impl<'a> TryFrom<&'a #enum_name> for &'a #field_type { + type Error = &'a #enum_name; + + fn try_from(value: &'a #enum_name) -> Result { + match value { + #enum_name::#variant_name(inner) => Ok(inner), + _ => Err(value), + } + } + } + }; + conversions.extend(try_from_ref_impl); + } + + conversions +} + +fn generate_enum_target_conversions( + enum_name: &Ident, + target_type: &Type, + variants: &[(&Ident, &Type)], +) -> TokenStream2 { + let mut conversions = TokenStream2::new(); + + for (variant_name, field_type) in variants { + // Generate From for TargetType + let from_impl = quote! { + impl From<#field_type> for #target_type { + fn from(value: #field_type) -> Self { + let enum_value = #enum_name::#variant_name(value); + Self::from(enum_value) + } + } + }; + conversions.extend(from_impl); + + // Generate TryFrom for FieldType + // + // This is a self conversion, so it case it does not work we want to return the original value + let try_from_impl = quote! { + impl TryFrom<#target_type> for #field_type { + type Error = #target_type; + + fn try_from(value: #target_type) -> Result { + match #enum_name::try_from(value) { + Ok(#enum_name::#variant_name(inner)) => Ok(inner), + Ok(x) => Err(#target_type::from(x)), + Err(x) => Err(x), + } + } + } + }; + conversions.extend(try_from_impl); + + // Generate TryFrom<&TargetType> for &FieldType + // + // This is a self conversion, so it case it does not work we want to return the original value + let try_from_ref_impl = quote! { + impl<'a> TryFrom<&'a #target_type> for &'a #field_type { + type Error = &'a #target_type; + + fn try_from(value: &'a #target_type) -> Result { + match <&'a #enum_name>::try_from(value) { + Ok(#enum_name::#variant_name(inner)) => Ok(inner), + Ok(_) => Err(value), + Err(_) => Err(value), + } + } + } + }; + conversions.extend(try_from_ref_impl); + } + + conversions +} + +struct EnumConversionsArgs { + target_types: Punctuated, +} + +impl Parse for EnumConversionsArgs { + fn parse(input: ParseStream) -> syn::Result { + Ok(EnumConversionsArgs { + target_types: Punctuated::parse_terminated(input)?, + }) + } +} + +/// Derive macro that generates conversions between an enum and its variants and other types. +/// +/// The macro can be used as follows: +/// +/// ```rust +/// use nested_enum_utils::enum_conversions; +/// +/// #[enum_conversions()] +/// enum MyEnum { +/// Variant1(u32), +/// Variant2(String), +/// } +/// ``` +/// +/// This will create From instances from each variant type to the enum and TryFrom instances from the enum to each variant type. +/// +/// The macro also accepts a list of target types to generate conversions to: +/// +/// ```rust +/// use nested_enum_utils::enum_conversions; +/// +/// #[enum_conversions(Outer)] +/// enum Inner { +/// Variant1(u32), +/// Variant2(String), +/// } +/// +/// #[enum_conversions()] +/// enum Outer { +/// Inner1(Inner), +/// // other variants +/// } +/// ``` +/// +/// This will, in addition, generate From instances from each variant type to the outer enum and TryFrom instances from the outer enum to each variant type. +/// The conversion to the outer enum relies on conversions between the inner enum and the outer enum, which is provided by the +/// enum_conversions attribute on the Outer enum. +/// +/// Limitations: +/// +/// - enums must have unnamed single fields +/// - field types must be distinct +#[proc_macro_attribute] +pub fn enum_conversions(attr: TokenStream, item: TokenStream) -> TokenStream { + let args = parse_macro_input!(attr as EnumConversionsArgs); + let input = parse_macro_input!(item as DeriveInput); + + let enum_name = &input.ident; + + let variants = match extract_enum_variants(&input) { + Ok(v) => v, + Err(e) => return e.to_compile_error().into(), + }; + + let mut all_conversions = TokenStream2::new(); + + // Generate self-conversions + all_conversions.extend(generate_enum_self_conversions(enum_name, &variants)); + + // Generate conversions for each target type + for target_type in args.target_types { + all_conversions.extend(generate_enum_target_conversions( + enum_name, + &target_type, + &variants, + )); + } + + let expanded = quote! { + #input + #all_conversions + }; + TokenStream::from(expanded) +} + +// Custom struct to parse arbitrary content inside the attribute brackets +struct CommonCode { + content: TokenStream2, +} + +impl Parse for CommonCode { + fn parse(input: ParseStream) -> syn::Result { + // Parse everything between the braces as a raw token stream + let content; + braced!(content in input); + let content = content.parse()?; + Ok(CommonCode { content }) + } +} + +/// Usage example: +/// +/// #[common_fields({ +/// /// Common size field for all variants +/// #[serde(default)] +/// pub size: u64 +/// })] +/// enum Test { +/// A { } +/// B { x: bool } +/// } +/// +/// Becomes: +/// +/// enum Test { +/// A { +/// /// Common size field for all variants +/// #[serde(default)] +/// pub size: u64 +/// } +/// B { +/// x: bool, +/// /// Common size field for all variants +/// #[serde(default)] +/// pub size: u64 +/// } +/// } +#[proc_macro_attribute] +pub fn common_fields(attr: TokenStream, item: TokenStream) -> TokenStream { + // Parse the common code from the attribute + let common_code = parse_macro_input!(attr as CommonCode); + let common_fields_tokens = common_code.content; + + // Parse the input enum + let mut input_enum = parse_macro_input!(item as ItemEnum); + + // Parse common fields by creating a temporary struct + let temp_struct_tokens = quote! { + struct TempStruct { + #common_fields_tokens + } + }; + + // Parse the temporary struct + let temp_struct: Result = syn::parse2(temp_struct_tokens); + + // Check for parsing errors + if let Err(err) = temp_struct { + // Create a literal from the error message string + let error_string = err.to_string(); + let error_lit = Literal::string(&error_string); + + return TokenStream::from(quote! { + compile_error!(#error_lit); + }); + } + + // Unwrap the struct now that we know it's Ok + let temp_struct = temp_struct.unwrap(); + + // Extract fields from the temporary struct + let common_fields = match temp_struct.fields { + Fields::Named(named) => named.named, + _ => { + let error_lit = Literal::string("Expected named fields in common code block"); + return TokenStream::from(quote! { + compile_error!(#error_lit); + }); + } + }; + + // Process each variant of the enum + for variant in &mut input_enum.variants { + // We only care about struct variants (named fields) + if let Fields::Named(ref mut fields) = variant.fields { + // Add each common field to this variant + for field in common_fields.iter() { + fields.named.push(field.clone()); + } + } else { + let error_lit = Literal::string("Expected named variants in enum"); + return TokenStream::from(quote! { + compile_error!(#error_lit); + }); + } + } + + // Return the updated enum + quote! { + #input_enum + } + .into() +} diff --git a/tests/basic.rs b/tests/basic.rs new file mode 100644 index 0000000..baa569e --- /dev/null +++ b/tests/basic.rs @@ -0,0 +1,96 @@ +use nested_enum_utils::{common_fields, enum_conversions}; + +#[test] +fn test_single_enum() { + #[derive(Debug)] + #[enum_conversions] + enum Test { + A(u32), + B(String), + } + + // convert from leaf to enum + let e: Test = 42u32.into(); + // convert from enum to leaf by reference + let lr: &u32 = (&e).try_into().unwrap(); + assert_eq!(*lr, 42); + // convert from enum to leaf by value + let l: u32 = e.try_into().unwrap(); + assert_eq!(l, 42); +} + +#[test] +fn test_nested_enums() { + #[derive(Debug)] + #[enum_conversions(Outer)] + enum Inner { + A(u32), + B(u8), + } + + #[derive(Debug)] + #[enum_conversions] + enum Outer { + A(Inner), + B(String), + } + + // convert from leaf to outer + let e: Outer = 42u32.into(); + // convert from outer to leaf by reference + let lr: &u32 = (&e).try_into().unwrap(); + assert_eq!(*lr, 42); + // convert from outer to leaf by value + let l: u32 = e.try_into().unwrap(); + assert_eq!(l, 42); +} + +#[test] +fn test_deeply_nested_enums() { + #[derive(Debug)] + #[enum_conversions(Outer)] + enum Inner { + A(u32), + B(u8), + } + + #[derive(Debug)] + #[enum_conversions(Outer)] + enum Mid { + A(Inner), + B(String), + } + + #[derive(Debug)] + #[enum_conversions] + enum Outer { + A(Mid), + B(f32), + } + + // convert from leaf to outer + let e: Outer = 42u32.into(); + // convert from outer to leaf by reference + let lr: &u32 = (&e).try_into().unwrap(); + assert_eq!(*lr, 42); + // convert from outer to leaf by value + let l: u32 = e.try_into().unwrap(); + assert_eq!(l, 42); +} + +#[test] +fn compile_fail() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compile_fail/*.rs"); +} + +#[test] +fn test_common_fields() { + #[common_fields({ id: u64 })] + #[allow(dead_code)] + enum Test { + A { x: u32 }, + B { y: String }, + } + let _v = Test::A { x: 42, id: 1 }; +} diff --git a/tests/compile_fail/duplicate_type.rs b/tests/compile_fail/duplicate_type.rs new file mode 100644 index 0000000..e9cbb44 --- /dev/null +++ b/tests/compile_fail/duplicate_type.rs @@ -0,0 +1,10 @@ +use nested_enum_utils::enum_conversions; + +#[derive(Debug)] +#[enum_conversions(Outer)] +enum Enum { + A(u8), + B(u8), +} + +fn main() {} diff --git a/tests/compile_fail/duplicate_type.stderr b/tests/compile_fail/duplicate_type.stderr new file mode 100644 index 0000000..11f982f --- /dev/null +++ b/tests/compile_fail/duplicate_type.stderr @@ -0,0 +1,38 @@ +error: EnumConversions only works with enums that have unnamed single fields of distinct types + --> tests/compile_fail/duplicate_type.rs:7:7 + | +7 | B(u8), + | ^^ + +error[E0412]: cannot find type `Enum` in this scope + --> tests/compile_fail/duplicate_type.rs:3:10 + | +3 | #[derive(Debug)] + | ^^^^^ not found in this scope + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | + +error[E0433]: failed to resolve: use of undeclared type `Enum` + --> tests/compile_fail/duplicate_type.rs:6:5 + | +6 | A(u8), + | ^^^^^ use of undeclared type `Enum` + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | + +error[E0433]: failed to resolve: use of undeclared type `Enum` + --> tests/compile_fail/duplicate_type.rs:7:5 + | +7 | B(u8), + | ^^^^^ use of undeclared type `Enum` + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | diff --git a/tests/compile_fail/multiple_fields.rs b/tests/compile_fail/multiple_fields.rs new file mode 100644 index 0000000..72a255f --- /dev/null +++ b/tests/compile_fail/multiple_fields.rs @@ -0,0 +1,9 @@ +use nested_enum_utils::enum_conversions; + +#[derive(Debug)] +#[enum_conversions(Outer)] +enum Enum { + A(u8, u8), +} + +fn main() {} diff --git a/tests/compile_fail/multiple_fields.stderr b/tests/compile_fail/multiple_fields.stderr new file mode 100644 index 0000000..bd9dfe3 --- /dev/null +++ b/tests/compile_fail/multiple_fields.stderr @@ -0,0 +1,27 @@ +error: EnumConversions only works with enums that have unnamed single fields + --> tests/compile_fail/multiple_fields.rs:6:5 + | +6 | A(u8, u8), + | ^^^^^^^^^ + +error[E0412]: cannot find type `Enum` in this scope + --> tests/compile_fail/multiple_fields.rs:3:10 + | +3 | #[derive(Debug)] + | ^^^^^ not found in this scope + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | + +error[E0433]: failed to resolve: use of undeclared type `Enum` + --> tests/compile_fail/multiple_fields.rs:6:5 + | +6 | A(u8, u8), + | ^^^^^^^^^ use of undeclared type `Enum` + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | diff --git a/tests/compile_fail/named_enum.rs b/tests/compile_fail/named_enum.rs new file mode 100644 index 0000000..c6ba300 --- /dev/null +++ b/tests/compile_fail/named_enum.rs @@ -0,0 +1,9 @@ +use nested_enum_utils::enum_conversions; + +#[derive(Debug)] +#[enum_conversions(Outer)] +enum Enum { + A { value : u8 }, +} + +fn main() {} diff --git a/tests/compile_fail/named_enum.stderr b/tests/compile_fail/named_enum.stderr new file mode 100644 index 0000000..d6b0353 --- /dev/null +++ b/tests/compile_fail/named_enum.stderr @@ -0,0 +1,27 @@ +error: EnumConversions only works with enums that have unnamed single fields + --> tests/compile_fail/named_enum.rs:6:5 + | +6 | A { value : u8 }, + | ^^^^^^^^^^^^^^^^ + +error[E0412]: cannot find type `Enum` in this scope + --> tests/compile_fail/named_enum.rs:3:10 + | +3 | #[derive(Debug)] + | ^^^^^ not found in this scope + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | + +error[E0433]: failed to resolve: use of undeclared type `Enum` + --> tests/compile_fail/named_enum.rs:6:5 + | +6 | A { value : u8 }, + | ^^^^^^^^^^^^^^^^ use of undeclared type `Enum` + | +help: consider importing this struct + | +1 + use syn::token::Enum; + | diff --git a/tests/compile_fail/nested_duplicate_type.rs b/tests/compile_fail/nested_duplicate_type.rs new file mode 100644 index 0000000..58cf703 --- /dev/null +++ b/tests/compile_fail/nested_duplicate_type.rs @@ -0,0 +1,24 @@ +use nested_enum_utils::enum_conversions; + +#[derive(Debug)] +#[enum_conversions(Outer)] +enum Inner1 { + A(u32), + B(u8), +} + +#[derive(Debug)] +#[enum_conversions(Outer)] +enum Inner2 { + A(u32), + B(u8), +} + +#[derive(Debug)] +#[enum_conversions] +enum Outer { + A(Inner1), + B(Inner2), +} + +fn main() {} diff --git a/tests/compile_fail/nested_duplicate_type.stderr b/tests/compile_fail/nested_duplicate_type.stderr new file mode 100644 index 0000000..7682483 --- /dev/null +++ b/tests/compile_fail/nested_duplicate_type.stderr @@ -0,0 +1,65 @@ +error[E0119]: conflicting implementations of trait `From` for type `Outer` + --> tests/compile_fail/nested_duplicate_type.rs:11:1 + | +4 | #[enum_conversions(Outer)] + | ------------------------ first implementation here +... +11 | #[enum_conversions(Outer)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Outer` + | + = note: this error originates in the attribute macro `enum_conversions` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `From` for type `Outer` + --> tests/compile_fail/nested_duplicate_type.rs:11:1 + | +4 | #[enum_conversions(Outer)] + | ------------------------ first implementation here +... +11 | #[enum_conversions(Outer)] + | ^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `Outer` + | + = note: this error originates in the attribute macro `enum_conversions` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `TryFrom` for type `u32` + --> tests/compile_fail/nested_duplicate_type.rs:11:1 + | +4 | #[enum_conversions(Outer)] + | -------------------------- first implementation here +... +11 | #[enum_conversions(Outer)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `u32` + | + = note: this error originates in the attribute macro `enum_conversions` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `TryFrom<&Outer>` for type `&u32` + --> tests/compile_fail/nested_duplicate_type.rs:11:1 + | +4 | #[enum_conversions(Outer)] + | -------------------------- first implementation here +... +11 | #[enum_conversions(Outer)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&u32` + | + = note: this error originates in the attribute macro `enum_conversions` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `TryFrom` for type `u8` + --> tests/compile_fail/nested_duplicate_type.rs:11:1 + | +4 | #[enum_conversions(Outer)] + | -------------------------- first implementation here +... +11 | #[enum_conversions(Outer)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `u8` + | + = note: this error originates in the attribute macro `enum_conversions` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0119]: conflicting implementations of trait `TryFrom<&Outer>` for type `&u8` + --> tests/compile_fail/nested_duplicate_type.rs:11:1 + | +4 | #[enum_conversions(Outer)] + | -------------------------- first implementation here +... +11 | #[enum_conversions(Outer)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ conflicting implementation for `&u8` + | + = note: this error originates in the attribute macro `enum_conversions` (in Nightly builds, run with -Z macro-backtrace for more info)