├── .github └── workflows │ └── test-suite.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── metastruct ├── .gitignore ├── Cargo.toml ├── examples │ └── hello_world.rs └── src │ └── lib.rs └── metastruct_macro ├── .gitignore ├── Cargo.toml ├── src ├── attributes.rs ├── exclude.rs ├── lib.rs ├── mapping.rs └── num_fields.rs └── tests ├── bimapping.rs ├── exclude_from_group.rs └── lifetime.rs /.github/workflows/test-suite.yml: -------------------------------------------------------------------------------- 1 | name: test-suite 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'pr/*' 8 | pull_request: 9 | env: 10 | # Deny warnings in CI 11 | RUSTFLAGS: "-D warnings" 12 | jobs: 13 | cargo-fmt: 14 | name: cargo-fmt 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Get latest version of stable Rust 19 | run: rustup update stable 20 | - name: Check formatting with cargo fmt 21 | run: cargo fmt --all -- --check 22 | test: 23 | strategy: 24 | matrix: 25 | os: [ubuntu-latest, macos-latest, windows-latest] 26 | runs-on: ${{ matrix.os }} 27 | name: test-${{ matrix.os }} 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Get latest version of stable Rust 31 | run: rustup update stable 32 | - name: Run tests 33 | run: cargo test --release 34 | - name: Check all examples, binaries, etc 35 | run: cargo check --all-targets 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "metastruct", 4 | "metastruct_macro", 5 | ] 6 | resolver = "2" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 to present, Sigma Prime and Metastruct contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `metastruct` 2 | 3 | Metastruct is Rust library for metaprogramming with struct fields. 4 | 5 | Some of the things you can do with `metastruct` include: 6 | 7 | - Iterate over a struct's fields. 8 | - Map a closure over all or some of a struct's fields. 9 | - Access the number of fields in a struct at compile-time via a `const`. 10 | 11 | This is achieved by a procedural macro, which generates `macro_rules!` macros. 12 | 13 | One way of understanding `metastruct` is as a shortcut to writing your own derive macros. If 14 | you have a trait that you'd like to implement on a one-off basis, metastruct can help you write 15 | that implementation without a derive macro. 16 | 17 | ## :construction: Under Construction :construction: 18 | 19 | This library is currently under construction and should not be considered stable. 20 | 21 | There's currently no documentation aside from a few scant code comments and examples/tests. 22 | 23 | ## License 24 | 25 | Apache 2.0 26 | -------------------------------------------------------------------------------- /metastruct/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | -------------------------------------------------------------------------------- /metastruct/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metastruct" 3 | version = "0.1.3" 4 | edition = "2021" 5 | description = "Abstractions for iterating and mapping over struct fields" 6 | keywords = ["field", "iterator", "macro"] 7 | categories = ["rust-patterns"] 8 | license = "Apache-2.0" 9 | readme = "../README.md" 10 | repository = "https://github.com/sigp/metastruct" 11 | 12 | [features] 13 | default = ["macro"] 14 | macro = ["metastruct_macro"] 15 | 16 | [dependencies] 17 | metastruct_macro = { version = "0.1.2", path = "../metastruct_macro", optional = true } 18 | -------------------------------------------------------------------------------- /metastruct/examples/hello_world.rs: -------------------------------------------------------------------------------- 1 | use metastruct::{metastruct, selectors::AllFields, NumFields}; 2 | use std::marker::PhantomData; 3 | 4 | #[metastruct( 5 | mappings( 6 | map_numeric_fields_of_obj(exclude(y)), 7 | map_mut_numeric_fields_of_obj(exclude(y), mutable), 8 | ), 9 | num_fields(all(), numeric(selector = "NumericFields", exclude(y))) 10 | )] 11 | pub struct Obj { 12 | pub x: u64, 13 | pub y: String, 14 | pub z: u8, 15 | #[metastruct(exclude)] 16 | pub _phantom: PhantomData<()>, 17 | } 18 | 19 | fn sum(obj: &Obj) -> usize { 20 | let mut total = 0usize; 21 | map_numeric_fields_of_obj!(obj, |_, x| total += *x as usize); 22 | total 23 | } 24 | 25 | fn increment_all(obj: &mut Obj) { 26 | map_mut_numeric_fields_of_obj!(obj, |_, x| { 27 | *x += 1; 28 | }); 29 | } 30 | 31 | fn main() { 32 | let mut obj = Obj { 33 | x: 10, 34 | y: "Hello world".to_string(), 35 | z: 5, 36 | _phantom: PhantomData, 37 | }; 38 | 39 | println!("initial sum: {}", sum(&obj)); 40 | increment_all(&mut obj); 41 | println!("after increment all: {}", sum(&obj)); 42 | 43 | println!( 44 | "num fields? {}/{} are numeric", 45 | >::NUM_FIELDS, 46 | >::NUM_FIELDS 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /metastruct/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "macro")] 2 | pub use metastruct_macro::metastruct; 3 | 4 | /// Trait for structs with a countable number of fields. 5 | /// 6 | /// The `Selector` type can be used to select different subsets of fields. 7 | /// 8 | /// Implementations of this trait are intended to be written using the `metastruct` macro 9 | /// and the `num_fields` attribute. 10 | pub trait NumFields { 11 | const NUM_FIELDS: usize; 12 | } 13 | 14 | pub mod selectors { 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 16 | pub enum AllFields {} 17 | } 18 | -------------------------------------------------------------------------------- /metastruct_macro/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | target 3 | -------------------------------------------------------------------------------- /metastruct_macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "metastruct_macro" 3 | version = "0.1.3" 4 | edition = "2021" 5 | description = "Abstractions for iterating and mapping over struct fields (proc macro crate)" 6 | keywords = ["field", "iterator", "macro"] 7 | categories = ["rust-patterns"] 8 | license = "Apache-2.0" 9 | readme = "../README.md" 10 | repository = "https://github.com/sigp/metastruct" 11 | 12 | [lib] 13 | name = "metastruct_macro" 14 | proc-macro = true 15 | 16 | [dependencies] 17 | darling = "0.13.0" 18 | itertools = "0.10.1" 19 | proc-macro2 = "1.0.32" 20 | quote = "1.0.10" 21 | syn = "1.0.82" 22 | smallvec = "1.8.0" 23 | -------------------------------------------------------------------------------- /metastruct_macro/src/attributes.rs: -------------------------------------------------------------------------------- 1 | //! Utilities to help with parsing configuration attributes. 2 | use darling::{Error, FromMeta}; 3 | use syn::{Ident, NestedMeta}; 4 | 5 | /// List of identifiers implementing `FromMeta`. 6 | /// 7 | /// Useful for imposing ordering, unlike the `HashMap` options provided by `darling`. 8 | #[derive(Debug)] 9 | pub struct IdentList { 10 | pub idents: Vec, 11 | } 12 | 13 | impl FromMeta for IdentList { 14 | fn from_list(items: &[NestedMeta]) -> Result { 15 | let idents = items 16 | .iter() 17 | .map(|nested_meta| { 18 | let meta = match nested_meta { 19 | NestedMeta::Meta(m) => m, 20 | NestedMeta::Lit(l) => { 21 | return Err(Error::custom(format!( 22 | "expected ident, got literal: {:?}", 23 | l 24 | ))) 25 | } 26 | }; 27 | let path = meta.path(); 28 | path.get_ident() 29 | .cloned() 30 | .ok_or(Error::custom(format!("can't parse as ident: {:?}", path))) 31 | }) 32 | .collect::>()?; 33 | Ok(Self { idents }) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /metastruct_macro/src/exclude.rs: -------------------------------------------------------------------------------- 1 | use crate::{attributes::IdentList, FieldOpts}; 2 | use syn::{Ident, Type}; 3 | 4 | pub(crate) fn calculate_excluded_fields<'a>( 5 | item_excludes: &'a Option, 6 | item_groups: &Option, 7 | fields: &'a [(Ident, Type)], 8 | field_opts: &[FieldOpts], 9 | ) -> Vec<&'a Ident> { 10 | item_excludes 11 | .as_ref() 12 | .into_iter() 13 | .map(|exclude| &exclude.idents) 14 | .flatten() 15 | .chain( 16 | fields 17 | .iter() 18 | .zip(field_opts) 19 | .filter_map(|((field_name, _), field_opts)| { 20 | let excluded_from_all = field_opts.exclude; 21 | let excluded_from_any_group = item_groups 22 | .as_ref() 23 | .and_then(|groups| { 24 | let excluded_groups = field_opts.exclude_from.as_ref()?; 25 | Some( 26 | groups 27 | .idents 28 | .iter() 29 | .any(|group| excluded_groups.idents.contains(group)), 30 | ) 31 | }) 32 | .unwrap_or(false); 33 | (excluded_from_all || excluded_from_any_group).then_some(field_name) 34 | }), 35 | ) 36 | .collect() 37 | } 38 | -------------------------------------------------------------------------------- /metastruct_macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use attributes::IdentList; 2 | use darling::FromMeta; 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use std::collections::HashMap; 6 | use std::iter::FromIterator; 7 | use syn::{parse_macro_input, Attribute, AttributeArgs, Ident, ItemStruct}; 8 | 9 | mod attributes; 10 | mod exclude; 11 | mod mapping; 12 | mod num_fields; 13 | 14 | #[derive(Debug, FromMeta)] 15 | struct MappingOpts { 16 | #[darling(default)] 17 | exclude: Option, 18 | #[darling(default)] 19 | mutable: bool, 20 | #[darling(default)] 21 | fallible: bool, 22 | #[darling(default)] 23 | groups: Option, 24 | } 25 | 26 | #[derive(Debug, FromMeta)] 27 | struct BiMappingOpts { 28 | other_type: Ident, 29 | #[darling(default)] 30 | self_by_value: bool, 31 | #[darling(default)] 32 | self_mutable: bool, 33 | #[darling(default)] 34 | other_by_value: bool, 35 | #[darling(default)] 36 | other_mutable: bool, 37 | #[darling(default)] 38 | exclude: Option, 39 | #[darling(default)] 40 | fallible: bool, 41 | #[darling(default)] 42 | groups: Option, 43 | } 44 | 45 | #[derive(Debug, FromMeta)] 46 | struct NumFieldsOpts { 47 | #[darling(default)] 48 | exclude: Option, 49 | #[darling(default)] 50 | selector: Option, 51 | #[darling(default)] 52 | groups: Option, 53 | } 54 | 55 | #[derive(Debug, Default, FromMeta)] 56 | struct FieldOpts { 57 | /// Exclude this field from *all* mapping macros. 58 | #[darling(default)] 59 | exclude: bool, 60 | /// Exclude this field from the named groups. 61 | /// 62 | /// The group names should match groups defined on the `MappingOpts` for one or more mappings. 63 | // FIXME(sproul): we currently don't verify this 64 | #[darling(default)] 65 | exclude_from: Option, 66 | } 67 | 68 | /// Top-level configuration via the `metastruct` attribute. 69 | #[derive(Debug, FromMeta)] 70 | struct StructOpts { 71 | #[darling(default)] 72 | mappings: HashMap, 73 | #[darling(default)] 74 | bimappings: HashMap, 75 | // FIXME(sproul): the `Ident` is kind of useless here, consider writing a custom FromMeta 76 | #[darling(default)] 77 | num_fields: HashMap, 78 | } 79 | 80 | #[proc_macro_attribute] 81 | pub fn metastruct(args: TokenStream, input: TokenStream) -> TokenStream { 82 | let attr_args = parse_macro_input!(args as AttributeArgs); 83 | let mut item = parse_macro_input!(input as ItemStruct); 84 | 85 | let type_name = &item.ident; 86 | 87 | // Generics used for impl blocks. 88 | let generics = &item.generics.split_for_impl(); 89 | 90 | let opts = StructOpts::from_list(&attr_args).unwrap(); 91 | 92 | let mut output_items: Vec = vec![]; 93 | 94 | // Collect field names and types. 95 | let fields = item 96 | .fields 97 | .iter() 98 | .map(|field| (field.ident.clone().expect(""), field.ty.clone())) 99 | .collect::>(); 100 | 101 | // Collect field options. 102 | let field_opts = item 103 | .fields 104 | .iter() 105 | .map(|field| { 106 | field 107 | .attrs 108 | .iter() 109 | .filter(|attr| is_metastruct_attr(attr)) 110 | .find_map(|attr| { 111 | let meta = attr.parse_meta().unwrap(); 112 | Some(FieldOpts::from_meta(&meta).unwrap()) 113 | }) 114 | .unwrap_or_default() 115 | }) 116 | .collect::>(); 117 | 118 | // Generate mapping macros. 119 | for (mapping_macro_name, mapping_opts) in &opts.mappings { 120 | output_items.push(mapping::generate_mapping_macro( 121 | mapping_macro_name, 122 | type_name, 123 | &fields, 124 | &field_opts, 125 | mapping_opts, 126 | )); 127 | } 128 | 129 | // Generate bi-mapping macros. 130 | for (mapping_macro_name, mapping_opts) in &opts.bimappings { 131 | output_items.push(mapping::generate_bimapping_macro( 132 | mapping_macro_name, 133 | type_name, 134 | &fields, 135 | &field_opts, 136 | mapping_opts, 137 | )); 138 | } 139 | 140 | // Generate `NumFields` implementations. 141 | for (_, num_fields_opts) in &opts.num_fields { 142 | output_items.push(num_fields::generate_num_fields_impl( 143 | type_name, 144 | generics, 145 | &fields, 146 | &field_opts, 147 | num_fields_opts, 148 | )); 149 | } 150 | 151 | // Output original struct definition after removing metastruct attributes from the fields. 152 | for field in &mut item.fields { 153 | field.attrs = discard_metastruct_attrs(&field.attrs); 154 | } 155 | output_items.push(quote! { #item }.into()); 156 | 157 | TokenStream::from_iter(output_items) 158 | } 159 | 160 | /// Keep all non-metastruct-related attributes from an array. 161 | fn discard_metastruct_attrs(attrs: &[Attribute]) -> Vec { 162 | attrs 163 | .iter() 164 | .filter(|attr| !is_metastruct_attr(attr)) 165 | .cloned() 166 | .collect() 167 | } 168 | 169 | /// Predicate for determining whether an attribute is a `metastruct` attribute. 170 | fn is_metastruct_attr(attr: &Attribute) -> bool { 171 | is_attr_with_ident(attr, "metastruct") 172 | } 173 | 174 | /// Predicate for determining whether an attribute has the given `ident` as its path. 175 | fn is_attr_with_ident(attr: &Attribute, ident: &str) -> bool { 176 | attr.path 177 | .get_ident() 178 | .map_or(false, |attr_ident| attr_ident.to_string() == ident) 179 | } 180 | -------------------------------------------------------------------------------- /metastruct_macro/src/mapping.rs: -------------------------------------------------------------------------------- 1 | use crate::{exclude::calculate_excluded_fields, BiMappingOpts, FieldOpts, MappingOpts}; 2 | use itertools::Itertools; 3 | use proc_macro::TokenStream; 4 | use quote::quote; 5 | use syn::{Ident, Type}; 6 | 7 | pub(crate) fn generate_mapping_macro( 8 | macro_name: &Ident, 9 | type_name: &Ident, 10 | fields: &[(Ident, Type)], 11 | field_opts: &[FieldOpts], 12 | mapping_opts: &MappingOpts, 13 | ) -> TokenStream { 14 | let exclude_idents = calculate_excluded_fields( 15 | &mapping_opts.exclude, 16 | &mapping_opts.groups, 17 | fields, 18 | field_opts, 19 | ); 20 | let (selected_fields, selected_field_types): (Vec<_>, Vec<_>) = fields 21 | .iter() 22 | .filter(|(field_name, _)| !exclude_idents.contains(&field_name)) 23 | .cloned() 24 | .unzip(); 25 | 26 | let field_reference = if mapping_opts.mutable { 27 | quote! { ref mut } 28 | } else { 29 | quote! { ref } 30 | }; 31 | 32 | let mapping_function_input_types = selected_field_types 33 | .iter() 34 | .map(|field_type| { 35 | if mapping_opts.mutable { 36 | quote! { mut #field_type } 37 | } else { 38 | quote! { #field_type } 39 | } 40 | }) 41 | .collect::>(); 42 | 43 | let function_call_exprs = selected_fields 44 | .iter() 45 | .map(|field| { 46 | if mapping_opts.fallible { 47 | quote! { __metastruct_f(__metastruct_i, #field)? } 48 | } else { 49 | quote! { __metastruct_f(__metastruct_i, #field) } 50 | } 51 | }) 52 | .collect::>(); 53 | 54 | quote! { 55 | #[macro_export] 56 | macro_rules! #macro_name { 57 | (&$lifetime:tt _, $v:expr, $f:expr) => { 58 | match $v { 59 | #type_name { 60 | #( 61 | #field_reference #selected_fields, 62 | )* 63 | .. 64 | } => { 65 | let mut __metastruct_i: usize = 0; 66 | #( 67 | let __metastruct_f: &mut dyn FnMut(usize, &$lifetime #mapping_function_input_types) -> _ = &mut $f; 68 | #function_call_exprs; 69 | __metastruct_i += 1; 70 | )* 71 | } 72 | } 73 | }; 74 | ($v:expr, $f:expr) => { 75 | #macro_name!(&'_ _, $v, $f) 76 | }; 77 | } 78 | } 79 | .into() 80 | } 81 | 82 | pub(crate) fn generate_bimapping_macro( 83 | macro_name: &Ident, 84 | left_type_name: &Ident, 85 | left_fields: &[(Ident, Type)], 86 | left_field_opts: &[FieldOpts], 87 | mapping_opts: &BiMappingOpts, 88 | ) -> TokenStream { 89 | let right_type_name = &mapping_opts.other_type; 90 | let exclude_idents = calculate_excluded_fields( 91 | &mapping_opts.exclude, 92 | &mapping_opts.groups, 93 | left_fields, 94 | left_field_opts, 95 | ); 96 | let (left_selected_fields, right_selected_fields, left_selected_field_types): ( 97 | Vec<_>, 98 | Vec<_>, 99 | Vec<_>, 100 | ) = left_fields 101 | .iter() 102 | .filter(|(field_name, _)| !exclude_idents.contains(&field_name)) 103 | .map(|(field_name, left_type)| { 104 | let right_field_name = Ident::new(&format!("{field_name}_r"), field_name.span()); 105 | (field_name, right_field_name, left_type) 106 | }) 107 | .multiunzip(); 108 | 109 | assert!( 110 | !mapping_opts.self_by_value || !mapping_opts.self_mutable, 111 | "self cannot be mapped both by value and by mutable reference" 112 | ); 113 | assert!( 114 | !mapping_opts.other_by_value || !mapping_opts.other_mutable, 115 | "other cannot be mapped both by value and by mutable reference" 116 | ); 117 | let (left_field_ref, left_field_ref_typ) = if mapping_opts.self_by_value { 118 | (quote! {}, quote! {}) 119 | } else if mapping_opts.self_mutable { 120 | (quote! { ref mut }, quote! { &'_ mut }) 121 | } else { 122 | (quote! { ref }, quote! { &'_ }) 123 | }; 124 | let (right_field_ref, right_field_ref_typ) = if mapping_opts.other_by_value { 125 | (quote! {}, quote! {}) 126 | } else if mapping_opts.other_mutable { 127 | (quote! { ref mut }, quote! { &'_ mut }) 128 | } else { 129 | (quote! { ref }, quote! { &'_ }) 130 | }; 131 | 132 | let mapping_function_types = left_selected_field_types 133 | .iter() 134 | .map(|field_type| { 135 | quote! { &mut dyn FnMut(usize, #left_field_ref_typ #field_type, #right_field_ref_typ _) -> _ } 136 | }) 137 | .collect::>(); 138 | 139 | let function_call_exprs = left_selected_fields 140 | .iter() 141 | .zip(&right_selected_fields) 142 | .map(|(left_field, right_field)| { 143 | if mapping_opts.fallible { 144 | quote! { __metastruct_f(__metastruct_i, #left_field, #right_field)? } 145 | } else { 146 | quote! { __metastruct_f(__metastruct_i, #left_field, #right_field) } 147 | } 148 | }) 149 | .collect::>(); 150 | 151 | quote! { 152 | #[macro_export] 153 | macro_rules! #macro_name { 154 | ($left:expr, $right:expr, $f:expr) => { 155 | match ($left, $right) { 156 | (#left_type_name { 157 | #( 158 | #left_field_ref #left_selected_fields, 159 | )* 160 | .. 161 | }, 162 | #right_type_name { 163 | #( 164 | #left_selected_fields: #right_field_ref #right_selected_fields, 165 | )* 166 | .. 167 | }) => { 168 | let mut __metastruct_i: usize = 0; 169 | #( 170 | let __metastruct_f: #mapping_function_types = &mut $f; 171 | #function_call_exprs; 172 | __metastruct_i += 1; 173 | )* 174 | } 175 | } 176 | } 177 | } 178 | } 179 | .into() 180 | } 181 | -------------------------------------------------------------------------------- /metastruct_macro/src/num_fields.rs: -------------------------------------------------------------------------------- 1 | use crate::{exclude::calculate_excluded_fields, FieldOpts, NumFieldsOpts}; 2 | use proc_macro::TokenStream; 3 | use quote::quote; 4 | use syn::{Ident, ImplGenerics, Type, TypeGenerics, WhereClause}; 5 | 6 | pub(crate) fn generate_num_fields_impl( 7 | type_name: &Ident, 8 | (impl_generics, ty_generics, where_clause): &(ImplGenerics, TypeGenerics, Option<&WhereClause>), 9 | fields: &[(Ident, Type)], 10 | field_opts: &[FieldOpts], 11 | num_fields_opts: &NumFieldsOpts, 12 | ) -> TokenStream { 13 | let (selector_ty, selector_ty_def) = if let Some(selector) = &num_fields_opts.selector { 14 | (quote! { #selector }, Some(quote! { pub enum #selector {} })) 15 | } else { 16 | (quote! { metastruct::selectors::AllFields }, None) 17 | }; 18 | 19 | let excluded_fields = calculate_excluded_fields( 20 | &num_fields_opts.exclude, 21 | &num_fields_opts.groups, 22 | fields, 23 | field_opts, 24 | ); 25 | let num_fields = fields 26 | .iter() 27 | .filter(|(field_name, _)| !excluded_fields.contains(&field_name)) 28 | .count(); 29 | 30 | quote! { 31 | #selector_ty_def 32 | 33 | impl #impl_generics metastruct::NumFields<#selector_ty> for #type_name #ty_generics 34 | #where_clause 35 | { 36 | const NUM_FIELDS: usize = #num_fields; 37 | } 38 | } 39 | .into() 40 | } 41 | -------------------------------------------------------------------------------- /metastruct_macro/tests/bimapping.rs: -------------------------------------------------------------------------------- 1 | use metastruct_macro::metastruct; 2 | 3 | #[metastruct(bimappings( 4 | bimap_foo(other_type = "Foo", self_mutable, other_by_value), 5 | bimap_foo_into_foo(other_type = "IntoFoo", self_mutable, other_by_value) 6 | ))] 7 | #[derive(Debug, Clone, PartialEq)] 8 | pub struct Foo { 9 | a: u64, 10 | b: u64, 11 | #[metastruct(exclude_from(copy))] 12 | c: String, 13 | } 14 | 15 | pub struct MyString(String); 16 | 17 | impl From for String { 18 | fn from(m: MyString) -> Self { 19 | m.0 20 | } 21 | } 22 | 23 | /// Type that has fields that can be converted to Foo's fields using `Into` 24 | pub struct IntoFoo { 25 | a: u32, 26 | b: u32, 27 | c: MyString, 28 | } 29 | 30 | #[test] 31 | fn bimap_self() { 32 | let mut x_foo = Foo { 33 | a: 0, 34 | b: 1, 35 | c: "X".to_string(), 36 | }; 37 | let y_foo = Foo { 38 | a: 1000, 39 | b: 2000, 40 | c: "Y".to_string(), 41 | }; 42 | 43 | bimap_foo!(&mut x_foo, y_foo.clone(), |_, x, y| { 44 | *x = y; 45 | }); 46 | 47 | assert_eq!(x_foo, y_foo); 48 | } 49 | 50 | #[test] 51 | fn bimap_into() { 52 | let mut x_foo = Foo { 53 | a: 0, 54 | b: 1, 55 | c: "X".to_string(), 56 | }; 57 | let y_foo = IntoFoo { 58 | a: 1000, 59 | b: 2000, 60 | c: MyString("Y".to_string()), 61 | }; 62 | 63 | fn set_from, U>(x: &mut T, y: U) { 64 | *x = y.into(); 65 | } 66 | 67 | bimap_foo_into_foo!(&mut x_foo, y_foo, |_, x, y| { 68 | set_from(x, y); 69 | }); 70 | 71 | assert_eq!(x_foo.a, 1000); 72 | assert_eq!(x_foo.b, 2000); 73 | assert_eq!(x_foo.c, "Y"); 74 | } 75 | -------------------------------------------------------------------------------- /metastruct_macro/tests/exclude_from_group.rs: -------------------------------------------------------------------------------- 1 | use metastruct_macro::metastruct; 2 | 3 | #[metastruct(mappings( 4 | neither_group(), 5 | only_group1(groups(group1)), 6 | only_group2(groups(group2)), 7 | both_groups(groups(group1, group2)) 8 | ))] 9 | pub struct Foo { 10 | both: u64, 11 | #[metastruct(exclude_from(group1))] 12 | group2: u64, 13 | #[metastruct(exclude_from(group2))] 14 | group1: u64, 15 | #[metastruct(exclude_from(group1, group2))] 16 | neither: u64, 17 | } 18 | 19 | fn sum_and_count_group1(foo: &Foo) -> (usize, u64) { 20 | let mut count = 0_usize; 21 | let mut total = 0_u64; 22 | only_group1!(foo, |_, x| { 23 | count += 1; 24 | total += *x 25 | }); 26 | (count, total) 27 | } 28 | 29 | fn sum_and_count_group2(foo: &Foo) -> (usize, u64) { 30 | let mut count = 0_usize; 31 | let mut total = 0_u64; 32 | only_group2!(foo, |_, x| { 33 | count += 1; 34 | total += *x 35 | }); 36 | (count, total) 37 | } 38 | 39 | fn sum_and_count_both(foo: &Foo) -> (usize, u64) { 40 | let mut count = 0_usize; 41 | let mut total = 0_u64; 42 | both_groups!(foo, |_, x| { 43 | count += 1; 44 | total += *x 45 | }); 46 | (count, total) 47 | } 48 | 49 | fn sum_and_count_neither(foo: &Foo) -> (usize, u64) { 50 | let mut count = 0_usize; 51 | let mut total = 0_u64; 52 | neither_group!(foo, |_, x| { 53 | count += 1; 54 | total += *x 55 | }); 56 | (count, total) 57 | } 58 | 59 | fn run_test(foo: Foo) { 60 | let (count1, sum1) = sum_and_count_group1(&foo); 61 | assert_eq!(count1, 2); 62 | assert_eq!(sum1, foo.both + foo.group1); 63 | 64 | let (count2, sum2) = sum_and_count_group2(&foo); 65 | assert_eq!(count2, 2); 66 | assert_eq!(sum2, foo.both + foo.group2); 67 | 68 | let (count_neither, sum_neither) = sum_and_count_neither(&foo); 69 | assert_eq!(count_neither, 4); 70 | assert_eq!( 71 | sum_neither, 72 | foo.both + foo.group1 + foo.group2 + foo.neither 73 | ); 74 | 75 | let (count_both, sum_both) = sum_and_count_both(&foo); 76 | assert_eq!(count_both, 1); 77 | assert_eq!(sum_both, foo.both); 78 | } 79 | 80 | #[test] 81 | fn exclude_from_groups() { 82 | run_test(Foo { 83 | both: 1, 84 | group2: 2, 85 | group1: 3, 86 | neither: 4, 87 | }); 88 | run_test(Foo { 89 | both: 99, 90 | group2: 1000, 91 | group1: 2, 92 | neither: 777777777, 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /metastruct_macro/tests/lifetime.rs: -------------------------------------------------------------------------------- 1 | use metastruct_macro::metastruct; 2 | 3 | #[metastruct(mappings(map_foo_fields()))] 4 | struct Foo { 5 | x: u64, 6 | y: u16, 7 | z: u32, 8 | } 9 | 10 | fn sum<'a>(total: &'a mut u64, foo: &'a Foo) { 11 | map_foo_fields!(&'a _, foo, |_, field| *total += *field as u64); 12 | } 13 | 14 | #[test] 15 | fn reference_with_lifetime() { 16 | let foo = Foo { x: 1, y: 2, z: 3 }; 17 | let mut total = 0; 18 | sum(&mut total, &foo); 19 | assert_eq!(total, 6); 20 | } 21 | --------------------------------------------------------------------------------