├── .gitignore ├── Cargo.toml ├── LICENSE ├── src └── lib.rs └── tests ├── arg_count.rs ├── as_mut.rs ├── generic.rs ├── reference.rs ├── rename.rs ├── trailing_punct.rs └── usage.rs /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | target/ 5 | **/*.bk 6 | core 7 | *.log 8 | Cargo.lock 9 | 10 | .vscode/* 11 | !.vscode/settings.json 12 | 13 | /.idea 14 | *.iml 15 | 16 | .DS_Store 17 | 18 | node_modules/ 19 | 20 | # Flamegraph 21 | *.html 22 | *.svg 23 | package-lock.json -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "is-macro" 3 | description = "Derive methods for using custom enums like Option / Result" 4 | version = "0.3.3" 5 | documentation = "https://docs.rs/is-macro" 6 | repository = "https://github.com/kdy1/is-macro" 7 | authors = ["강동윤 "] 8 | edition = "2018" 9 | license = "MIT" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [lib] 14 | proc-macro = true 15 | 16 | [dependencies] 17 | proc-macro2 = "1" 18 | quote = "1" 19 | syn = { version = "2", features = ["fold", "full", "derive", "extra-traits", "fold"] } 20 | Inflector = { version = "0.11.4", default-features = false } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use inflector::Inflector; 4 | use proc_macro2::Span; 5 | use quote::{quote, ToTokens}; 6 | use syn::parse::Parse; 7 | use syn::punctuated::{Pair, Punctuated}; 8 | use syn::spanned::Spanned; 9 | use syn::{ 10 | parse, parse2, parse_quote, Data, DataEnum, DeriveInput, Expr, ExprLit, Field, Fields, 11 | Generics, Ident, ImplItem, ItemImpl, Lit, Meta, MetaNameValue, Path, Token, Type, TypePath, 12 | TypeReference, TypeTuple, WhereClause, 13 | }; 14 | 15 | /// A proc macro to generate methods like is_variant / expect_variant. 16 | /// 17 | /// 18 | /// # Example 19 | /// 20 | /// ```rust 21 | /// 22 | /// use is_macro::Is; 23 | /// #[derive(Debug, Is)] 24 | /// pub enum Enum { 25 | /// A, 26 | /// B(T), 27 | /// C(Option), 28 | /// } 29 | /// 30 | /// // Rust's type inference cannot handle this. 31 | /// assert!(Enum::<()>::A.is_a()); 32 | /// 33 | /// assert_eq!(Enum::B(String::from("foo")).b(), Some(String::from("foo"))); 34 | /// 35 | /// assert_eq!(Enum::B(String::from("foo")).expect_b(), String::from("foo")); 36 | /// ``` 37 | /// 38 | /// # Renaming 39 | /// 40 | /// ```rust 41 | /// 42 | /// use is_macro::Is; 43 | /// #[derive(Debug, Is)] 44 | /// pub enum Enum { 45 | /// #[is(name = "video_mp4")] 46 | /// VideoMp4, 47 | /// } 48 | /// 49 | /// assert!(Enum::VideoMp4.is_video_mp4()); 50 | /// ``` 51 | #[proc_macro_derive(Is, attributes(is))] 52 | pub fn is(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 53 | let input: DeriveInput = syn::parse(input).expect("failed to parse derive input"); 54 | let generics: Generics = input.generics.clone(); 55 | 56 | let items = match input.data { 57 | Data::Enum(e) => expand(e), 58 | _ => panic!("`Is` can be applied only on enums"), 59 | }; 60 | 61 | ItemImpl { 62 | attrs: vec![], 63 | defaultness: None, 64 | unsafety: None, 65 | impl_token: Default::default(), 66 | generics: Default::default(), 67 | trait_: None, 68 | self_ty: Box::new(Type::Path(TypePath { 69 | qself: None, 70 | path: Path::from(input.ident), 71 | })), 72 | brace_token: Default::default(), 73 | items, 74 | } 75 | .with_generics(generics) 76 | .into_token_stream() 77 | .into() 78 | } 79 | 80 | #[derive(Debug)] 81 | struct Input { 82 | name: String, 83 | } 84 | 85 | impl Parse for Input { 86 | fn parse(input: parse::ParseStream) -> syn::Result { 87 | let _: Ident = input.parse()?; 88 | let _: Token![=] = input.parse()?; 89 | 90 | let name = input.parse::()?; 91 | 92 | Ok(Input { 93 | name: match name.lit { 94 | Lit::Str(s) => s.value(), 95 | _ => panic!("is(name = ...) expects a string literal"), 96 | }, 97 | }) 98 | } 99 | } 100 | 101 | fn expand(input: DataEnum) -> Vec { 102 | let mut items = vec![]; 103 | 104 | for v in &input.variants { 105 | let attrs = v 106 | .attrs 107 | .iter() 108 | .filter(|attr| attr.path().is_ident("is")) 109 | .collect::>(); 110 | if attrs.len() >= 2 { 111 | panic!("derive(Is) expects no attribute or one attribute") 112 | } 113 | let i = match attrs.into_iter().next() { 114 | None => Input { 115 | name: { 116 | v.ident.to_string().to_snake_case() 117 | // 118 | }, 119 | }, 120 | Some(attr) => { 121 | // 122 | 123 | let mut input = Input { 124 | name: Default::default(), 125 | }; 126 | 127 | let mut apply = |v: &MetaNameValue| { 128 | assert!( 129 | v.path.is_ident("name"), 130 | "Currently, is() only supports `is(name = 'foo')`" 131 | ); 132 | 133 | input.name = match &v.value { 134 | Expr::Lit(ExprLit { 135 | lit: Lit::Str(s), .. 136 | }) => s.value(), 137 | _ => unimplemented!( 138 | "is(): name must be a string literal but {:?} is provided", 139 | v.value 140 | ), 141 | }; 142 | }; 143 | 144 | match &attr.meta { 145 | Meta::NameValue(v) => { 146 | // 147 | apply(v) 148 | } 149 | Meta::List(l) => { 150 | // Handle is(name = "foo") 151 | input = parse2(l.tokens.clone()).expect("failed to parse input"); 152 | } 153 | _ => unimplemented!("is({:?})", attr.meta), 154 | } 155 | 156 | input 157 | } 158 | }; 159 | 160 | let name = &*i.name; 161 | { 162 | let name_of_is = Ident::new(&format!("is_{name}"), v.ident.span()); 163 | let docs_of_is = format!( 164 | "Returns `true` if `self` is of variant [`{variant}`].\n\n\ 165 | [`{variant}`]: #variant.{variant}", 166 | variant = v.ident, 167 | ); 168 | 169 | let variant = &v.ident; 170 | 171 | let item_impl: ItemImpl = parse_quote!( 172 | impl Type { 173 | #[doc = #docs_of_is] 174 | #[inline] 175 | pub const fn #name_of_is(&self) -> bool { 176 | match *self { 177 | Self::#variant { .. } => true, 178 | _ => false, 179 | } 180 | } 181 | } 182 | ); 183 | 184 | items.extend(item_impl.items); 185 | } 186 | 187 | { 188 | let name_of_cast = Ident::new(&format!("as_{name}"), v.ident.span()); 189 | let name_of_cast_mut = Ident::new(&format!("as_mut_{name}"), v.ident.span()); 190 | let name_of_expect = Ident::new(&format!("expect_{name}"), v.ident.span()); 191 | let name_of_take = Ident::new(name, v.ident.span()); 192 | 193 | let docs_of_cast = format!( 194 | "Returns `Some` if `self` is a reference of variant [`{variant}`], and `None` otherwise.\n\n\ 195 | [`{variant}`]: #variant.{variant}", 196 | variant = v.ident, 197 | ); 198 | let docs_of_cast_mut = format!( 199 | "Returns `Some` if `self` is a mutable reference of variant [`{variant}`], and `None` otherwise.\n\n\ 200 | [`{variant}`]: #variant.{variant}", 201 | variant = v.ident, 202 | ); 203 | let docs_of_expect = format!( 204 | "Unwraps the value, yielding the content of [`{variant}`].\n\n\ 205 | # Panics\n\n\ 206 | Panics if the value is not [`{variant}`], with a panic message including \ 207 | the content of `self`.\n\n\ 208 | [`{variant}`]: #variant.{variant}", 209 | variant = v.ident, 210 | ); 211 | let docs_of_take = format!( 212 | "Returns `Some` if `self` is of variant [`{variant}`], and `None` otherwise.\n\n\ 213 | [`{variant}`]: #variant.{variant}", 214 | variant = v.ident, 215 | ); 216 | 217 | if let Fields::Unnamed(fields) = &v.fields { 218 | let types = fields.unnamed.iter().map(|f| f.ty.clone()); 219 | let cast_ty = types_to_type(types.clone().map(|ty| add_ref(false, ty))); 220 | let cast_ty_mut = types_to_type(types.clone().map(|ty| add_ref(true, ty))); 221 | let ty = types_to_type(types); 222 | 223 | let mut fields: Punctuated = fields 224 | .unnamed 225 | .clone() 226 | .into_pairs() 227 | .enumerate() 228 | .map(|(i, pair)| { 229 | let handle = |f: Field| { 230 | // 231 | Ident::new(&format!("v{i}"), f.span()) 232 | }; 233 | match pair { 234 | Pair::Punctuated(v, p) => Pair::Punctuated(handle(v), p), 235 | Pair::End(v) => Pair::End(handle(v)), 236 | } 237 | }) 238 | .collect(); 239 | 240 | // Make sure that we don't have any trailing punctuation 241 | // This ensure that if we have a single unnamed field, 242 | // we will produce a value of the form `(v)`, 243 | // not a single-element tuple `(v,)` 244 | if let Some(mut pair) = fields.pop() { 245 | if let Pair::Punctuated(v, _) = pair { 246 | pair = Pair::End(v); 247 | } 248 | fields.extend(std::iter::once(pair)); 249 | } 250 | 251 | let variant = &v.ident; 252 | 253 | let item_impl: ItemImpl = parse_quote!( 254 | impl #ty { 255 | #[doc = #docs_of_cast] 256 | #[inline] 257 | pub fn #name_of_cast(&self) -> Option<#cast_ty> { 258 | match self { 259 | Self::#variant(#fields) => Some((#fields)), 260 | _ => None, 261 | } 262 | } 263 | 264 | #[doc = #docs_of_cast_mut] 265 | #[inline] 266 | pub fn #name_of_cast_mut(&mut self) -> Option<#cast_ty_mut> { 267 | match self { 268 | Self::#variant(#fields) => Some((#fields)), 269 | _ => None, 270 | } 271 | } 272 | 273 | #[doc = #docs_of_expect] 274 | #[inline] 275 | pub fn #name_of_expect(self) -> #ty 276 | where 277 | Self: ::std::fmt::Debug, 278 | { 279 | match self { 280 | Self::#variant(#fields) => (#fields), 281 | _ => panic!("called expect on {:?}", self), 282 | } 283 | } 284 | 285 | #[doc = #docs_of_take] 286 | #[inline] 287 | pub fn #name_of_take(self) -> Option<#ty> { 288 | match self { 289 | Self::#variant(#fields) => Some((#fields)), 290 | _ => None, 291 | } 292 | } 293 | } 294 | ); 295 | 296 | items.extend(item_impl.items); 297 | } 298 | } 299 | } 300 | 301 | items 302 | } 303 | 304 | fn types_to_type(types: impl Iterator) -> Type { 305 | let mut types: Punctuated<_, _> = types.collect(); 306 | if types.len() == 1 { 307 | types.pop().expect("len is 1").into_value() 308 | } else { 309 | TypeTuple { 310 | paren_token: Default::default(), 311 | elems: types, 312 | } 313 | .into() 314 | } 315 | } 316 | 317 | fn add_ref(mutable: bool, ty: Type) -> Type { 318 | Type::Reference(TypeReference { 319 | and_token: Default::default(), 320 | lifetime: None, 321 | mutability: if mutable { 322 | Some(Default::default()) 323 | } else { 324 | None 325 | }, 326 | elem: Box::new(ty), 327 | }) 328 | } 329 | 330 | /// Extension trait for `ItemImpl` (impl block). 331 | trait ItemImplExt { 332 | /// Instead of 333 | /// 334 | /// ```rust,ignore 335 | /// let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); 336 | /// 337 | /// let item: Item = Quote::new(def_site::()) 338 | /// .quote_with(smart_quote!( 339 | /// Vars { 340 | /// Type: type_name, 341 | /// impl_generics, 342 | /// ty_generics, 343 | /// where_clause, 344 | /// }, 345 | /// { 346 | /// impl impl_generics ::swc_common::AstNode for Type ty_generics 347 | /// where_clause {} 348 | /// } 349 | /// )).parse(); 350 | /// ``` 351 | /// 352 | /// You can use this like 353 | /// 354 | /// ```rust,ignore 355 | // let item = Quote::new(def_site::()) 356 | /// .quote_with(smart_quote!(Vars { Type: type_name }, { 357 | /// impl ::swc_common::AstNode for Type {} 358 | /// })) 359 | /// .parse::() 360 | /// .with_generics(input.generics); 361 | /// ``` 362 | fn with_generics(self, generics: Generics) -> Self; 363 | } 364 | 365 | impl ItemImplExt for ItemImpl { 366 | fn with_generics(mut self, mut generics: Generics) -> Self { 367 | // TODO: Check conflicting name 368 | 369 | let need_new_punct = !generics.params.empty_or_trailing(); 370 | if need_new_punct { 371 | generics 372 | .params 373 | .push_punct(syn::token::Comma(Span::call_site())); 374 | } 375 | 376 | // Respan 377 | if let Some(t) = generics.lt_token { 378 | self.generics.lt_token = Some(t) 379 | } 380 | if let Some(t) = generics.gt_token { 381 | self.generics.gt_token = Some(t) 382 | } 383 | 384 | let ty = self.self_ty; 385 | 386 | // Handle generics defined on struct, enum, or union. 387 | let mut item: ItemImpl = { 388 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 389 | let item = if let Some((ref polarity, ref path, ref for_token)) = self.trait_ { 390 | quote! { 391 | impl #impl_generics #polarity #path #for_token #ty #ty_generics #where_clause {} 392 | } 393 | } else { 394 | quote! { 395 | impl #impl_generics #ty #ty_generics #where_clause {} 396 | 397 | } 398 | }; 399 | parse2(item.into_token_stream()) 400 | .unwrap_or_else(|err| panic!("with_generics failed: {}", err)) 401 | }; 402 | 403 | // Handle generics added by proc-macro. 404 | item.generics 405 | .params 406 | .extend(self.generics.params.into_pairs()); 407 | match self.generics.where_clause { 408 | Some(WhereClause { 409 | ref mut predicates, .. 410 | }) => predicates.extend( 411 | generics 412 | .where_clause 413 | .into_iter() 414 | .flat_map(|wc| wc.predicates.into_pairs()), 415 | ), 416 | ref mut opt @ None => *opt = generics.where_clause, 417 | } 418 | 419 | ItemImpl { 420 | attrs: self.attrs, 421 | defaultness: self.defaultness, 422 | unsafety: self.unsafety, 423 | impl_token: self.impl_token, 424 | brace_token: self.brace_token, 425 | items: self.items, 426 | ..item 427 | } 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /tests/arg_count.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | #[derive(Debug, Is)] 4 | pub enum Enum { 5 | A(), 6 | B(usize, usize), 7 | C(String), 8 | D(&'static str, &'static mut u32), 9 | } 10 | -------------------------------------------------------------------------------- /tests/as_mut.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | #[derive(Debug, PartialEq, Is)] 4 | pub enum Enum { 5 | A(u32), 6 | B(Vec), 7 | } 8 | 9 | #[test] 10 | fn test() { 11 | let mut e = Enum::A(0); 12 | *e.as_mut_a().unwrap() += 1; 13 | assert_eq!(e, Enum::A(1)); 14 | 15 | let mut e = Enum::B(vec![]); 16 | e.as_mut_b().unwrap().push(1); 17 | assert_eq!(e, Enum::B(vec![1])); 18 | } 19 | -------------------------------------------------------------------------------- /tests/generic.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | #[derive(Debug, Is)] 4 | pub enum Enum { 5 | A, 6 | B(T), 7 | C(Option), 8 | } 9 | -------------------------------------------------------------------------------- /tests/reference.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | #[derive(Debug, Is)] 4 | pub enum Enum { 5 | A, 6 | B(&'static str), 7 | C(&'static mut u32), 8 | } 9 | -------------------------------------------------------------------------------- /tests/rename.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | #[derive(Debug, Is)] 3 | pub enum Enum { 4 | #[is(name = "video_mp4")] 5 | VideoMp4, 6 | } 7 | 8 | #[test] 9 | fn test() { 10 | assert!(Enum::VideoMp4.is_video_mp4()); 11 | } 12 | -------------------------------------------------------------------------------- /tests/trailing_punct.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | #[derive(Debug, Is)] 4 | pub enum Enum { 5 | B(usize,), 6 | } 7 | -------------------------------------------------------------------------------- /tests/usage.rs: -------------------------------------------------------------------------------- 1 | use is_macro::Is; 2 | 3 | #[derive(Debug, Is)] 4 | pub enum Enum { 5 | A, 6 | B(String), 7 | C(bool), 8 | } 9 | --------------------------------------------------------------------------------