Skip to content

Commit

Permalink
feat(macros): enumregex and enumname macros
Browse files Browse the repository at this point in the history
  • Loading branch information
DaRacci committed Aug 17, 2023
1 parent 1d791fa commit 124a030
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 9 deletions.
5 changes: 5 additions & 0 deletions crates/macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ quote = "1.0.28"
proc-macro2 = "1.0.63"
downcast-rs = "1.2.0"

# Logging & Errors
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
thiserror = { workspace = true }
anyhow = { workspace = true }
57 changes: 57 additions & 0 deletions crates/macros/src/enums/names.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (C) 2023 James Draycott <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

use crate::enums::variants;
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DataEnum, DeriveInput};

pub fn names(input: DeriveInput) -> TokenStream {
let name = &input.ident;
let data = match &input.data {
Data::Enum(ref data) => data,
other => return crate::error_input::<DataEnum, _>(input.span(), other),
};

let variant_names = variants::impl_names(data);
let lowercase_names = variant_names.iter().map(|v| v.to_string().to_lowercase()).collect::<Vec<String>>();
quote! {
#[automatically_derived]
impl #name {
#[automatically_derived]
pub const fn name(&self) -> &'static str {
match self {
#(#name::#variant_names => stringify!(#variant_names)),*
}
}
}

#[automatically_derived]
impl TryFrom<&str> for #name {
#[automatically_derived]
type Error = anyhow::Error;

#[automatically_derived]
fn try_from(name: &str) -> Result<Self, Self::Error> {
match name.to_lowercase().as_str() {
#(#lowercase_names => Ok(#name::#variant_names)),*,
_ => Err(anyhow::anyhow!("Unknown variant name: {}", name)),
}
}
}
}
}
107 changes: 107 additions & 0 deletions crates/macros/src/enums/regex.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* Copyright (C) 2023 James Draycott <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

use crate::enums::variants;
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DataEnum, DeriveInput};

pub fn regex(input: DeriveInput) -> TokenStream {
let name = &input.ident;
let data = match &input.data {
Data::Enum(ref data) => data,
other => return crate::error_input::<DataEnum, _>(input.span(), other),
};

let variant_names = variants::impl_names(data);
let concatted = variant_names.iter().map(|i| i.to_string()).collect::<Vec<String>>().join("|");
let len = variant_names.len();

quote! {
#[automatically_derived]
impl #name {
/// The regex that matches any single variant of this enum
/// This is case insensitive.
#[automatically_derived]
pub const REGEX: &'static str = concat!(
r"(?:(?i)(?x)",
#concatted,
r")",
);

/// The regex that matches having multiple variants of this enum in a row (e.g. `hourly-daily`)
/// These are separated by a `-`, and will be in the order they are defined in the enum.
#[automatically_derived]
pub const MULTI_REGEX: &'static str = concat!(
r"(?:(?i)(?x)",
#concatted,
r"(?:-",
#concatted,
r"){0,",
#len,
r"})",
);
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use syn::parse_quote;

#[test]
fn test_regex() {
let input = parse_quote! {
enum Test {
A,
B,
C,
}
};

let expected = quote! {
#[automatically_derived]
impl Test {
/// The regex that matches any single variant of this enum
/// This is case insensitive.
#[automatically_derived]
pub const REGEX: &'static str = concat!(
r"(?:(?i)(?x)",
"A|B|C",
r")",
);

/// The regex that matches having multiple variants of this enum in a row (e.g. `hourly-daily`)
/// These are separated by a `-`, and will be in the order they are defined in the enum.
#[automatically_derived]
pub const MULTI_REGEX: &'static str = concat!(
r"(?:(?i)(?x)",
"A|B|C",
r"(?:-",
"A|B|C",
r"){0,",
3usize,
r"})",
);
}
};

let actual = regex(input);
assert_eq!(expected.to_string(), actual.to_string());
}
}
19 changes: 10 additions & 9 deletions crates/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,17 +174,18 @@ pub fn enum_variants(input: TokenStream) -> TokenStream {
enums::variants::variants(input).into()
}

let variant_names = variants.iter().map(|variant| &variant.ident).collect::<Vec<_>>();
#[proc_macro_derive(EnumNames)]
pub fn enum_names(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

let expanded = quote! {
impl #name {
pub fn get_variants() -> Vec<#name> {
vec![#(#name::#variant_names),*]
}
}
};
enums::names::names(input).into()
}

TokenStream::from(expanded)
#[proc_macro_derive(EnumRegex)]
pub fn enum_regex(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);

enums::regex::regex(input).into()
}

#[proc_macro_derive(Delegation, attributes(delegate))]
Expand Down
51 changes: 51 additions & 0 deletions crates/macros/tests/enums/names.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2023 James Draycott <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

use macros::EnumNames;
use std::assert_matches::assert_matches;

#[derive(Debug, EnumNames)]
enum Test {
Apple,
Banana,
Cherry,
}

#[test]
fn get_name() {
assert_eq!(Test::Apple.name(), "Apple");
assert_eq!(Test::Banana.name(), "Banana");
assert_eq!(Test::Cherry.name(), "Cherry");
}

#[test]
fn try_from() {
assert_matches!(Test::try_from("Apple"), Ok(Test::Apple));
assert_matches!(Test::try_from("Banana"), Ok(Test::Banana));
assert_matches!(Test::try_from("Cherry"), Ok(Test::Cherry));
}

#[test]
fn try_from_different_case() {
assert_matches!(Test::try_from("aPpLe"), Ok(Test::Apple));
assert_matches!(Test::try_from("banana"), Ok(Test::Banana));
assert_matches!(Test::try_from("CHERRY"), Ok(Test::Cherry));
}

#[test]
fn try_from_unknown() {
assert_matches!(Test::try_from("Orange"), Err(_));
}

0 comments on commit 124a030

Please sign in to comment.