typeunion/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::parse::{Parse, ParseStream, Result};
4use syn::punctuated::Punctuated;
5use syn::{parse_macro_input, Attribute, Ident, Token, Visibility};
6
7struct TypeItem {
8    attrs: Vec<Attribute>,
9    vis: Visibility,
10    name: Ident,
11    cases: Punctuated<Ident, Token![+]>,
12}
13
14impl Parse for TypeItem {
15    fn parse(input: ParseStream) -> Result<Self> {
16        let attrs = input.call(Attribute::parse_outer)?;
17        let vis = input.parse()?;
18        let _ = input.parse::<Token![type]>()?;
19        let name = input.parse()?;
20        let _ = input.parse::<Token![=]>()?;
21        let mut cases = Punctuated::new();
22
23        loop {
24            cases.push_value(input.parse()?);
25            if input.peek(Token![;]) {
26                let _ = input.parse::<Token![;]>()?;
27                break;
28            }
29            cases.push_punct(input.parse()?);
30        }
31
32        Ok(Self {
33            attrs,
34            vis,
35            name,
36            cases,
37        })
38    }
39}
40
41struct Args {
42    superset: Option<Ident>,
43}
44
45impl Parse for Args {
46    fn parse(input: ParseStream) -> Result<Self> {
47        Ok(if let Ok(_) = input.parse::<Token![super]>() {
48            let _ = input.parse::<Token![=]>()?;
49            Self {
50                superset: input.parse()?,
51            }
52        } else {
53            Self { superset: None }
54        })
55    }
56}
57
58/// Create an enum that contains a case for all given types
59///
60/// # Examples
61/// By default, enum cases are named after their contained type. To pick a different name, you can use a type alias:
62/// ```rust
63/// use typeunion::type_union;
64///
65/// type Int = i64;
66///
67/// #[type_union]
68/// #[derive(Debug, PartialEq)]
69/// type Union = String + Int;
70///
71/// // `From` is derived automatically for all cases
72/// let my_string: Union = "Hello World!".to_string().into();
73/// let my_enum_case = Union::String("Hello World!".to_string());
74/// assert_eq!(my_string, my_enum_case);
75/// ```
76///
77/// Typeunions can declare a super set, that they should be convertible to:
78/// ```rust
79/// use typeunion::type_union;
80/// use std::sync::Arc;
81///
82/// type BoxedStr = Box<str>;
83/// type ArcStr = Arc<str>;
84///
85/// #[type_union(super = SomeString)]
86/// type UniqueString = String + BoxedStr;
87///
88/// #[type_union]
89/// #[derive(Debug, PartialEq)]
90/// type SomeString = String + BoxedStr + ArcStr;
91///
92/// let a: UniqueString = "a".to_string().into();
93/// let b: SomeString = "a".to_string().into();
94/// let a_lower: SomeString = a.into();
95/// assert_eq!(a_lower, b);
96/// ```
97#[proc_macro_attribute]
98pub fn type_union(attr: TokenStream, item: TokenStream) -> TokenStream {
99    let Args { superset } = parse_macro_input!(attr as Args);
100    let TypeItem {
101        attrs,
102        vis,
103        name,
104        cases,
105    } = parse_macro_input!(item as TypeItem);
106    let cases = cases.into_iter().map(|ident| ident).collect::<Vec<_>>();
107
108    let impls = if let Some(superset) = superset {
109        quote! {
110            impl From<#name> for #superset {
111                fn from(value: #name) -> Self {
112                    match value {
113                        #(#name::#cases(case) => #superset::#cases(case)),*
114                    }
115                }
116            }
117        }
118    } else {
119        quote!()
120    };
121
122    quote! {
123        #(#attrs)*
124        #vis enum #name {
125            #(#cases(#cases)),*
126        }
127
128        #impls
129
130        #(
131            impl From<#cases> for #name {
132                fn from(value: #cases) -> Self {
133                    #name::#cases(value)
134                }
135            }
136        )*
137    }
138    .into()
139}