├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── imp.rs ├── lib.rs ├── test.rs └── unvenial.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "prettyplease" 7 | version = "0.1.23" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" 10 | dependencies = [ 11 | "proc-macro2", 12 | "syn", 13 | ] 14 | 15 | [[package]] 16 | name = "proc-macro2" 17 | version = "1.0.51" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" 20 | dependencies = [ 21 | "unicode-ident", 22 | ] 23 | 24 | [[package]] 25 | name = "quote" 26 | version = "1.0.23" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 29 | dependencies = [ 30 | "proc-macro2", 31 | ] 32 | 33 | [[package]] 34 | name = "structstruck" 35 | version = "0.5.0" 36 | dependencies = [ 37 | "prettyplease", 38 | "proc-macro2", 39 | "quote", 40 | "syn", 41 | "venial", 42 | ] 43 | 44 | [[package]] 45 | name = "syn" 46 | version = "1.0.107" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 49 | dependencies = [ 50 | "proc-macro2", 51 | "quote", 52 | "unicode-ident", 53 | ] 54 | 55 | [[package]] 56 | name = "unicode-ident" 57 | version = "1.0.6" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 60 | 61 | [[package]] 62 | name = "venial" 63 | version = "0.5.0" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "61584a325b16f97b5b25fcc852eb9550843a251057a5e3e5992d2376f3df4bb2" 66 | dependencies = [ 67 | "proc-macro2", 68 | "quote", 69 | ] 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "structstruck" 3 | authors = ["Julius Michaelis "] 4 | license = "MIT" 5 | categories = ["rust-patterns", "development-tools::procedural-macro-helpers"] 6 | keywords = ["proc_macro", "nested", "struct"] 7 | version = "0.5.0" 8 | edition = "2021" 9 | description = "Nested struct and enum definitions" 10 | repository = "https://github.com/jcaesar/structstruck" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | proc-macro2 = "1.0.51" 17 | quote = "1.0.23" 18 | venial = "0.5.0" 19 | 20 | [dev-dependencies] 21 | prettyplease = "0.1.23" 22 | syn = "1.0.107" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 jcaesar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StructStruck 2 | 3 | Ever had a deeply nested JSON struct 4 | ```json 5 | { 6 | "outer": { 7 | "middle": { 8 | "inner": { 9 | "foo": "bar" 10 | } 11 | } 12 | } 13 | } 14 | ``` 15 | and wanted to write the Rust structs to handle that data just in the same nested way? 16 | ```rust 17 | struct Parent { 18 | outer: struct { 19 | middle: struct { 20 | inner: struct { 21 | foo: String, 22 | } 23 | } 24 | } 25 | } 26 | ``` 27 | This proc macro crate allows exactly that. 28 | Check the [docs](https://docs.rs/structstruck) on how exaclty. 29 | 30 | For illustration, some more usecases: 31 | 32 | * an enum where every variant has its own struct, named exactly the same as the variant. 33 | ```rust 34 | structstruck::strike! { 35 | enum Token { 36 | Identifier(struct { 37 | name: String, 38 | }), 39 | Punctuation(struct { 40 | character: char, 41 | }), 42 | } 43 | } 44 | ``` 45 | 46 | * my original use case: conveniently write kubernetes custom resources with `kube`. 47 | ```rust 48 | structstruck::strike! { 49 | #[structstruck::each[derive(Deserialize, Serialize, Clone, Debug, Validate, JsonSchema)]] 50 | #[structstruck::each[serde(rename_all = "camelCase")]] 51 | #[derive(CustomResource)] 52 | #[kube( 53 | group = "kafka.strimzi.io", 54 | version = "v1beta2", 55 | kind = "Kafka", 56 | namespaced 57 | )] 58 | struct KafkaSpec { 59 | kafka: struct KafkaCluster { 60 | #[validate(length(min = 1))] 61 | version: String, 62 | #[validate(range(min = 1))] 63 | replicas: u32, 64 | listeners: Vec, 70 | config: HashMap, 71 | storage: struct { 72 | r#type: String, 73 | volumes: Vec, 75 | r#type: String, 76 | size: String, 77 | delete_claim: bool, 78 | }>, 79 | }, 80 | }, 81 | zookeeper: struct { 82 | #[validate(range(min = 1))] 83 | replicas: u32, 84 | storage: Volume, 85 | }, 86 | entity_operator: struct { 87 | topic_operator: Option>, 88 | user_operator: Option>, 89 | }, 90 | } 91 | } 92 | ``` 93 | 94 | -------------------------------------------------------------------------------- /src/imp.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::Delimiter; 2 | use proc_macro2::Group; 3 | use proc_macro2::Ident; 4 | use proc_macro2::Punct; 5 | use proc_macro2::Spacing; 6 | use proc_macro2::Span; 7 | use proc_macro2::TokenStream; 8 | use proc_macro2::TokenTree; 9 | use quote::quote; 10 | use quote::quote_spanned; 11 | use quote::ToTokens; 12 | use std::iter::once; 13 | use std::mem; 14 | use std::ops::Deref; 15 | use venial::parse_declaration; 16 | use venial::Attribute; 17 | use venial::AttributeValue; 18 | use venial::Declaration; 19 | use venial::GenericParam; 20 | use venial::GenericParamList; 21 | use venial::StructFields; 22 | 23 | fn stream_span(input: impl Iterator>) -> Option { 24 | let mut ret = None; 25 | for tok in input { 26 | let tok = tok.deref(); 27 | match ret { 28 | None => ret = Some(tok.span()), 29 | Some(span) => match span.join(tok.span()) { 30 | Some(span) => ret = Some(span), 31 | None => return ret, 32 | }, 33 | } 34 | } 35 | ret 36 | } 37 | 38 | #[derive(Default, Clone, Copy)] 39 | pub(crate) struct NameHints<'a> { 40 | long: bool, 41 | parent_name: &'a str, 42 | variant_name: Option<&'a str>, 43 | field_name: Option<&'a str>, 44 | } 45 | impl<'a> NameHints<'a> { 46 | fn from(parent_name: &'a str, attributes: &mut Vec) -> Self { 47 | let mut long = false; 48 | attributes.retain(|attr| { 49 | let enable_long = check_crate_attr(attr, "long_names"); 50 | long |= enable_long; 51 | !enable_long 52 | }); 53 | NameHints { 54 | long, 55 | parent_name, 56 | variant_name: None, 57 | field_name: None, 58 | } 59 | } 60 | 61 | fn get_name_hint(&self, num: Option, span: Span) -> Ident { 62 | let num = num.filter(|&n| n > 0).map(|n| n.to_string()); 63 | let names = match self.long { 64 | true => &[ 65 | Some(self.parent_name), 66 | self.variant_name, 67 | self.field_name, 68 | num.as_deref(), 69 | ][..], 70 | false => &[ 71 | self.field_name 72 | .or(self.variant_name) 73 | .or(Some(self.parent_name)), 74 | num.as_deref(), 75 | ][..], 76 | }; 77 | let name = names 78 | .into_iter() 79 | .map(|x| x.map(pascal_case).unwrap_or(String::new())) 80 | .fold(String::new(), |s, p| s + &p); 81 | Ident::new(&name, span) 82 | } 83 | 84 | fn with_field_name(&self, field_name: &'a str) -> Self { 85 | Self { 86 | field_name: Some(field_name), 87 | ..*self 88 | } 89 | } 90 | 91 | fn with_variant_name(&self, variant_name: &'a str) -> Self { 92 | Self { 93 | variant_name: Some(variant_name), 94 | ..*self 95 | } 96 | } 97 | } 98 | 99 | fn check_crate_attr_no_params(attr: &Attribute, name: &str, ret: &mut TokenStream) -> bool { 100 | if check_crate_attr(attr, name) { 101 | match attr.value { 102 | AttributeValue::Empty => {} 103 | _ => { 104 | report_error( 105 | stream_span(attr.get_value_tokens().iter()), 106 | ret, 107 | &format!("#[{}] does not take parameters", name), 108 | ); 109 | } 110 | } 111 | true 112 | } else { 113 | false 114 | } 115 | } 116 | 117 | fn check_crate_attr(attr: &Attribute, attr_name: &str) -> bool { 118 | use TokenTree::{Ident, Punct}; 119 | matches!( 120 | &attr.path[..], 121 | [Ident(crat), Punct(c1), Punct(c2), Ident(attr)] 122 | if crat == env!("CARGO_CRATE_NAME") 123 | && c1.as_char() == ':' 124 | && c1.spacing() == Spacing::Joint 125 | && c2.as_char() == ':' 126 | && attr == attr_name 127 | ) 128 | } 129 | 130 | /// capitalizes the first letter of each word and the one after an underscore 131 | /// e.g. `foo_bar` -> `FooBar` 132 | /// this also keeps consecutive uppercase letters 133 | fn pascal_case(s: &str) -> String { 134 | let mut ret = String::new(); 135 | let mut uppercase_next = true; 136 | for c in s.chars() { 137 | if c == '_' { 138 | uppercase_next = true; 139 | } else if uppercase_next { 140 | ret.push(c.to_ascii_uppercase()); 141 | uppercase_next = false; 142 | } else { 143 | ret.push(c); 144 | } 145 | } 146 | ret 147 | } 148 | 149 | pub(crate) fn recurse_through_definition( 150 | input: TokenStream, 151 | mut strike_attrs: Vec, 152 | make_pub: bool, 153 | ret: &mut TokenStream, 154 | ) -> Option { 155 | let input_vec = input.into_iter().collect::>(); 156 | let span = stream_span(input_vec.iter()); 157 | let input = hack_append_type_decl_semicolon(input_vec); 158 | let input = move_out_inner_attrs(input); 159 | let mut parsed = match parse_declaration(input) { 160 | Ok(parsed) => parsed, 161 | Err(e) => { 162 | // Sadly, venial still panics on invalid syntax 163 | report_error(span, ret, &format!("{}", e)); 164 | return None; 165 | } 166 | }; 167 | match &mut parsed { 168 | Declaration::Struct(s) => { 169 | strike_through_attributes(&mut s.attributes, &mut strike_attrs, ret); 170 | let name = s.name.to_string(); 171 | let path = &NameHints::from(&name, &mut s.attributes); 172 | recurse_through_struct_fields( 173 | &mut s.fields, 174 | &strike_attrs, 175 | ret, 176 | false, 177 | path, 178 | s.name.span(), 179 | ); 180 | if make_pub { 181 | s.vis_marker.get_or_insert_with(make_pub_marker); 182 | } 183 | } 184 | Declaration::Enum(e) => { 185 | strike_through_attributes(&mut e.attributes, &mut strike_attrs, ret); 186 | let name = e.name.to_string(); 187 | let path = &NameHints::from(&name, &mut e.attributes); 188 | for (v, _) in &mut e.variants.iter_mut() { 189 | let name = v.name.to_string(); 190 | let path = &path.with_variant_name(&name); 191 | recurse_through_struct_fields( 192 | &mut v.contents, 193 | &strike_attrs, 194 | ret, 195 | is_plain_pub(&e.vis_marker), 196 | path, 197 | v.name.span(), 198 | ); 199 | } 200 | if make_pub { 201 | e.vis_marker.get_or_insert_with(make_pub_marker); 202 | } 203 | } 204 | Declaration::Union(u) => { 205 | strike_through_attributes(&mut u.attributes, &mut strike_attrs, ret); 206 | let name = u.name.to_string(); 207 | let path = &NameHints::from(&name, &mut u.attributes); 208 | named_struct_fields(&mut u.fields, &strike_attrs, ret, false, path); 209 | if make_pub { 210 | u.vis_marker.get_or_insert_with(make_pub_marker); 211 | } 212 | } 213 | Declaration::TyDefinition(t) => { 214 | strike_through_attributes(&mut t.attributes, &mut strike_attrs, ret); 215 | let name = t.name.to_string(); 216 | let path = &NameHints::from(&name, &mut t.attributes); 217 | let ttok = mem::take(&mut t.initializer_ty.tokens); 218 | recurse_through_type_list( 219 | &type_tree(&ttok, ret), 220 | &strike_attrs, 221 | ret, 222 | &None, 223 | false, 224 | &mut t.initializer_ty.tokens, 225 | path, 226 | ); 227 | if make_pub { 228 | t.vis_marker.get_or_insert_with(make_pub_marker); 229 | } 230 | } 231 | _ => { 232 | report_error( 233 | span, 234 | ret, 235 | "Unsupported declaration (only struct, enum, and union are allowed)", 236 | ); 237 | return None; 238 | } 239 | } 240 | if let Declaration::Struct(s) = &mut parsed { 241 | if let StructFields::Tuple(_) = s.fields { 242 | if s.tk_semicolon.is_none() { 243 | s.tk_semicolon = Some(Punct::new(';', Spacing::Alone)) 244 | } 245 | } 246 | } 247 | parsed.to_tokens(ret); 248 | parsed.generic_params().cloned() 249 | } 250 | 251 | fn hack_append_type_decl_semicolon(input_vec: Vec) -> TokenStream { 252 | let is_type_decl = input_vec 253 | .iter() 254 | .any(|t| matches!(t, TokenTree::Ident(kw) if kw == "type")) 255 | && input_vec.iter().all(|t| { 256 | matches!(t, TokenTree::Ident(kw) if kw == "type") 257 | || !matches!(t, TokenTree::Ident(kw) if is_decl_kw(kw)) 258 | }); 259 | let input = match is_type_decl { 260 | true => input_vec 261 | .into_iter() 262 | .chain(once(TokenTree::Punct(Punct::new(';', Spacing::Alone)))) 263 | .collect(), 264 | false => input_vec.into_iter().collect(), 265 | }; 266 | input 267 | } 268 | 269 | pub(crate) fn make_pub_marker() -> venial::VisMarker { 270 | venial::VisMarker { 271 | tk_token1: TokenTree::Ident(Ident::new("pub", Span::mixed_site())), 272 | tk_token2: None, 273 | } 274 | } 275 | 276 | pub(crate) fn is_plain_pub(vis_marker: &Option) -> bool { 277 | match vis_marker { 278 | Some(venial::VisMarker { 279 | tk_token1: TokenTree::Ident(i), 280 | tk_token2: None, 281 | }) if i.to_string() == "pub" => true, 282 | _ => false, 283 | } 284 | } 285 | 286 | fn move_out_inner_attrs(input: TokenStream) -> TokenStream { 287 | let mut prefix = vec![]; 288 | let mut ret = vec![]; 289 | for e in input { 290 | match e { 291 | TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => { 292 | let mut tt: Vec = vec![]; 293 | let gt = g.stream().into_iter().collect::>(); 294 | let mut gt = >[..]; 295 | loop { 296 | match gt { 297 | [TokenTree::Punct(hash), TokenTree::Punct(bang), TokenTree::Group(tree), rest @ ..] 298 | if hash.as_char() == '#' && bang.as_char() == '!' => 299 | { 300 | gt = rest; 301 | prefix.extend_from_slice(&[ 302 | TokenTree::Punct(hash.to_owned()), 303 | TokenTree::Group(tree.to_owned()), 304 | ]); 305 | } 306 | [rest @ ..] => { 307 | for t in rest { 308 | tt.push(t.to_owned()); 309 | } 310 | break; 311 | } 312 | } 313 | } 314 | let mut gr = Group::new(g.delimiter(), tt.into_iter().collect()); 315 | gr.set_span(g.span()); 316 | ret.push(TokenTree::Group(gr)); 317 | } 318 | e => ret.push(e), 319 | } 320 | } 321 | prefix.into_iter().chain(ret.into_iter()).collect() 322 | } 323 | 324 | fn recurse_through_struct_fields( 325 | fields: &mut venial::StructFields, 326 | strike_attrs: &[Attribute], 327 | ret: &mut TokenStream, 328 | in_pub_enum: bool, 329 | path: &NameHints, 330 | span: Span, 331 | ) { 332 | match fields { 333 | StructFields::Unit => (), 334 | StructFields::Named(n) => named_struct_fields(n, strike_attrs, ret, in_pub_enum, path), 335 | StructFields::Tuple(t) => { 336 | tuple_struct_fields(t, strike_attrs, ret, in_pub_enum, path, span) 337 | } 338 | } 339 | } 340 | 341 | fn named_struct_fields( 342 | n: &mut venial::NamedStructFields, 343 | strike_attrs: &[Attribute], 344 | ret: &mut TokenStream, 345 | in_pub_enum: bool, 346 | path: &NameHints, 347 | ) { 348 | for (field, _) in &mut n.fields.iter_mut() { 349 | // clone path here to start at the same level for each field 350 | // this is necessary because the path is modified/cleared in the recursion 351 | let path = path.clone(); 352 | let field_name = field.name.to_string(); 353 | let field_name = match field_name.starts_with("r#") { 354 | true => &field_name[2..], 355 | false => &field_name, 356 | }; 357 | let ttok = mem::take(&mut field.ty.tokens); 358 | let path = path.with_field_name(field_name); 359 | let name_hint = path.get_name_hint(None, field.name.span()); 360 | recurse_through_type_list( 361 | &type_tree(&ttok, ret), 362 | strike_attrs, 363 | ret, 364 | &Some(name_hint), 365 | is_plain_pub(&field.vis_marker) || in_pub_enum, 366 | &mut field.ty.tokens, 367 | &path, 368 | ); 369 | } 370 | } 371 | 372 | fn tuple_struct_fields( 373 | t: &mut venial::TupleStructFields, 374 | strike_attrs: &[Attribute], 375 | ret: &mut TokenStream, 376 | in_pub_enum: bool, 377 | path: &NameHints, 378 | span: Span, 379 | ) { 380 | for (num, (field, _)) in &mut t.fields.iter_mut().enumerate() { 381 | // clone path here to start at the same level for each field 382 | // this is necessary because the path is modified/cleared in the recursion 383 | let mut path = path.clone(); 384 | let ttok = mem::take(&mut field.ty.tokens); 385 | let ttok = type_tree(&ttok, ret); 386 | 387 | // Slight hack for tuple structs: 388 | // struct Foo(pub struct Bar()); is ambigous: 389 | // Which does the pub belong to, Bar or Foo::0? 390 | // I'd say Bar, but venial parses the pub as the visibility specifier of the current struct field 391 | // So, transfer the visibility specifier to the declaration token stream, but only if there isn't already one: 392 | // I also don't want to break struct Foo(pub pub struct Bar()); (both Bar and Foo::0 public) 393 | let vtok; 394 | let ttok = match ttok 395 | .iter() 396 | .any(|t| matches!(t, TypeTree::Token(TokenTree::Ident(kw)) if kw == "pub")) 397 | { 398 | true => ttok, 399 | false => match mem::take(&mut field.vis_marker) { 400 | Some(vis) => { 401 | vtok = vis.into_token_stream().into_iter().collect::>(); 402 | vtok.iter() 403 | .map(TypeTree::Token) 404 | .chain(ttok.into_iter()) 405 | .collect() 406 | } 407 | None => ttok, 408 | }, 409 | }; 410 | let name_hint = path.get_name_hint(Some(num), span); 411 | recurse_through_type_list( 412 | &ttok, 413 | strike_attrs, 414 | ret, 415 | &Some(name_hint), 416 | is_plain_pub(&field.vis_marker) || in_pub_enum, 417 | &mut field.ty.tokens, 418 | &mut path, 419 | ); 420 | } 421 | } 422 | 423 | fn strike_through_attributes( 424 | dec_attrs: &mut Vec, 425 | strike_attrs: &mut Vec, 426 | ret: &mut TokenStream, 427 | ) { 428 | dec_attrs.retain(|attr| { 429 | if check_crate_attr_no_params(attr, "clear_each", ret) { 430 | // future feature idea: clear/skip only some attributes 431 | strike_attrs.clear(); 432 | return false; 433 | } 434 | true 435 | }); 436 | let mut skip_each = false; 437 | dec_attrs.retain(|attr| { 438 | if check_crate_attr_no_params(attr, "skip_each", ret) { 439 | skip_each |= true; 440 | return false; 441 | } 442 | true 443 | }); 444 | dec_attrs.retain(|attr| { 445 | let each = check_crate_attr(attr, "each"); 446 | let strikethrough = 447 | matches!(&attr.path[..], [TokenTree::Ident(kw)] if kw == "strikethrough"); 448 | if strikethrough { 449 | report_strikethrough_deprecated(ret, attr.path[0].span()); 450 | } 451 | if strikethrough || each { 452 | match &attr.value { 453 | AttributeValue::Group(brackets, value) => { 454 | strike_attrs.push(Attribute { 455 | tk_bang: attr.tk_bang.clone(), 456 | tk_hash: attr.tk_hash.clone(), 457 | tk_brackets: brackets.clone(), 458 | // Hack a bit: Put all the tokens into the path, none in the value. 459 | path: value.to_vec(), 460 | value: AttributeValue::Empty, 461 | }); 462 | } 463 | _ => { 464 | report_error( 465 | stream_span(attr.get_value_tokens().iter()), 466 | ret, 467 | "#[structstruck::each …]: … must be a [group]", 468 | ); 469 | } 470 | }; 471 | return false; 472 | } 473 | true 474 | }); 475 | 476 | if !skip_each { 477 | dec_attrs.splice(0..0, strike_attrs.iter().cloned()); 478 | } 479 | } 480 | 481 | fn report_strikethrough_deprecated(ret: &mut TokenStream, span: Span) { 482 | // stolen from proc-macro-warning, which depends on syn 483 | let q = quote_spanned!(span => 484 | #[allow(dead_code)] 485 | #[allow(non_camel_case_types)] 486 | #[allow(non_snake_case)] 487 | fn strikethrough_used() { 488 | #[deprecated(note = "The strikethrough attribute is depcrecated. Use structstruck::each instead.")] 489 | #[allow(non_upper_case_globals)] 490 | const _w: () = (); 491 | let _ = _w; 492 | } 493 | ); 494 | q.to_tokens(ret); 495 | } 496 | 497 | fn get_tt_punct<'t>(t: &'t TypeTree<'t>, c: char) -> Option<&'t Punct> { 498 | match t { 499 | TypeTree::Token(TokenTree::Punct(p)) if p.as_char() == c => Some(p), 500 | _ => None, 501 | } 502 | } 503 | 504 | fn recurse_through_type_list( 505 | tok: &[TypeTree], 506 | strike_attrs: &[Attribute], 507 | ret: &mut TokenStream, 508 | name_hint: &Option, 509 | pub_hint: bool, 510 | type_ret: &mut Vec, 511 | path: &NameHints, 512 | ) { 513 | let mut tok = tok; 514 | loop { 515 | let end = tok.iter().position(|t| get_tt_punct(t, ',').is_some()); 516 | let current = &tok[..end.unwrap_or(tok.len())]; 517 | recurse_through_type( 518 | current, 519 | strike_attrs, 520 | ret, 521 | name_hint, 522 | pub_hint, 523 | type_ret, 524 | path, 525 | ); 526 | if let Some(comma) = end { 527 | type_ret.push(match tok[comma] { 528 | TypeTree::Token(comma) => comma.clone(), 529 | _ => unreachable!(), 530 | }); 531 | tok = &tok[comma + 1..]; 532 | } else { 533 | return; 534 | } 535 | } 536 | } 537 | fn recurse_through_type( 538 | tok: &[TypeTree], 539 | strike_attrs: &[Attribute], 540 | ret: &mut TokenStream, 541 | name_hint: &Option, 542 | pub_hint: bool, 543 | type_ret: &mut Vec, 544 | path: &NameHints, 545 | ) { 546 | if let Some(c) = tok.windows(3).find_map(|t| { 547 | get_tt_punct(&t[0], ':') 548 | .or(get_tt_punct(&t[2], ':')) 549 | .is_none() 550 | .then(|| get_tt_punct(&t[1], ':')) 551 | .flatten() 552 | }) { 553 | report_error( 554 | Some(c.span()), 555 | ret, 556 | "Colon in top level of type expression. Did you forget a comma somewhere?", 557 | ); 558 | } 559 | let kw = tok.iter().position(|t| get_decl_ident(t).is_some()); 560 | if let Some(kw) = kw { 561 | if let Some(dup) = tok[kw + 1..].iter().find_map(get_decl_ident) { 562 | report_error( 563 | Some(dup.span()), 564 | ret, 565 | "More than one struct/enum/.. declaration found", 566 | ); 567 | } 568 | let mut decl = Vec::new(); 569 | un_tree_type(tok, &mut decl); 570 | let pos = decl 571 | .iter() 572 | .position(|t| matches!(t, TokenTree::Ident(kw) if is_decl_kw(kw))) 573 | .unwrap(); 574 | let generics = if let Some(name @ TokenTree::Ident(_)) = decl.get(pos + 1) { 575 | type_ret.push(name.clone()); 576 | recurse_through_definition( 577 | decl.into_iter().collect(), 578 | strike_attrs.to_vec(), 579 | pub_hint, 580 | ret, 581 | ) 582 | } else { 583 | let name = match name_hint { 584 | Some(name) => TokenTree::Ident(name.clone()), 585 | None => { 586 | report_error( 587 | stream_span(decl.iter()), 588 | ret, 589 | "No context for naming substructure", 590 | ); 591 | TokenTree::Punct(Punct::new('!', Spacing::Alone)) 592 | } 593 | }; 594 | let tail = decl.drain((pos + 1)..).collect::(); 595 | let head = decl.into_iter().collect::(); 596 | let newthing = quote! {#head #name #tail}; 597 | let generics = 598 | recurse_through_definition(newthing, strike_attrs.to_vec(), pub_hint, ret); 599 | 600 | type_ret.push(name); 601 | generics 602 | }; 603 | if let Some(generics) = generics { 604 | type_ret.push(generics.tk_l_bracket.into()); 605 | let mut gp = generics.params.clone(); 606 | gp.iter_mut().for_each(|(gp, _)| { 607 | *gp = GenericParam { 608 | name: gp.name.clone(), 609 | tk_prefix: gp 610 | .tk_prefix 611 | .clone() 612 | .filter(|pfx| matches!(pfx, TokenTree::Punct(_))), 613 | bound: None, 614 | } 615 | }); 616 | type_ret.extend(gp.into_token_stream()); 617 | type_ret.push(generics.tk_r_bracket.into()); 618 | } 619 | } else { 620 | un_type_tree(tok, type_ret, |g, type_ret| { 621 | recurse_through_type_list(g, strike_attrs, ret, name_hint, false, type_ret, path) 622 | }); 623 | } 624 | } 625 | 626 | fn get_decl_ident<'a>(t: &'a TypeTree) -> Option<&'a Ident> { 627 | match t { 628 | TypeTree::Token(TokenTree::Ident(ref kw)) if is_decl_kw(kw) => Some(kw), 629 | _ => None, 630 | } 631 | } 632 | 633 | fn un_tree_type(tok: &[TypeTree], type_ret: &mut Vec) { 634 | un_type_tree(tok, type_ret, un_tree_type) 635 | } 636 | 637 | fn un_type_tree( 638 | tok: &[TypeTree], 639 | type_ret: &mut Vec, 640 | mut f: impl FnMut(&[TypeTree], &mut Vec), 641 | ) { 642 | for tt in tok.iter() { 643 | match tt { 644 | TypeTree::Group(o, g, c) => { 645 | type_ret.push(TokenTree::Punct((*o).clone())); 646 | f(g, type_ret); 647 | if let Some(c) = c { 648 | type_ret.push(TokenTree::Punct((*c).clone())); 649 | } 650 | } 651 | TypeTree::Token(t) => type_ret.push((*t).clone()), 652 | } 653 | } 654 | } 655 | 656 | #[cfg_attr(test, derive(Debug))] 657 | pub(crate) enum TypeTree<'a> { 658 | Group(&'a Punct, Vec>, Option<&'a Punct>), 659 | Token(&'a TokenTree), 660 | } 661 | 662 | pub(crate) fn type_tree<'a>(args: &'a [TokenTree], ret: &'_ mut TokenStream) -> Vec> { 663 | let mut stac = vec![]; 664 | let mut current = vec![]; 665 | for tt in args { 666 | match tt { 667 | TokenTree::Punct(open) if open.as_char() == '<' => { 668 | stac.push((open, mem::take(&mut current))); 669 | } 670 | TokenTree::Punct(close) if close.as_char() == '>' => { 671 | if let Some((open, parent)) = stac.pop() { 672 | let child = mem::replace(&mut current, parent); 673 | current.push(TypeTree::Group(open, child, Some(close))); 674 | } else { 675 | report_error(Some(close.span()), ret, "Unexpected >"); 676 | current.push(TypeTree::Token(tt)); 677 | } 678 | } 679 | tt => current.push(TypeTree::Token(tt)), 680 | } 681 | } 682 | while let Some((open, parent)) = stac.pop() { 683 | report_error(Some(open.span()), ret, "Unclosed group"); 684 | let child = mem::replace(&mut current, parent); 685 | current.push(TypeTree::Group(open, child, None)); 686 | } 687 | current 688 | } 689 | 690 | fn is_decl_kw(kw: &Ident) -> bool { 691 | kw == "struct" 692 | || kw == "enum" 693 | || kw == "union" 694 | || kw == "type" 695 | || kw == "fn" 696 | || kw == "mod" 697 | || kw == "trait" 698 | } 699 | 700 | fn report_error(span: Option, ret: &mut TokenStream, error: &str) { 701 | let error = format!( 702 | "{} error: {} - starting from:", 703 | env!("CARGO_PKG_NAME"), 704 | error 705 | ); 706 | match span { 707 | Some(span) => { 708 | quote_spanned! { 709 | span => compile_error!(#error); 710 | } 711 | .to_tokens(ret); 712 | } 713 | None => panic!("{}", error), 714 | } 715 | } 716 | 717 | pub fn flatten_empty_groups(ts: proc_macro2::TokenStream) -> proc_macro2::TokenStream { 718 | ts.into_iter() 719 | .flat_map(|tt| match tt { 720 | proc_macro2::TokenTree::Group(g) if g.delimiter() == proc_macro2::Delimiter::None => { 721 | flatten_empty_groups(g.stream()) 722 | } 723 | proc_macro2::TokenTree::Group(group) => { 724 | let inner = flatten_empty_groups(group.stream()); 725 | let mut ngroup = proc_macro2::Group::new(group.delimiter(), inner); 726 | ngroup.set_span(group.span()); 727 | once(proc_macro2::TokenTree::Group(ngroup)).collect() 728 | } 729 | x => once(x).collect(), 730 | }) 731 | .collect() 732 | } 733 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! ## Nested struct and enum definitions 2 | //! 3 | //! One of the best parts of Rust's ecosystem is `serde`, 4 | //! and how it allows to comfortably use native Rust types when working with 5 | //! serialized data in pretty much any format. 6 | //! 7 | //! Take this JSON object for example: 8 | //! ```json 9 | //! { 10 | //! "name": "asdf", 11 | //! "storage": { 12 | //! "diskSize": "10Gi", 13 | //! "storageTypes": { 14 | //! "hdd": false, 15 | //! "ssd": true 16 | //! } 17 | //! } 18 | //! } 19 | //! ``` 20 | //! If you have some practice, you can probably immediately imagine a set of Rust structs 21 | //! which the JSON object could deserialize into: 22 | //! ```no_run 23 | //! struct Resource { 24 | //! name: String, 25 | //! storage: Storage, 26 | //! } 27 | //! struct Storage { 28 | //! disk_size: String, 29 | //! storage_types: StorageTypes, 30 | //! } 31 | //! struct StorageTypes { 32 | //! hdd: bool, 33 | //! ssd: bool, 34 | //! } 35 | //! ``` 36 | //! Since Rust's structs are "flat", every JSON subobject needs its own struct, 37 | //! and they need to be typed out one next to the other, and not nested like the JSON object. 38 | //! This can get unwieldy for large objects with many fields and subobjects. 39 | //! 40 | //! What if instead, you could just create your structs in the same nested style? 41 | //! ```no_run 42 | //! # // Can't check whether these things are equal in doctests because I can only call public functions 43 | //! # structstruck::strike!{ 44 | //! struct Resource { 45 | //! name: String, 46 | //! storage: struct { 47 | //! disk_size: String, 48 | //! storage_types: struct { 49 | //! hdd: bool, 50 | //! ssd: bool, 51 | //! } 52 | //! } 53 | //! } 54 | //! # }; 55 | //! ``` 56 | //! This crate allows you to do exactly that, at the expense of one macro. 57 | //! 58 | //! ### Usage 59 | //! 60 | //! Wrap your nested struct into an invocation of `structstruck::strike!`. 61 | //! ```no_run 62 | //! structstruck::strike! { 63 | //! struct Outer { 64 | //! inner: struct { 65 | //! value: usize 66 | //! } 67 | //! } 68 | //! } 69 | //! ``` 70 | //! This will expand to flat struct definitions: 71 | //! ```no_run 72 | //! struct Outer { 73 | //! inner: Inner, 74 | //! } 75 | //! struct Inner { 76 | //! value: usize 77 | //! } 78 | //! ``` 79 | //! Since the inner struct's name was not given, it was automatically inferred from the field name 80 | //! (similarly done for tuple enum variants). 81 | //! 82 | //! The inferred name can be overwritten if necessary: 83 | //! ```no_run 84 | //! structstruck::strike! { 85 | //! struct Outer { 86 | //! inner: struct InNer { 87 | //! value: usize 88 | //! } 89 | //! } 90 | //! } 91 | //! ``` 92 | //! 93 | //! #### Supported declarations 94 | //! structstruck, despite its name, works with enums and structs, and with tuple and named variants. 95 | //! ```no_run 96 | //! structstruck::strike! { 97 | //! struct Outer { 98 | //! enum_demo: enum { 99 | //! NamedVariant { 100 | //! tuple_struct: struct (usize) 101 | //! } 102 | //! TupleVariant(struct InsideTupleVariant (isize)) 103 | //! } 104 | //! } 105 | //! } 106 | //! ``` 107 | //! This will generate the following declarations: 108 | //! ```no_run 109 | //! struct TupleStruct(usize); 110 | //! struct InsideTupleVariant(isize); 111 | //! enum EnumDemo { 112 | //! NamedVariant { tuple_struct: TupleStruct }, 113 | //! TupleVariant(InsideTupleVariant), 114 | //! } 115 | //! ``` 116 | //! 117 | //! #### Substructs in generics 118 | //! Declarations may appear inside generics arguments. (It works "as you would expect".) 119 | //! ```no_run 120 | //! structstruck::strike! { 121 | //! struct Parent { 122 | //! a: Option, 125 | //! b: Result< 126 | //! struct Then { 127 | //! d: u64, 128 | //! }, 129 | //! struct Else { 130 | //! e: u128, 131 | //! }, 132 | //! > 133 | //! } 134 | //! } 135 | //! ``` 136 | //! The above results in 137 | //! ```no_run 138 | //! struct A { 139 | //! c: u32, 140 | //! } 141 | //! struct Then { 142 | //! d: u64, 143 | //! } 144 | //! struct Else { 145 | //! e: u128, 146 | //! } 147 | //! struct Parent { 148 | //! a: Option, 149 | //! b: Result, 150 | //! } 151 | //! ``` 152 | //! (The structs themselves being generic is not supported yet(?).) 153 | //! 154 | //! #### Attributes 155 | //! Applying attributes (or doc comments) to a single inner struct would be syntactically awkward: 156 | //! ```no_run 157 | //! structstruck::strike! { 158 | //! struct Outer { 159 | //! documented: /** documentation */ struct {}, 160 | //! attributed: #[allow(madness)] struct {}, 161 | //! } 162 | //! } 163 | //! ``` 164 | //! Thus, `structstruck` allows to use inner attributes at the start of the struct declarations and automatically transforms them to outer attributes 165 | //! ```no_run 166 | //! structstruck::strike! { 167 | //! struct Outer { 168 | //! documented: struct { 169 | //! //! documentation 170 | //! }, 171 | //! attributed: struct { 172 | //! #![forbid(madness)] 173 | //! }, 174 | //! } 175 | //! } 176 | //! ``` 177 | //! 178 | //! To quickly apply attributes to all declarations, attributes can be wrapped in the `#[structstruck::each[…]]` 179 | //! pseudoattribute. 180 | //! ```no_run 181 | //! structstruck::strike! { 182 | //! // It's structstruck::each[…], not structstruck::each(…) 183 | //! // This appears to confuse even the rustdoc syntax highlighter 184 | //! #[structstruck::each[derive(Debug)]] 185 | //! struct Parent { 186 | //! a: Option, 189 | //! b: Result< 190 | //! struct Then { 191 | //! d: u64, 192 | //! }, 193 | //! struct Else { 194 | //! e: u128, 195 | //! }, 196 | //! > 197 | //! } 198 | //! } 199 | //! println!("{:#?}", Parent { ..todo!("value skipped for brevity") }); 200 | //! ``` 201 | //! 202 | //! The behavior of `each` can be influenced in two ways: 203 | //! * `structstruck::exclude_each` will ignore any attributes in `each` for the current struct only. 204 | //! * `structstruck::clear_each` will ignore any `structstruck::each` from parent structs for the current struct and children. 205 | //! 206 | //! The order of attributes does not matter. 207 | //! 208 | //! For example: 209 | //! ```no_run 210 | //! structstruck::strike! { 211 | //! struct A { 212 | //! #![structstruck::each[deny(unused)]] 213 | //! b: struct { 214 | //! #![structstruck::each[allow(unused)]] 215 | //! #![structstruck::skip_each] 216 | //! #![structstruck::clear_each] 217 | //! c: struct {} 218 | //! } 219 | //! } 220 | //! } 221 | //! # const A: Option = None; 222 | //! # fn foo() { A.unwrap().b; } 223 | //! ``` 224 | //! will place no attributes on `B` and only `allow(unused)` on `C`. 225 | //! 226 | //! #### Avoiding name collisions 227 | //! If you want include the parent struct name (or parent enum name and variant name) 228 | //! in the name of the child struct, add `#[structstruck::long_names]` to the struct. 229 | //! ```no_run 230 | //! structstruck::strike! { 231 | //! #[structstruck::long_names] 232 | //! struct Outer { 233 | //! inner: struct { value: usize } 234 | //! } 235 | //! } 236 | //! ``` 237 | //! This will generate the following declarations: 238 | //! ```no_run 239 | //! struct OuterInner { 240 | //! value: usize 241 | //! } 242 | //! struct Outer { 243 | //! inner: OuterInner, 244 | //! } 245 | //! ``` 246 | //! This can be combined with `structstruck::each` to use the full path of all ancestor struct names. 247 | //! ```no_run 248 | //! structstruck::strike! { 249 | //! #[structstruck::each[structstruck::long_names]] 250 | //! struct A { 251 | //! b: struct { 252 | //! c: struct { } 253 | //! } 254 | //! } 255 | //! } 256 | //! ``` 257 | //! will generate three structs, named `A`, `AB`, and `ABC`. 258 | //! 259 | //! This is useful to prevent collisions when using the same field name multiple times or a type with the same name as a field exists. 260 | //! 261 | //! ### Missing features, limitations 262 | //! * Generic parameter constraints need to be repeated for each struct. 263 | //! * Usage error handling is minimal. 264 | //! * rustfmt really doesn't play along. 265 | 266 | mod imp; 267 | #[cfg(test)] 268 | mod test; 269 | 270 | /// Main functionality 271 | /// 272 | /// See crate level documentation. 273 | // I would have loved to make this a proc_macro_attribute. 274 | // But it seems those require that the declarations are actual valid Rust. 275 | // proc_macro relaxes this to valid TokenTrees. 276 | #[proc_macro] 277 | pub fn strike(item: proc_macro::TokenStream) -> proc_macro::TokenStream { 278 | let mut ret = Default::default(); 279 | let item = imp::flatten_empty_groups(item.into()); 280 | imp::recurse_through_definition(item, vec![], false, &mut ret); 281 | ret.into() 282 | } 283 | -------------------------------------------------------------------------------- /src/test.rs: -------------------------------------------------------------------------------- 1 | use crate::imp::{recurse_through_definition, type_tree, TypeTree}; 2 | use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; 3 | use quote::quote; 4 | 5 | fn pretty(plan: proc_macro2::TokenStream) -> String { 6 | let planstr = plan.to_string(); 7 | let plan = &syn::parse_file(&planstr); 8 | let plan = match plan { 9 | Ok(plan) => plan, 10 | Err(_) => return planstr, 11 | }; 12 | prettyplease::unparse(plan) 13 | } 14 | 15 | fn check(nested: proc_macro2::TokenStream, planexpected: proc_macro2::TokenStream) { 16 | let mut plan = proc_macro2::TokenStream::new(); 17 | recurse_through_definition(nested, vec![], false, &mut plan); 18 | // No Eq implementations. :/ 19 | let plan = pretty(plan); 20 | let planexpected = pretty(planexpected); 21 | assert!( 22 | plan == planexpected, 23 | "\n left: {}\n right: {}", 24 | plan, 25 | planexpected 26 | ); 27 | } 28 | 29 | #[test] 30 | fn strikethrough_derive() { 31 | let from = quote! { 32 | #[structstruck::each[striked_attr]] 33 | #[structstruck::each[derive(Debug, Default, PartialEq)]] 34 | #[gubbel] 35 | struct Parent { 36 | a: #[gobbel] struct { 37 | b: struct Shared { d: i32 }, 38 | c: Shared, 39 | }, 40 | e: u32, 41 | } 42 | }; 43 | 44 | let out = quote! { 45 | #[striked_attr] 46 | #[derive(Debug, Default, PartialEq)] 47 | struct Shared { 48 | d: i32 49 | } 50 | #[striked_attr] 51 | #[derive(Debug, Default, PartialEq)] 52 | #[gobbel] 53 | struct A { 54 | b: Shared, 55 | c: Shared, 56 | } 57 | #[striked_attr] 58 | #[derive(Debug, Default, PartialEq)] 59 | #[gubbel] 60 | struct Parent { 61 | a: A, 62 | e: u32, 63 | } 64 | }; 65 | check(from, out); 66 | } 67 | 68 | #[test] 69 | fn explicit_pub() { 70 | let from = quote! { 71 | struct Parent { 72 | a: pub struct { 73 | c: u32, 74 | }, 75 | b: pub(crate) struct { 76 | d: u64, 77 | }, 78 | } 79 | }; 80 | let out = quote! { 81 | pub struct A { 82 | c: u32, 83 | } 84 | pub(crate) struct B { 85 | d: u64, 86 | } 87 | struct Parent { 88 | a: A, 89 | b: B, 90 | } 91 | }; 92 | check(from, out); 93 | } 94 | 95 | /// If the field is pub, its type must also be 96 | /// 97 | /// (But only if it's fully pub. pub(crate) e.g. doesn't require anything.) 98 | #[test] 99 | fn implicit_pub() { 100 | let from = quote! { 101 | struct Parent { 102 | pub pub_to_none: struct {}, 103 | pub(crate) pub_crate_ign: struct {}, 104 | pub no_overwrite: pub(crate) struct {}, 105 | } 106 | }; 107 | let out = quote! { 108 | pub struct PubToNone {} 109 | struct PubCrateIgn {} 110 | pub(crate) struct NoOverwrite {} 111 | struct Parent { 112 | pub pub_to_none: PubToNone, 113 | pub(crate) pub_crate_ign: PubCrateIgn, 114 | pub no_overwrite: NoOverwrite, 115 | } 116 | }; 117 | check(from, out); 118 | } 119 | 120 | #[test] 121 | fn in_generics() { 122 | let from = quote! { 123 | struct Parent { 124 | a: Option, 127 | b: Result< 128 | struct Then { 129 | d: u64, 130 | }, 131 | struct Else { 132 | e: u128, 133 | }, 134 | > 135 | } 136 | }; 137 | let out = quote! { 138 | struct A { 139 | c: u32, 140 | } 141 | struct Then { 142 | d: u64, 143 | } 144 | struct Else { 145 | e: u128, 146 | } 147 | struct Parent { 148 | a: Option, 149 | b: Result 150 | } 151 | }; 152 | check(from, out); 153 | } 154 | 155 | #[test] 156 | fn enum_named() { 157 | let from = quote! { 158 | enum Parent { 159 | A { 160 | a: enum { Foo { b: i8 } }, 161 | c: i16 162 | } 163 | B {} 164 | } 165 | }; 166 | let out = quote! { 167 | enum A { 168 | Foo { b: i8 } 169 | } 170 | enum Parent { 171 | A { a: A, c: i16 }, 172 | B {} 173 | } 174 | }; 175 | check(from, out); 176 | } 177 | 178 | #[test] 179 | fn tupledec() { 180 | let from = quote! { 181 | struct Parent { 182 | a: struct (i16), 183 | b: struct (struct Bar { bar: i64 }), 184 | c: enum { Foo(struct(i32))} 185 | } 186 | }; 187 | let out = quote! { 188 | struct A (i16); 189 | struct Bar { bar: i64 } 190 | struct B (Bar); 191 | struct Foo (i32); 192 | enum C { Foo (Foo) } 193 | struct Parent { a : A , b : B , c : C } 194 | }; 195 | check(from, out); 196 | } 197 | 198 | #[test] 199 | fn tuples_need_semicolon_bug() { 200 | let from = quote! { 201 | struct Outer { 202 | enum_demo: enum { 203 | NamedVariant { 204 | tuple_struct: struct (usize) 205 | } 206 | TupleVariant(struct (isize)) 207 | } 208 | } 209 | }; 210 | let out = quote! { 211 | struct TupleStruct (usize); 212 | struct TupleVariant (isize); 213 | enum EnumDemo { 214 | NamedVariant { tuple_struct : TupleStruct } , 215 | TupleVariant (TupleVariant) 216 | } 217 | struct Outer { enum_demo : EnumDemo } 218 | }; 219 | check(from, out); 220 | } 221 | 222 | #[test] 223 | fn double_generics_bug() { 224 | let from = quote! { 225 | pub struct EventSourceSpec { 226 | pub kafka: Option >, 231 | } 232 | }; 233 | let out = quote! { 234 | pub struct KafkaSourceSpec { pub url : String , } 235 | pub struct EventSourceSpec { pub kafka : Option < HashMap < String , KafkaSourceSpec > > , } 236 | }; 237 | check(from, out); 238 | } 239 | 240 | #[test] 241 | fn triple_generics() { 242 | let from = quote! { 243 | struct A ( 244 | Option > > 245 | ); 246 | }; 247 | let out = quote! { 248 | struct B(); 249 | struct C(); 250 | struct A(Option > > ); 251 | }; 252 | check(from, out); 253 | } 254 | 255 | #[test] 256 | fn raw_identifier_panic_bug() { 257 | let plain = quote! { 258 | struct A { 259 | r#type: () // This was actually enough for a segfault 260 | }; 261 | }; 262 | recurse_through_definition(plain, vec![], false, &mut proc_macro2::TokenStream::new()); 263 | } 264 | 265 | #[test] 266 | fn raw_identifier_as_name() { 267 | let from = quote! { 268 | struct A { r#type: struct () }; 269 | }; 270 | let out = quote! { 271 | struct Type(); 272 | struct A { r#type: Type } 273 | }; 274 | check(from, out); 275 | } 276 | 277 | #[test] 278 | fn type_tree_parsing() { 279 | let inp = quote! { 280 | Hoge < Huge < Hage, Hegge >, struckt Hacke<'a,U> where P: E<> {} > 281 | }; 282 | let expect = quote! { 283 | Hoge ( Huge ( Hage, Hegge ), struckt Hacke('a,U) where P: E() {} ) 284 | }; 285 | let inp = inp.into_iter().collect::>(); 286 | let mut out = proc_macro2::TokenStream::new(); 287 | let res = type_tree(&inp, &mut out); 288 | fn tost(inp: Vec) -> TokenStream { 289 | inp.into_iter() 290 | .map(|t| match t { 291 | TypeTree::Group(_, s, _) => { 292 | TokenTree::Group(Group::new(Delimiter::Parenthesis, tost(s))) 293 | } 294 | TypeTree::Token(t) => t.clone(), 295 | }) 296 | .collect() 297 | } 298 | println!("{}", out); 299 | assert_eq!(tost(res).to_string(), expect.to_string()); 300 | assert!(out.is_empty()); 301 | } 302 | 303 | #[test] 304 | fn generics_on_def() { 305 | let from = quote! { 306 | struct Outer { 307 | unnamed: struct{t: T}, 308 | whatev: struct Named{t: T}, 309 | }; 310 | }; 311 | let out = quote! { 312 | struct Unnamed < T > { t : T } 313 | struct Named < T > { t : T } 314 | struct Outer { unnamed : Unnamed , whatev : Named , } 315 | }; 316 | check(from, out); 317 | } 318 | 319 | #[test] 320 | fn pub_enum() { 321 | let from = quote! { 322 | enum Opts { 323 | Login(pub struct { 324 | hs: Url, 325 | }), 326 | Run(pub struct { 327 | channel: Option, 328 | }), 329 | } 330 | }; 331 | let out = quote! { 332 | pub struct Login { 333 | hs: Url, 334 | } 335 | 336 | pub struct Run { 337 | channel: Option, 338 | } 339 | 340 | enum Opts { 341 | Login(Login), 342 | Run(Run), 343 | } 344 | }; 345 | check(from, out); 346 | } 347 | 348 | #[test] 349 | fn tuple_pub_struct() { 350 | let from = quote! { 351 | struct Foo(pub struct Bar(u32)); 352 | }; 353 | let out = quote! { 354 | pub struct Bar(u32); 355 | struct Foo(Bar); 356 | }; 357 | check(from, out); 358 | } 359 | 360 | #[test] 361 | fn pub_tuple_pub_struct() { 362 | let from = quote! { 363 | struct Foo(pub pub struct Bar(u32)); 364 | }; 365 | let out = quote! { 366 | pub struct Bar(u32); 367 | struct Foo(pub Bar); 368 | }; 369 | check(from, out); 370 | } 371 | 372 | #[test] 373 | fn public_enum() { 374 | let from = quote! { 375 | enum Outer { 376 | Struct(pub pub struct { a: Zing }), 377 | Enum(pub pub enum { A, B, C }), 378 | }; 379 | }; 380 | let out = quote! { 381 | pub struct Struct { a : Zing } 382 | pub enum Enum { A , B , C } 383 | enum Outer { 384 | Struct (pub Struct) , 385 | Enum (pub Enum) , 386 | } 387 | }; 388 | check(from, out); 389 | } 390 | 391 | #[test] 392 | fn inner_comment() { 393 | // Doc comments just desugar to #[doc = r"…"], but whatev, I'll test both. 394 | let from = quote! { 395 | struct Struck { 396 | //! Foo 397 | #![bar] 398 | blubb: i32 399 | }; 400 | }; 401 | let out = quote! { 402 | /// Foo 403 | #[bar] 404 | struct Struck { blubb: i32 } 405 | }; 406 | check(from, out); 407 | } 408 | 409 | #[test] 410 | fn inner_comment_as_in_doc() { 411 | let from = quote! { 412 | struct Outer { 413 | documented: struct { 414 | //! documentation 415 | }, 416 | attributed: struct { 417 | #![attribute] 418 | }, 419 | } 420 | }; 421 | let out = quote! { 422 | struct Outer { 423 | documented: /** documentation*/ struct {}, 424 | attributed: #[attribute] struct {}, 425 | } 426 | }; 427 | let mut rout = Default::default(); 428 | recurse_through_definition(out, vec![], false, &mut rout); 429 | check(from, rout); 430 | } 431 | 432 | #[test] 433 | fn strikethrough_weird() { 434 | let out = quote! { 435 | #[structstruck::each = foo] 436 | struct struct { } 437 | }; 438 | let mut rout = Default::default(); 439 | recurse_through_definition(out, vec![], false, &mut rout); 440 | assert!(rout 441 | .into_iter() 442 | .any(|t| matches!(t, TokenTree::Ident(kw) if kw == "compile_error"))); 443 | } 444 | 445 | #[test] 446 | fn pub_markers_sane() { 447 | use crate::imp::*; 448 | assert!(is_plain_pub(&Some(make_pub_marker()))) 449 | } 450 | 451 | #[test] 452 | fn issue_2() { 453 | let from = quote! { 454 | enum Expr<'src> { 455 | Binary(struct<'src> { 456 | left: Box>, 457 | operator: BinaryOp, 458 | right: Box>, 459 | }), 460 | Literal(enum<'src> { 461 | StringLit(&'src str), 462 | NumLit(&'src str), 463 | }), 464 | } 465 | }; 466 | let out = quote! { 467 | struct Binary < 'src > { left : Box < Expr < 'src >> , operator : BinaryOp , right : Box < Expr < 'src >> , } 468 | enum Literal < 'src > { StringLit (& 'src str) , NumLit (& 'src str) , } 469 | enum Expr < 'src > { Binary (Binary<'src>) , Literal (Literal<'src>) , } 470 | }; 471 | check(from, out) 472 | } 473 | 474 | #[test] 475 | fn pub_enum_autopubs() { 476 | let from = quote! { 477 | pub enum Outer { 478 | An(struct ()), 479 | Ny(struct {}), 480 | }; 481 | }; 482 | let out = quote! { 483 | pub struct An (); 484 | pub struct Ny {} 485 | pub enum Outer { 486 | An(An), 487 | Ny(Ny), 488 | } 489 | }; 490 | check(from, out); 491 | } 492 | 493 | #[test] 494 | fn missing_comma_issue4() { 495 | let from = quote! { 496 | struct Incorrect { 497 | eater: struct { 498 | stomach: (), 499 | } // notice the missing comma 500 | eaten: struct { 501 | apple: bool, 502 | } 503 | } 504 | }; 505 | let mut to = TokenStream::new(); 506 | recurse_through_definition(from, vec![], false, &mut to); 507 | assert!(to.clone().into_iter().any(|tok| match tok { 508 | TokenTree::Ident(id) => id == "compile_error", 509 | _ => false, 510 | })); 511 | } 512 | 513 | #[test] 514 | /// TODO 515 | fn not_quite_fixed_issue4() { 516 | let from = quote! { 517 | struct Incorrect { 518 | eater: struct { 519 | stomach: (), 520 | } // notice the missing comma 521 | you can still put arbitrary junk here and venial will ignore it ;( 522 | ! ) 42 523 | } 524 | }; 525 | let out = quote! { 526 | struct Eater { 527 | stomach: (), 528 | } 529 | struct Incorrect { 530 | eater: Eater 531 | } 532 | }; 533 | check(from, out); 534 | } 535 | 536 | #[test] 537 | fn issue4_variant() { 538 | let from = quote! { 539 | struct Incorrect { 540 | uff: Result 541 | } 542 | }; 543 | let mut to = TokenStream::new(); 544 | recurse_through_definition(from, vec![], false, &mut to); 545 | assert!(to.clone().into_iter().any(|tok| match tok { 546 | TokenTree::Ident(id) => id == "compile_error", 547 | _ => false, 548 | })); 549 | } 550 | 551 | #[test] 552 | fn issue5_unions() { 553 | let from = quote! { 554 | struct x_thing { 555 | a: union { 556 | value: u32, 557 | b: struct { 558 | thing_a: TypeA, 559 | thing_b: TypeB, 560 | }, 561 | }, 562 | some_data: [char; 123], 563 | } 564 | }; 565 | let out = quote! { 566 | struct B { 567 | thing_a: TypeA, 568 | thing_b: TypeB, 569 | } 570 | union A { 571 | value: u32, 572 | b: B, 573 | } 574 | struct x_thing { 575 | a: A, 576 | some_data: [char; 123], 577 | } 578 | }; 579 | check(from, out); 580 | } 581 | 582 | #[test] 583 | fn typedef() { 584 | let from = quote! { 585 | struct Thing { 586 | foo: type = u32, 587 | } 588 | }; 589 | let out = quote! { 590 | type Foo = u32; 591 | struct Thing { 592 | foo: Foo, 593 | } 594 | }; 595 | check(from, out); 596 | } 597 | 598 | #[test] 599 | fn issue6_path() { 600 | let from = quote! { 601 | struct ItemDefine { 602 | semantic_token: keywords::semantic, 603 | ident: Ident, 604 | semantic_fields: struct { 605 | brace_token: token::Brace, 606 | fields: Vec, 607 | } 608 | } 609 | }; 610 | let out = quote! { 611 | struct SemanticFields { 612 | brace_token: token::Brace, 613 | fields: Vec, 614 | } 615 | struct ItemDefine { 616 | semantic_token: keywords::semantic, 617 | ident: Ident, 618 | semantic_fields: SemanticFields, 619 | } 620 | }; 621 | check(from, out); 622 | } 623 | 624 | #[test] 625 | fn two_tuple_values() { 626 | let from = quote! { 627 | enum Parent { 628 | Tuple(struct{b: u8}, struct{c: u8}) 629 | } 630 | }; 631 | let out = quote! { 632 | struct Tuple { b: u8 } 633 | struct Tuple1 { c: u8 } 634 | enum Parent { 635 | Tuple(Tuple, Tuple1), 636 | } 637 | }; 638 | check(from, out); 639 | } 640 | 641 | /* repeating some of the tests above, but with path names */ 642 | 643 | #[test] 644 | fn path_in_generics() { 645 | let from = quote! { 646 | #[structstruck::each[structstruck::long_names]] 647 | struct Parent { 648 | a: Option, 651 | b: Result< 652 | struct Then { 653 | d: u64, 654 | }, 655 | struct Else { 656 | e: u128, 657 | }, 658 | > 659 | } 660 | }; 661 | let out = quote! { 662 | struct ParentA { 663 | c: u32, 664 | } 665 | struct Then { 666 | d: u64, 667 | } 668 | struct Else { 669 | e: u128, 670 | } 671 | struct Parent { 672 | a: Option, 673 | b: Result 674 | } 675 | }; 676 | check(from, out); 677 | } 678 | #[test] 679 | fn path_enum_named() { 680 | let from = quote! { 681 | #[structstruck::each[structstruck::long_names]] 682 | enum Parent { 683 | A { 684 | a: enum { Foo { b: i8 } }, 685 | c: i16 686 | } 687 | B {} 688 | } 689 | }; 690 | let out = quote! { 691 | enum ParentAA { 692 | Foo { b: i8 } 693 | } 694 | enum Parent { 695 | A { a: ParentAA, c: i16 }, 696 | B {} 697 | } 698 | }; 699 | check(from, out); 700 | } 701 | 702 | #[test] 703 | fn path_tupledec() { 704 | let from = quote! { 705 | #[structstruck::each[structstruck::long_names]] 706 | struct Parent { 707 | a: struct (i16), 708 | b: struct (struct Bar { bar: i64 }), 709 | c: enum { Foo(struct(i32))} 710 | } 711 | }; 712 | let out = quote! { 713 | struct ParentA (i16); 714 | struct Bar { bar: i64 } 715 | struct ParentB (Bar); 716 | struct ParentCFoo (i32); 717 | enum ParentC { Foo (ParentCFoo) } 718 | struct Parent { a : ParentA , b : ParentB , c : ParentC } 719 | }; 720 | check(from, out); 721 | } 722 | 723 | #[test] 724 | fn path_raw_identifier_as_name() { 725 | let from = quote! { 726 | #[structstruck::each[structstruck::long_names]] 727 | struct A { r#type: struct () }; 728 | }; 729 | let out = quote! { 730 | struct AType(); 731 | struct A { r#type: AType } 732 | }; 733 | check(from, out); 734 | } 735 | 736 | #[test] 737 | fn path_generics_on_def() { 738 | let from = quote! { 739 | #[structstruck::each[structstruck::long_names]] 740 | struct Outer { 741 | unnamed: struct{t: T}, 742 | whatev: struct Named{t: T}, 743 | }; 744 | }; 745 | let out = quote! { 746 | struct OuterUnnamed < T > { t : T } 747 | struct Named < T > { t : T } 748 | struct Outer { unnamed : OuterUnnamed , whatev : Named , } 749 | }; 750 | check(from, out); 751 | } 752 | 753 | #[test] 754 | fn path_pub_enum() { 755 | let from = quote! { 756 | #[structstruck::each[structstruck::long_names]] 757 | enum Opts { 758 | Login(pub struct { 759 | hs: Url, 760 | }), 761 | Run(pub struct { 762 | channel: Option, 763 | }), 764 | } 765 | }; 766 | let out = quote! { 767 | pub struct OptsLogin { 768 | hs: Url, 769 | } 770 | 771 | pub struct OptsRun { 772 | channel: Option, 773 | } 774 | 775 | enum Opts { 776 | Login(OptsLogin), 777 | Run(OptsRun), 778 | } 779 | }; 780 | check(from, out); 781 | } 782 | 783 | #[test] 784 | fn path_issue_2() { 785 | let from = quote! { 786 | #[structstruck::each[structstruck::long_names]] 787 | enum Expr<'src> { 788 | Binary(struct<'src> { 789 | left: Box>, 790 | operator: BinaryOp, 791 | right: Box>, 792 | }), 793 | Literal(enum<'src> { 794 | StringLit(&'src str), 795 | NumLit(&'src str), 796 | }), 797 | } 798 | }; 799 | let out = quote! { 800 | struct ExprBinary < 'src > { left : Box < Expr < 'src >> , operator : BinaryOp , right : Box < Expr < 'src >> , } 801 | enum ExprLiteral < 'src > { StringLit (& 'src str) , NumLit (& 'src str) , } 802 | enum Expr < 'src > { Binary (ExprBinary<'src>) , Literal (ExprLiteral<'src>) , } 803 | }; 804 | check(from, out) 805 | } 806 | 807 | #[test] 808 | fn path_issue5_unions() { 809 | let from = quote! { 810 | #[structstruck::each[structstruck::long_names]] 811 | struct x_thing { 812 | a: union { 813 | value: u32, 814 | b: struct { 815 | thing_a: TypeA, 816 | thing_b: TypeB, 817 | }, 818 | }, 819 | some_data: [char; 123], 820 | } 821 | }; 822 | let out = quote! { 823 | struct XThingAB { 824 | thing_a: TypeA, 825 | thing_b: TypeB, 826 | } 827 | union XThingA { 828 | value: u32, 829 | b: XThingAB, 830 | } 831 | struct x_thing { 832 | a: XThingA, 833 | some_data: [char; 123], 834 | } 835 | }; 836 | check(from, out); 837 | } 838 | 839 | #[test] 840 | fn path_typedef() { 841 | let from = quote! { 842 | #[structstruck::each[structstruck::long_names]] 843 | struct Thing { 844 | foo: type = u32, 845 | } 846 | }; 847 | let out = quote! { 848 | type ThingFoo = u32; 849 | struct Thing { 850 | foo: ThingFoo, 851 | } 852 | }; 853 | check(from, out); 854 | } 855 | 856 | #[test] 857 | fn path_issue6_path() { 858 | let from = quote! { 859 | #[structstruck::each[structstruck::long_names]] 860 | struct ItemDefine { 861 | semantic_token: keywords::semantic, 862 | ident: Ident, 863 | semantic_fields: struct { 864 | brace_token: token::Brace, 865 | fields: Vec, 866 | } 867 | } 868 | }; 869 | let out = quote! { 870 | struct ItemDefineSemanticFields { 871 | brace_token: token::Brace, 872 | fields: Vec, 873 | } 874 | struct ItemDefine { 875 | semantic_token: keywords::semantic, 876 | ident: Ident, 877 | semantic_fields: ItemDefineSemanticFields, 878 | } 879 | }; 880 | check(from, out); 881 | } 882 | 883 | #[test] 884 | fn path_two_tuple_values() { 885 | let from = quote! { 886 | #[structstruck::each[structstruck::long_names]] 887 | struct Outer{ 888 | inner: struct{ 889 | value: struct{ 890 | a: u32, 891 | } 892 | } 893 | } 894 | }; 895 | let out = quote! { 896 | struct OuterInnerValue { 897 | a: u32 898 | } 899 | struct OuterInner{ 900 | value: OuterInnerValue 901 | } 902 | struct Outer{ 903 | inner: OuterInner 904 | } 905 | }; 906 | check(from, out); 907 | } 908 | 909 | #[test] 910 | fn nested() { 911 | let from = quote! { 912 | #[structstruck::each[structstruck::long_names]] 913 | enum Parent { 914 | Tuple(struct{b: u8}, struct{c: u8}) 915 | } 916 | }; 917 | let out = quote! { 918 | struct ParentTuple { b: u8 } 919 | struct ParentTuple1 { c: u8 } 920 | enum Parent { 921 | Tuple(ParentTuple, ParentTuple1), 922 | } 923 | }; 924 | check(from, out); 925 | } 926 | /* end repeated tests for path names */ 927 | 928 | #[test] 929 | fn path_deep_nesting() { 930 | let from = quote! { 931 | #[structstruck::each[structstruck::long_names]] 932 | struct A{ 933 | b: struct{ 934 | c: struct { 935 | d:() 936 | } 937 | } 938 | } 939 | }; 940 | let out = quote! { 941 | struct ABC{ 942 | d: () 943 | } 944 | struct AB{ 945 | c: ABC 946 | } 947 | struct A{ 948 | b: AB 949 | } 950 | }; 951 | check(from, out); 952 | } 953 | 954 | #[test] 955 | fn path_not_on_outermost() { 956 | let from = quote! { 957 | struct A { 958 | b: struct { 959 | #![structstruck::each[structstruck::long_names]] 960 | c: struct { 961 | d:() 962 | } 963 | } 964 | } 965 | }; 966 | let out = quote! { 967 | struct BC { 968 | d: () 969 | } 970 | struct B { 971 | c: BC 972 | } 973 | struct A { 974 | b: B 975 | } 976 | }; 977 | check(from, out); 978 | } 979 | 980 | #[test] 981 | fn not_path() { 982 | let from = quote! { 983 | #[structstruck::long_names] 984 | struct A { 985 | b: struct { 986 | c: struct { 987 | d: () 988 | } 989 | } 990 | } 991 | }; 992 | let out = quote! { 993 | struct C { 994 | d: () 995 | } 996 | struct AB { 997 | c: C 998 | } 999 | struct A { 1000 | b: AB 1001 | } 1002 | }; 1003 | check(from, out); 1004 | } 1005 | 1006 | #[test] 1007 | fn path_break_middle() { 1008 | let from = quote! { 1009 | #[structstruck::each[structstruck::long_names]] 1010 | struct A { 1011 | b: struct { 1012 | c: struct X { 1013 | d: struct {} 1014 | } 1015 | } 1016 | } 1017 | }; 1018 | let out = quote! { 1019 | struct XD { 1020 | } 1021 | struct X { 1022 | d: XD 1023 | } 1024 | struct AB { 1025 | c: X 1026 | } 1027 | struct A { 1028 | b: AB 1029 | } 1030 | }; 1031 | check(from, out); 1032 | } 1033 | 1034 | #[test] 1035 | fn strikethrough_deprecated() { 1036 | let out = quote! { 1037 | #[strikethrough[past]] 1038 | struct struct { } 1039 | }; 1040 | let mut rout = Default::default(); 1041 | recurse_through_definition(out, vec![], false, &mut rout); 1042 | let out = dbg!(rout.to_string()); 1043 | assert!(out.contains("deprecated")); 1044 | assert!(out.contains("structstruck::each")); 1045 | } 1046 | 1047 | #[test] 1048 | fn skip_reset() { 1049 | let from = quote! { 1050 | struct A { 1051 | #![structstruck::each[E1]] 1052 | b: struct { 1053 | #![structstruck::each[E2]] 1054 | #![structstruck::clear_each] 1055 | #![structstruck::each[E3]] 1056 | #![structstruck::skip_each] 1057 | c: struct C {} 1058 | } 1059 | } 1060 | }; 1061 | let out = quote! { 1062 | #[E2] 1063 | #[E3] 1064 | struct C {} 1065 | struct B { 1066 | c: C 1067 | } 1068 | #[E1] 1069 | struct A { 1070 | b: B 1071 | } 1072 | }; 1073 | check(from, out); 1074 | } 1075 | 1076 | #[test] 1077 | fn skip() { 1078 | let from = quote! { 1079 | #[structstruck::each[OnAllMinusC]] 1080 | struct A { 1081 | b: struct B { 1082 | c: 1083 | #[structstruck::skip_each] 1084 | struct C {} 1085 | } 1086 | } 1087 | }; 1088 | let out = quote! { 1089 | struct C {} 1090 | #[OnAllMinusC] 1091 | struct B { 1092 | c: C, 1093 | } 1094 | #[OnAllMinusC] 1095 | struct A { 1096 | b: B, 1097 | } 1098 | }; 1099 | check(from, out); 1100 | } 1101 | -------------------------------------------------------------------------------- /src/unvenial.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Group, TokenStream}; 2 | use quote::ToTokens; 3 | use venial::{Enum, NamedStructFields, Punctuated, TupleStructFields}; 4 | 5 | pub(crate) trait UpdateTokens { 6 | fn update_tokens(&mut self); 7 | } 8 | 9 | impl UpdateTokens for NamedStructFields { 10 | fn update_tokens(&mut self) { 11 | punctuated_to_group(&self.fields, &mut self.tk_braces); 12 | } 13 | } 14 | 15 | impl UpdateTokens for TupleStructFields { 16 | fn update_tokens(&mut self) { 17 | punctuated_to_group(&self.fields, &mut self.tk_parens); 18 | } 19 | } 20 | 21 | impl UpdateTokens for Enum { 22 | fn update_tokens(&mut self) { 23 | punctuated_to_group(&self.variants, &mut self.tk_braces); 24 | } 25 | } 26 | 27 | fn punctuated_to_group(from: &venial::Punctuated, to: &mut Group) { 28 | let mut group = TokenStream::new(); 29 | for (field, punct) in from.iter() { 30 | field.to_tokens(&mut group); 31 | punct.to_tokens(&mut group); 32 | } 33 | let mut group = Group::new(to.delimiter(), group); 34 | group.set_span(to.span()); 35 | *to = group; 36 | } 37 | 38 | pub(crate) fn modify_punctuated(modify: &mut Punctuated, mut f: impl FnMut(&mut T)) { 39 | let mut new: Punctuated = Default::default(); 40 | for (v, p) in modify.iter() { 41 | let mut v: T = v.clone(); 42 | f(&mut v); 43 | new.push(v, Some(p.clone())); 44 | } 45 | *modify = new; 46 | } 47 | --------------------------------------------------------------------------------