diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6fa2b30 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,132 @@ +on: + push: + branches: + - master + pull_request: + branches: + - '**' + +name: CI + +jobs: + audit: + name: Audit + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: rustfmt + + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + components: clippy + + - name: Install Clippy + run: rustup component add clippy + + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + + coverage: + name: Coverage + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly + override: true + + - name: tarpaulin Cache + id: tarpaulin_cache + uses: actions/cache@v2 + with: + path: ~/.cargo/bin/cargo-tarpaulin + key: ${{ runner.os }}-cargo-tarpaulin + + - uses: actions-rs/cargo@v1 + if: steps.tarpaulin_cache.outputs.cache-hit != 'true' + with: + command: install + args: cargo-tarpaulin + + - name: 'Run `cargo-tarpaulin`' + uses: actions-rs/cargo@v1 + with: + command: tarpaulin + args: --workspace + + - name: Upload to codecov.io + uses: codecov/codecov-action@v1 + with: + file: ./target/tarpaulin/cobertura.xml + + build_and_test_linux: + name: Build and Test (Linux) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions-rs/cargo@v1 + with: + command: test + args: --release + + build_and_test_windows: + name: Build and Test (Windows) + runs-on: windows-latest + timeout-minutes: 10 + steps: + - name: Prepare symlink configuration + run: git config --global core.symlinks true + + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions-rs/cargo@v1 + with: + command: test + args: --release diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c1dc309..0000000 --- a/.travis.yml +++ /dev/null @@ -1,41 +0,0 @@ -language: rust - -sudo: required - -rust: - - stable - - nightly - -matrix: - allow_failures: - - rust: nightly - -addons: - apt: - packages: - - libcurl4-openssl-dev - - libelf-dev - - libdw-dev - - cmake - - gcc - - binutils-dev - - libiberty-dev - -after_success: | - if [ "${TRAVIS_RUST_VERSION}" == "stable" ] - then - wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz && - tar xzf master.tar.gz && - cd kcov-master && - mkdir build && - cd build && - cmake .. && - make && - make install DESTDIR=../../kcov-build && - cd ../.. && - rm -rf kcov-master && - export PATH="${PATH}:$(pwd)/kcov-build/usr/local/bin/" && - ./scripts/coverage.sh && - bash <(curl -s https://codecov.io/bash) && - echo "Uploaded code coverage" - fi diff --git a/Cargo.toml b/Cargo.toml index 1117e56..72ccce6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,15 +15,10 @@ path = "src/lib.rs" proc-macro = true [dependencies] -proc-macro2 = "1.0.26" +proc-macro2 = "1.0.34" proc_macro_roids = "0.7.0" -quote = "1.0.9" -syn = { version = "1.0.69", features = ["extra-traits", "visit"] } +quote = "1.0.10" +syn = { version = "1.0.82", features = ["extra-traits", "visit"] } [dev-dependencies] -pretty_assertions = "0.6.1" - -[badges] -appveyor = { repository = "azriel91/enum_variant_type" } -travis-ci = { repository = "azriel91/enum_variant_type" } -codecov = { repository = "azriel91/enum_variant_type" } +pretty_assertions = "1.0.0" diff --git a/README.md b/README.md index f11df25..d0ffcc8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Crates.io](https://img.shields.io/crates/v/enum_variant_type.svg)](https://crates.io/crates/enum_variant_type) -[![Build Status](https://ci.appveyor.com/api/projects/status/github/azriel91/enum_variant_type?branch=master&svg=true)](https://ci.appveyor.com/project/azriel91/enum_variant_type/branch/master) -[![Build Status](https://travis-ci.org/azriel91/enum_variant_type.svg?branch=master)](https://travis-ci.org/azriel91/enum_variant_type) -[![Coverage Status](https://codecov.io/gh/azriel91/enum_variant_type/branch/master/graph/badge.svg)](https://codecov.io/gh/azriel91/enum_variant_type) +[![docs.rs](https://img.shields.io/docsrs/enum_variant_type)](https://docs.rs/enum_variant_type) +[![CI](https://github.com/azriel91/enum_variant_type/workflows/CI/badge.svg)](https://github.com/azriel91/enum_variant_type/actions/workflows/ci.yml) +[![Coverage Status](https://codecov.io/gh/azriel91/enum_variant_type/branch/main/graph/badge.svg)](https://codecov.io/gh/azriel91/enum_variant_type) # Enum Variant Type @@ -29,10 +29,7 @@ pub enum MyEnum { Tuple(u32, u64), /// Struct variant. #[evt(derive(Debug))] - Struct { - field_0: u32, - field_1: u64, - }, + Struct { field_0: u32, field_1: u64 }, /// Skipped variant. #[evt(skip)] Skipped, @@ -42,7 +39,11 @@ pub enum MyEnum { use core::convert::TryFrom; let unit: Unit = Unit::try_from(MyEnum::Unit).unwrap(); let tuple: Tuple = Tuple::try_from(MyEnum::Tuple(12, 34)).unwrap(); -let named: Struct = Struct::try_from(MyEnum::Struct { field_0: 12, field_1: 34 }).unwrap(); +let named: Struct = Struct::try_from(MyEnum::Struct { + field_0: 12, + field_1: 34, +}) +.unwrap(); let enum_unit = MyEnum::from(unit); let enum_tuple = MyEnum::from(tuple); @@ -144,6 +145,12 @@ impl TryFrom for Struct { +#### Additional options specified by an `evt` attribute on enum: + +* `#[evt(derive(Clone, Copy))]`: Derives `Clone`, `Copy` on **every** variant. +* `#[evt(module = "module1")]`: Generated structs are placed into `mod module1 { ... }`. +* `#[evt(implement_marker_traits(MarkerTrait1))]`: Generated structs all `impl MarkerTrait1`. + ## License Licensed under either of diff --git a/README.tpl b/README.tpl index 7ec5be5..6c45feb 100644 --- a/README.tpl +++ b/README.tpl @@ -1,5 +1,7 @@ [![Crates.io](https://img.shields.io/crates/v/enum_variant_type.svg)](https://crates.io/crates/enum_variant_type) -{{badges}} +[![docs.rs](https://img.shields.io/docsrs/enum_variant_type)](https://docs.rs/enum_variant_type) +[![CI](https://github.com/azriel91/enum_variant_type/workflows/CI/badge.svg)](https://github.com/azriel91/enum_variant_type/actions/workflows/ci.yml) +[![Coverage Status](https://codecov.io/gh/azriel91/enum_variant_type/branch/main/graph/badge.svg)](https://codecov.io/gh/azriel91/enum_variant_type) # Enum Variant Type diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 50e29e9..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,121 +0,0 @@ -# Appveyor configuration template for Rust using rustup for Rust installation -# https://github.com/starkat99/appveyor-rust - -## Operating System (VM environment) ## - -# Rust needs at least Visual Studio 2013 Appveyor OS for MSVC targets. -os: Visual Studio 2015 - -## Build Matrix ## - -# This configuration will setup a build for each channel & target combination (12 windows -# combinations in all). -# -# There are 3 channels: stable, beta, and nightly. -# -# Alternatively, the full version may be specified for the channel to build using that specific -# version (e.g. channel: 1.5.0) -# -# The values for target are the set of windows Rust build targets. Each value is of the form -# -# ARCH-pc-windows-TOOLCHAIN -# -# Where ARCH is the target architecture, either x86_64 or i686, and TOOLCHAIN is the linker -# toolchain to use, either msvc or gnu. See https://www.rust-lang.org/downloads.html#win-foot for -# a description of the toolchain differences. -# See https://github.com/rust-lang-nursery/rustup.rs/#toolchain-specification for description of -# toolchains and host triples. -# -# Comment out channel/target combos you do not wish to build in CI. -# -# You may use the `cargoflags` and `RUSTFLAGS` variables to set additional flags for cargo commands -# and rustc, respectively. For instance, you can uncomment the cargoflags lines in the nightly -# channels to enable unstable features when building for nightly. Or you could add additional -# matrix entries to test different combinations of features. -environment: - matrix: - -### MSVC Toolchains ### - - # Stable 64-bit MSVC - - channel: stable - target: x86_64-pc-windows-msvc - # # Stable 32-bit MSVC - # - channel: stable - # target: i686-pc-windows-msvc - # # Beta 64-bit MSVC - # - channel: beta - # target: x86_64-pc-windows-msvc - # # Beta 32-bit MSVC - # - channel: beta - # target: i686-pc-windows-msvc - # Nightly 64-bit MSVC - - channel: nightly - target: x86_64-pc-windows-msvc - #cargoflags: --features "unstable" - # # Nightly 32-bit MSVC - # - channel: nightly - # target: i686-pc-windows-msvc - # #cargoflags: --features "unstable" - -### GNU Toolchains ### - - # # Stable 64-bit GNU - # - channel: stable - # target: x86_64-pc-windows-gnu - # # Stable 32-bit GNU - # - channel: stable - # target: i686-pc-windows-gnu - # # Beta 64-bit GNU - # - channel: beta - # target: x86_64-pc-windows-gnu - # # Beta 32-bit GNU - # - channel: beta - # target: i686-pc-windows-gnu - # # Nightly 64-bit GNU - # - channel: nightly - # target: x86_64-pc-windows-gnu - # #cargoflags: --features "unstable" - # # Nightly 32-bit GNU - # - channel: nightly - # target: i686-pc-windows-gnu - # #cargoflags: --features "unstable" - -### Allowed failures ### - -# See Appveyor documentation for specific details. In short, place any channel or targets you wish -# to allow build failures on (usually nightly at least is a wise choice). This will prevent a build -# or test failure in the matching channels/targets from failing the entire build. -matrix: - allow_failures: - - channel: nightly - -# If you only care about stable channel build failures, uncomment the following line: - #- channel: beta - -## Install Script ## - -# This is the most important part of the Appveyor configuration. This installs the version of Rust -# specified by the 'channel' and 'target' environment variables from the build matrix. This uses -# rustup to install Rust. -# -# For simple configurations, instead of using the build matrix, you can simply set the -# default-toolchain and default-host manually here. -install: - - appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe - - rustup-init -yv --default-toolchain %channel% --default-host %target% - - set PATH=%PATH%;%USERPROFILE%\.cargo\bin - - rustc -vV - - cargo -vV - -## Build Script ## - -# 'cargo test' takes care of building for us, so disable Appveyor's build stage. This prevents -# the "directory does not contain a project or solution file" error. -build: false - -# Uses 'cargo test' to run tests and build. Alternatively, the project may call compiled programs -#directly or perform other testing commands. Rust will automatically be placed in the PATH -# environment variable. -test_script: - - cargo test --verbose %cargoflags% diff --git a/src/lib.rs b/src/lib.rs index 1531fc3..45df781 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(missing_debug_implementations, missing_docs)] // kcov-ignore +#![deny(missing_debug_implementations, missing_docs)] #![no_std] #![recursion_limit = "128"] @@ -26,10 +26,7 @@ //! Tuple(u32, u64), //! /// Struct variant. //! #[evt(derive(Debug))] -//! Struct { -//! field_0: u32, -//! field_1: u64, -//! }, +//! Struct { field_0: u32, field_1: u64 }, //! /// Skipped variant. //! #[evt(skip)] //! Skipped, @@ -39,7 +36,11 @@ //! use core::convert::TryFrom; //! let unit: Unit = Unit::try_from(MyEnum::Unit).unwrap(); //! let tuple: Tuple = Tuple::try_from(MyEnum::Tuple(12, 34)).unwrap(); -//! let named: Struct = Struct::try_from(MyEnum::Struct { field_0: 12, field_1: 34 }).unwrap(); +//! let named: Struct = Struct::try_from(MyEnum::Struct { +//! field_0: 12, +//! field_1: 34, +//! }) +//! .unwrap(); //! //! let enum_unit = MyEnum::from(unit); //! let enum_tuple = MyEnum::from(tuple); @@ -140,16 +141,24 @@ //! ``` //! //! +//! +//! ### Additional options specified by an `evt` attribute on enum: +//! +//! * `#[evt(derive(Clone, Copy))]`: Derives `Clone`, `Copy` on **every** variant. +//! * `#[evt(module = "module1")]`: Generated structs are placed into `mod module1 { ... }`. +//! * `#[evt(implement_marker_traits(MarkerTrait1))]`: Generated structs all `impl MarkerTrait1`. extern crate alloc; extern crate proc_macro; use alloc::vec::Vec; use proc_macro::TokenStream; +use proc_macro2::{Ident, Span}; use proc_macro_roids::{namespace_parameters, FieldsExt}; use quote::quote; use syn::{ - parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, Path, + parse_macro_input, parse_quote, Attribute, Data, DataEnum, DeriveInput, Field, Fields, Lit, + Meta, NestedMeta, Path, }; /// Attributes that should be copied across. @@ -158,6 +167,7 @@ const ATTRIBUTES_TO_COPY: &[&str] = &["doc", "cfg", "allow", "deny"]; /// Derives a struct for each enum variant. /// /// Struct fields including their attributes are copied over. +#[cfg(not(tarpaulin_include))] #[proc_macro_derive(EnumVariantType, attributes(evt))] pub fn enum_variant_type(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -174,6 +184,58 @@ fn enum_variant_type_impl(ast: DeriveInput) -> proc_macro2::TokenStream { let data_enum = data_enum(&ast); let variants = &data_enum.variants; + let mut wrap_in_module = None::; + let mut derive_for_all_variants = None::; + let mut marker_trait_paths = Vec::::new(); + + for attr in ast.attrs.iter() { + if attr.path.is_ident("evt") { + if let Ok(Meta::List(list)) = attr.parse_meta() { + for item in list.nested.iter() { + match item { + NestedMeta::Meta(Meta::NameValue(name_value)) => { + if let (true, Lit::Str(lit_str)) = + (name_value.path.is_ident("module"), &name_value.lit) + { + wrap_in_module = + Some(Ident::new(&lit_str.value(), Span::call_site())); + } else { + panic!("Expected `evt` attribute argument in the form: `#[evt(module = \"some_module_name\")]`"); + } + } + NestedMeta::Meta(Meta::List(list)) => { + if list.path.is_ident("derive") { + let items = list.nested.iter().map(|nested_meta| { + if let NestedMeta::Meta(Meta::Path(path)) = nested_meta { + path.clone() + } else { + panic!("Expected `evt` attribute argument in the form: `#[evt(derive(Clone, Debug))]`"); + } + }); + derive_for_all_variants = Some(parse_quote! { + #[derive( #(#items),* )] + }); + } else if list.path.is_ident("implement_marker_traits") { + marker_trait_paths = list.nested + .iter() + .map(|nested| if let NestedMeta::Meta(Meta::Path(path)) = nested { + path.clone() + } else { + panic!("Expected `evt` attribute argument in the form #[evt(implement_marker_traits(MarkerTrait1, MarkerTrait2))]"); + }).collect(); + } + } + _ => { + panic!("Unexpected usage of `evt` attribute, please see examples at:\n") + } + } + } + } else { + panic!("Unexpected usage of `evt` attribute, please see examples at:\n") + } + } + } + let mut struct_declarations = proc_macro2::TokenStream::new(); let ns: Path = parse_quote!(evt); @@ -272,23 +334,37 @@ fn enum_variant_type_impl(ast: DeriveInput) -> proc_macro2::TokenStream { quote! { #(#attrs_to_copy)* + #derive_for_all_variants #variant_struct_attrs #vis #data_struct #impl_from_variant_for_enum #impl_try_from_enum_for_variant + + #(impl #ty_generics #marker_trait_paths for #variant_name #ty_generics {})* } }); struct_declarations.extend(struct_declarations_iter); - struct_declarations + + if let Some(module_to_wrap_in) = wrap_in_module { + quote! { + #vis mod #module_to_wrap_in { + use super::#enum_name; + + #struct_declarations + } + } + } else { + struct_declarations + } } fn data_enum(ast: &DeriveInput) -> &DataEnum { if let Data::Enum(data_enum) = &ast.data { data_enum } else { - panic!("`EnumVariantType` derive can only be used on an enum."); // kcov-ignore + panic!("`EnumVariantType` derive can only be used on an enum."); } } @@ -433,4 +509,178 @@ mod tests { assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); } + + #[test] + fn put_variants_in_module() { + let ast: DeriveInput = parse_quote! { + #[evt(module = "example")] + pub enum MyEnum { + A, + B + } + }; + + let actual_tokens = enum_variant_type_impl(ast); + let expected_tokens = quote! { + pub mod example { + use super::MyEnum; + + pub struct A; + + impl core::convert::From for MyEnum { + fn from(variant_struct: A) -> Self { + MyEnum::A + } + } + + impl core::convert::TryFrom for A { + type Error = MyEnum; + fn try_from(enum_variant: MyEnum) -> Result { + if let MyEnum::A = enum_variant { + core::result::Result::Ok(A) + } else { + core::result::Result::Err(enum_variant) + } + } + } + + pub struct B; + + impl core::convert::From for MyEnum { + fn from(variant_struct: B) -> Self { + MyEnum::B + } + } + + impl core::convert::TryFrom for B { + type Error = MyEnum; + fn try_from(enum_variant: MyEnum) -> Result { + if let MyEnum::B = enum_variant { + core::result::Result::Ok(B) + } else { + core::result::Result::Err(enum_variant) + } + } + } + } + }; + + assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); + } + + #[test] + fn derive_traits_for_all_variants() { + let ast: DeriveInput = parse_quote! { + #[evt(derive(Debug))] + pub enum MyEnum { + A, + #[evt(derive(Clone))] + B + } + }; + + let actual_tokens = enum_variant_type_impl(ast); + let expected_tokens = quote! { + #[derive(Debug)] + pub struct A; + + impl core::convert::From for MyEnum { + fn from(variant_struct: A) -> Self { + MyEnum::A + } + } + + impl core::convert::TryFrom for A { + type Error = MyEnum; + fn try_from(enum_variant: MyEnum) -> Result { + if let MyEnum::A = enum_variant { + core::result::Result::Ok(A) + } else { + core::result::Result::Err(enum_variant) + } + } + } + + #[derive(Debug)] + #[derive(Clone)] + pub struct B; + + impl core::convert::From for MyEnum { + fn from(variant_struct: B) -> Self { + MyEnum::B + } + } + + impl core::convert::TryFrom for B { + type Error = MyEnum; + fn try_from(enum_variant: MyEnum) -> Result { + if let MyEnum::B = enum_variant { + core::result::Result::Ok(B) + } else { + core::result::Result::Err(enum_variant) + } + } + } + }; + + assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); + } + + #[test] + fn derive_marker_trait() { + let ast: DeriveInput = parse_quote! { + #[evt(implement_marker_traits(MarkerTrait1))] + pub enum MyEnum { + A, + B + } + }; + + let actual_tokens = enum_variant_type_impl(ast); + let expected_tokens = quote! { + pub struct A; + + impl core::convert::From for MyEnum { + fn from(variant_struct: A) -> Self { + MyEnum::A + } + } + + impl core::convert::TryFrom for A { + type Error = MyEnum; + fn try_from(enum_variant: MyEnum) -> Result { + if let MyEnum::A = enum_variant { + core::result::Result::Ok(A) + } else { + core::result::Result::Err(enum_variant) + } + } + } + + impl MarkerTrait1 for A {} + + pub struct B; + + impl core::convert::From for MyEnum { + fn from(variant_struct: B) -> Self { + MyEnum::B + } + } + + impl core::convert::TryFrom for B { + type Error = MyEnum; + fn try_from(enum_variant: MyEnum) -> Result { + if let MyEnum::B = enum_variant { + core::result::Result::Ok(B) + } else { + core::result::Result::Err(enum_variant) + } + } + } + + impl MarkerTrait1 for B {} + }; + + assert_eq!(expected_tokens.to_string(), actual_tokens.to_string()); + } } diff --git a/tarpaulin.toml b/tarpaulin.toml new file mode 100644 index 0000000..bfad298 --- /dev/null +++ b/tarpaulin.toml @@ -0,0 +1,5 @@ +[default] + +[report] +out = ["Html", "Xml"] +output-dir = "target/tarpaulin"