├── .github └── workflows │ ├── build.yml │ ├── coverage.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── TODO.md ├── struct-field-names-as-array-derive ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── attrs.rs │ └── lib.rs └── struct-field-names-as-array ├── Cargo.toml ├── LICENSE ├── README.md ├── src └── lib.rs └── tests ├── rename_all.rs └── tests.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push] 3 | jobs: 4 | CleanupAndTest: 5 | runs-on: ubuntu-latest 6 | container: 7 | image: rust:latest 8 | options: --security-opt seccomp=unconfined 9 | steps: 10 | - name: Add cargo features 11 | run: rustup component add rustfmt clippy 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | with: 15 | path: repo 16 | - name: Fmt + clippy 17 | run: | 18 | cd repo 19 | cargo fmt 20 | cargo clippy --all-features --all-targets --allow-dirty --fix 21 | - name: Toc 22 | run: | 23 | curl https://raw.githubusercontent.com/ekalinin/github-markdown-toc/master/gh-md-toc -o gh-md-toc 24 | chmod a+x gh-md-toc 25 | ./gh-md-toc --insert --no-backup --hide-footer --skip-header repo/README.md 26 | rm gh-md-toc 27 | - name: Apply cleanup 28 | uses: EndBug/add-and-commit@v9 29 | with: 30 | message: 'applying code formatting, lint fixes and toc creation' 31 | cwd: repo 32 | - name: Fail build if clippy finds any error or warning 33 | run: | 34 | cd repo 35 | cargo clippy --all-features --all-targets -- -D warnings -D clippy::pedantic 36 | - name: Run test suite 37 | run: | 38 | cd repo 39 | cargo test --all-features 40 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Codecov 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | permissions: 10 | contents: read 11 | jobs: 12 | Codecov: 13 | runs-on: ubuntu-latest 14 | container: 15 | image: xd009642/tarpaulin:latest 16 | options: --security-opt seccomp=unconfined 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | - name: Load cached dependencies 21 | uses: Swatinem/rust-cache@v2 22 | - name: Generate code coverage 23 | run: cargo tarpaulin --verbose --all-features --workspace --timeout 120 --out Xml 24 | - name: Upload to codecov.io 25 | uses: codecov/codecov-action@v3 26 | with: 27 | fail_ci_if_error: true 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - v[0-9]+.[0-9]+.[0-9]+ 6 | jobs: 7 | Publish: 8 | runs-on: ubuntu-latest 9 | container: 10 | image: rust:latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | - name: Install toml-cli 15 | run: cargo install toml-cli 16 | - name: Check Version 17 | run: | 18 | test "v$(toml get -r struct-field-names-as-array-derive/Cargo.toml package.version)" = "${{ github.ref_name }}" 19 | test "v$(toml get -r struct-field-names-as-array/Cargo.toml package.version)" = "${{ github.ref_name }}" 20 | test "$(toml get -r struct-field-names-as-array/Cargo.toml dependencies.struct-field-names-as-array-derive.version)" = "=$(echo ${{ github.ref_name }} | cut -c 2-)" 21 | - name: Publish 22 | run: | 23 | cargo publish -p struct-field-names-as-array-derive 24 | cargo publish -p struct-field-names-as-array 25 | env: 26 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 4 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [0.3.0] 7 | 8 | ### Added 9 | 10 | * `struct-field-names-as-array-derive` crate 11 | 12 | * `FieldNamesAsArray` trait 13 | 14 | * `FieldNamesAsSlice` trait 15 | 16 | * `FieldNamesAsSlice` procedural macro 17 | 18 | ### Changed 19 | 20 | * upgraded rust edition from 2018 to 2021 21 | 22 | * `struct-field-names-as-array-derive`: `syn v1 -> v2` 23 | 24 | ## [0.2.0] 25 | 26 | ### Added 27 | 28 | * `visibility` container attribute 29 | 30 | ### Changed 31 | 32 | * default visibility of `FIELD_NAMES_AS_ARRAY` now private 33 | 34 | * `FIELD_NAMES_AS_ARRAY` is now an array, not a slice 35 | 36 | 37 | ## [0.1.4] 38 | 39 | ### Added 40 | 41 | * `rename_all` container attribute 42 | 43 | 44 | ## [0.1.3] 45 | 46 | ### Added 47 | 48 | * documentation for the generated `FIELD_NAMES_AS_ARRAY` constant 49 | 50 | 51 | ## [0.1.2] 52 | 53 | ### Added 54 | 55 | * `field_names_as_array(skip)` attribute to `FieldNamesAsArray` 56 | 57 | 58 | ## [0.1.1] 59 | 60 | ### Changed 61 | 62 | * downgraded rust edition from `2021` to `2018` 63 | 64 | 65 | ## [0.1.0] 66 | 67 | ### Added 68 | 69 | * `FieldNamesAsArray` procedural macro 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ 4 | "struct-field-names-as-array", 5 | "struct-field-names-as-array-derive", 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-22 Jonas Fassbender 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documenta- 5 | tion files (the "Software"), to deal in the Software with- 6 | out restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, 8 | and/or sell copies of the Software, and to permit persons 9 | to whom the Software is furnished to do so, subject to the 10 | following conditions: 11 | 12 | The above copyright notice and this permission notice shall 13 | be included in all copies or substantial portions of the 14 | Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY 17 | KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 18 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 19 | PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS 20 | OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 21 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 22 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # struct-field-names-as-array 2 | 3 | [![Build Status](https://github.com/jofas/struct_field_names_as_array/actions/workflows/build.yml/badge.svg)](https://github.com/jofas/struct_field_names_as_array/actions/workflows/build.yml) 4 | [![Codecov](https://codecov.io/gh/jofas/struct_field_names_as_array/branch/master/graph/badge.svg?token=69YKZ1JIBK)](https://codecov.io/gh/jofas/struct_field_names_as_array) 5 | [![Latest Version](https://img.shields.io/crates/v/struct-field-names-as-array.svg)](https://crates.io/crates/struct-field-names-as-array) 6 | [![Downloads](https://img.shields.io/crates/d/struct-field-names-as-array?label=downloads)](https://crates.io/crates/struct-field-names-as-array) 7 | [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://docs.rs/struct-field-names-as-array/latest/struct_field_names_as_array) 8 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 9 | 10 | Provides the `FieldNamesAsArray` and `FieldNamesAsSlice` traits and 11 | procedural macros for deriving them. 12 | The traits contain associated constants 13 | (`FieldNamesAsArray::FIELD_NAMES_AS_ARRAY` and `FieldNamesAsSlice::FIELD_NAMES_AS_SLICE`) 14 | listing the field names of a struct. 15 | 16 | **Note:** The macros can only be derived from named structs. 17 | 18 | ## Table of Contents 19 | 20 | 21 | * [Usage](#usage) 22 | * [Attributes](#attributes) 23 | * [Container Attributes](#container-attributes) 24 | * [Rename all](#rename-all) 25 | * [Field Attributes](#field-attributes) 26 | * [Skip](#skip) 27 | * [Rename](#rename) 28 | 29 | 30 | ## Usage 31 | 32 | You can derive the `FieldNamesAsArray` and `FieldNamesAsSlice` macros 33 | like this: 34 | 35 | ```rust 36 | use struct_field_names_as_array::FieldNamesAsArray; 37 | 38 | #[derive(FieldNamesAsArray)] 39 | struct Foo { 40 | bar: String, 41 | baz: String, 42 | bat: String, 43 | } 44 | 45 | assert_eq!(Foo::FIELD_NAMES_AS_ARRAY, ["bar", "baz", "bat"]); 46 | ``` 47 | 48 | ```rust 49 | use struct_field_names_as_array::FieldNamesAsSlice; 50 | 51 | #[derive(FieldNamesAsSlice)] 52 | struct Foo { 53 | bar: String, 54 | baz: String, 55 | bat: String, 56 | } 57 | 58 | assert_eq!(Foo::FIELD_NAMES_AS_SLICE, ["bar", "baz", "bat"]); 59 | ``` 60 | 61 | ## Attributes 62 | 63 | The `FieldNamesAsArray` macro comes with the 64 | `field_names_as_array` attribute. 65 | Orthogonally, `FieldNamesAsSlice` supports the `field_names_as_slice` 66 | attribute with the same arguments. 67 | The arguments are listed below. 68 | 69 | ### Container Attributes 70 | 71 | Container attributes are global attributes that change the behavior 72 | of the whole field names collection, rather than that of a single field. 73 | 74 | #### Rename all 75 | 76 | The `rename_all` attribute renames every field of the struct according 77 | to the provided naming convention. 78 | This attribute works exactly like the [serde][serde_rename_all] 79 | equivalent. 80 | Supported are these naming conventions: 81 | 82 | - `lowercase` 83 | - `UPPERCASE` 84 | - `PascalCase` 85 | - `camelCase` 86 | - `snake_case` 87 | - `SCREAMING_SNAKE_CASE` 88 | - `kebab-case` 89 | - `SCREAMING-KEBAB-CASE` 90 | 91 | ```rust 92 | use struct_field_names_as_array::FieldNamesAsArray; 93 | 94 | #[derive(FieldNamesAsArray)] 95 | #[field_names_as_array(rename_all = "SCREAMING-KEBAB-CASE")] 96 | struct Foo { 97 | field_one: String, 98 | field_two: String, 99 | field_three: String, 100 | } 101 | 102 | assert_eq!( 103 | Foo::FIELD_NAMES_AS_ARRAY, 104 | ["FIELD-ONE", "FIELD-TWO", "FIELD-THREE"], 105 | ); 106 | ``` 107 | 108 | **Note:** Same as serde's implementation of `rename_all`, it is 109 | assumed that your field names follow the rust naming convention. 110 | Namely, all field names must be given in `snake_case`. 111 | If you don't follow this convention, applying `rename_all` may result 112 | in unexpected field names. 113 | 114 | ### Field Attributes 115 | 116 | Field attributes can be added to the fields of a named struct and 117 | change the behavior of a single field. 118 | 119 | #### Skip 120 | 121 | The `skip` attribute removes the field from the generated constant. 122 | 123 | ```rust 124 | use struct_field_names_as_array::FieldNamesAsSlice; 125 | 126 | #[derive(FieldNamesAsSlice)] 127 | struct Foo { 128 | bar: String, 129 | baz: String, 130 | #[field_names_as_slice(skip)] 131 | bat: String, 132 | } 133 | 134 | assert_eq!(Foo::FIELD_NAMES_AS_SLICE, ["bar", "baz"]); 135 | ``` 136 | 137 | #### Rename 138 | 139 | The `rename` attribute renames the field in the generated constant. 140 | 141 | ```rust 142 | use struct_field_names_as_array::FieldNamesAsArray; 143 | 144 | #[derive(FieldNamesAsArray)] 145 | struct Foo { 146 | bar: String, 147 | baz: String, 148 | #[field_names_as_array(rename = "foo")] 149 | bat: String, 150 | } 151 | 152 | assert_eq!(Foo::FIELD_NAMES_AS_ARRAY, ["bar", "baz", "foo"]); 153 | ``` 154 | 155 | [serde_rename_all]: https://serde.rs/container-attrs.html#rename_all 156 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * [x] build action 4 | 5 | * [x] badges 6 | 7 | * [ ] `flatten` attribute 8 | 9 | * [ ] `expand` attribute with optional separator 10 | 11 | * [ ] attribute hierarchy 12 | 13 | * [ ] example what this could be useful for 14 | (document databases: es example basic, skip, nested) 15 | 16 | * [ ] documentation for `FieldNamesAsArray` 17 | 18 | * [x] documentation for visibility 19 | 20 | * [x] tags (`crates.io` and github) 21 | 22 | * [x] clippy action 23 | 24 | * [x] `tok` action 25 | 26 | * [ ] release `v0.1.4` 27 | 28 | * [ ] allow access to the struct's fields via a field name 29 | -------------------------------------------------------------------------------- /struct-field-names-as-array-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "struct-field-names-as-array-derive" 3 | version = "0.3.0" 4 | authors = ["jofas "] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | description = "Procedural macros for the struct-field-names-as-array crate" 9 | keywords = ["macro", "proc_macro"] 10 | homepage = "https://github.com/jofas/struct_field_names_as_array" 11 | repository = "https://github.com/jofas/struct_field_names_as_array" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [dependencies] 19 | syn = { version = "2", features = ["derive", "printing", "extra-traits"] } 20 | quote = "1" 21 | proc-macro2 = "1" 22 | -------------------------------------------------------------------------------- /struct-field-names-as-array-derive/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /struct-field-names-as-array-derive/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /struct-field-names-as-array-derive/src/attrs.rs: -------------------------------------------------------------------------------- 1 | use syn::{meta::ParseNestedMeta, AttrStyle, Attribute, LitStr, Result}; 2 | 3 | pub trait ParseAttributes: Sized { 4 | fn default(attribute: &'static str) -> Self; 5 | fn parse_attribute(&mut self, m: ParseNestedMeta) -> Result<()>; 6 | 7 | fn parse_attributes(attribute_name: &'static str, attributes: &[Attribute]) -> Result { 8 | let mut res = Self::default(attribute_name); 9 | 10 | for attribute in attributes { 11 | if attribute.style != AttrStyle::Outer { 12 | continue; 13 | } 14 | 15 | if attribute.path().is_ident(attribute_name) { 16 | attribute.parse_nested_meta(|meta| res.parse_attribute(meta))?; 17 | } 18 | } 19 | 20 | Ok(res) 21 | } 22 | } 23 | 24 | pub struct ContainerAttributes { 25 | attribute: &'static str, 26 | rename_all: RenameAll, 27 | } 28 | 29 | impl ContainerAttributes { 30 | pub fn apply_to_field(&self, field: &str) -> String { 31 | self.rename_all.rename_field(field) 32 | } 33 | 34 | pub fn attribute(&self) -> &'static str { 35 | self.attribute 36 | } 37 | } 38 | 39 | impl ParseAttributes for ContainerAttributes { 40 | fn default(attribute: &'static str) -> Self { 41 | Self { 42 | attribute, 43 | rename_all: RenameAll::Snake, 44 | } 45 | } 46 | 47 | fn parse_attribute(&mut self, m: ParseNestedMeta) -> Result<()> { 48 | if m.path.is_ident("skip") { 49 | return Err(m.error("skip is a field attribute, not a container attribute")); 50 | } 51 | 52 | if m.path.is_ident("rename_all") { 53 | self.rename_all = RenameAll::from_str(&m.value()?.parse::()?.value()); 54 | return Ok(()); 55 | } 56 | 57 | Err(m.error("unknown attribute")) 58 | } 59 | } 60 | 61 | pub struct FieldAttributes { 62 | skip: bool, 63 | rename: Option, 64 | } 65 | 66 | impl FieldAttributes { 67 | pub fn apply_to_field(&self, field: &str) -> Option { 68 | if self.skip { 69 | return None; 70 | } 71 | 72 | if let Some(rename) = &self.rename { 73 | return Some(rename.to_owned()); 74 | } 75 | 76 | Some(field.to_owned()) 77 | } 78 | } 79 | 80 | impl ParseAttributes for FieldAttributes { 81 | fn default(_attribute: &'static str) -> Self { 82 | Self { 83 | skip: false, 84 | rename: None, 85 | } 86 | } 87 | 88 | fn parse_attribute(&mut self, m: ParseNestedMeta) -> Result<()> { 89 | if m.path.is_ident("rename_all") { 90 | return Err(m.error("rename_all is a container attribute, not a field attribute")); 91 | } 92 | 93 | if m.path.is_ident("skip") { 94 | self.skip = true; 95 | return Ok(()); 96 | } 97 | 98 | if m.path.is_ident("rename") { 99 | self.rename = Some(m.value()?.parse::()?.value()); 100 | return Ok(()); 101 | } 102 | 103 | Err(m.error("unknown attribute")) 104 | } 105 | } 106 | 107 | #[derive(Clone, Copy)] 108 | pub enum RenameAll { 109 | Lower, 110 | Upper, 111 | Pascal, 112 | Camel, 113 | Snake, 114 | ScreamingSnake, 115 | Kebab, 116 | ScreamingKebab, 117 | } 118 | 119 | impl RenameAll { 120 | const FROM_STR: &'static [(&'static str, Self)] = &[ 121 | ("lowercase", Self::Lower), 122 | ("UPPERCASE", Self::Upper), 123 | ("PascalCase", Self::Pascal), 124 | ("camelCase", Self::Camel), 125 | ("snake_case", Self::Snake), 126 | ("SCREAMING_SNAKE_CASE", Self::ScreamingSnake), 127 | ("kebab-case", Self::Kebab), 128 | ("SCREAMING-KEBAB-CASE", Self::ScreamingKebab), 129 | ]; 130 | 131 | fn from_str(s: &str) -> Self { 132 | for (v, r) in Self::FROM_STR { 133 | if v == &s { 134 | return *r; 135 | } 136 | } 137 | 138 | panic!("unable to parse rename_all rule: {s}"); 139 | } 140 | 141 | fn rename_field(self, v: &str) -> String { 142 | match self { 143 | Self::Lower | Self::Snake => v.to_owned(), 144 | Self::Upper | Self::ScreamingSnake => v.to_ascii_uppercase(), 145 | Self::Pascal => { 146 | let mut pascal = String::new(); 147 | let mut capitalize = true; 148 | for ch in v.chars() { 149 | if ch == '_' { 150 | capitalize = true; 151 | } else if capitalize { 152 | pascal.push(ch.to_ascii_uppercase()); 153 | capitalize = false; 154 | } else { 155 | pascal.push(ch); 156 | } 157 | } 158 | pascal 159 | } 160 | Self::Camel => { 161 | let pascal = Self::Pascal.rename_field(v); 162 | pascal[..1].to_ascii_lowercase() + &pascal[1..] 163 | } 164 | Self::Kebab => v.replace('_', "-"), 165 | Self::ScreamingKebab => Self::ScreamingSnake.rename_field(v).replace('_', "-"), 166 | } 167 | } 168 | } 169 | 170 | #[cfg(test)] 171 | mod tests { 172 | use super::RenameAll; 173 | 174 | #[test] 175 | fn rename_fields() { 176 | for &(original, upper, pascal, camel, screaming, kebab, screaming_kebab) in &[ 177 | ( 178 | "outcome", "OUTCOME", "Outcome", "outcome", "OUTCOME", "outcome", "OUTCOME", 179 | ), 180 | ( 181 | "very_tasty", 182 | "VERY_TASTY", 183 | "VeryTasty", 184 | "veryTasty", 185 | "VERY_TASTY", 186 | "very-tasty", 187 | "VERY-TASTY", 188 | ), 189 | ("a", "A", "A", "a", "A", "a", "A"), 190 | ("z42", "Z42", "Z42", "z42", "Z42", "z42", "Z42"), 191 | ] { 192 | assert_eq!(RenameAll::Upper.rename_field(original), upper); 193 | assert_eq!(RenameAll::Pascal.rename_field(original), pascal); 194 | assert_eq!(RenameAll::Camel.rename_field(original), camel); 195 | assert_eq!(RenameAll::Snake.rename_field(original), original); 196 | assert_eq!(RenameAll::ScreamingSnake.rename_field(original), screaming); 197 | assert_eq!(RenameAll::Kebab.rename_field(original), kebab); 198 | assert_eq!( 199 | RenameAll::ScreamingKebab.rename_field(original), 200 | screaming_kebab 201 | ); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /struct-field-names-as-array-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Derive macros for the 2 | //! [struct-field-names-as-array](https://crates.io/crates/struct-field-names-as-array) 3 | //! crate. 4 | //! 5 | //! Please refer to struct-field-names-as-array's 6 | //! [documentation](https://docs.rs/struct-field-names-as-array) 7 | //! for more information. 8 | 9 | extern crate proc_macro; 10 | use proc_macro::TokenStream; 11 | 12 | use quote::quote; 13 | use syn::punctuated::Punctuated; 14 | use syn::token::Comma; 15 | use syn::{ 16 | parse_macro_input, Data, DataStruct, DeriveInput, Error, Field, Fields, FieldsNamed, Result, 17 | }; 18 | 19 | mod attrs; 20 | 21 | use attrs::{ContainerAttributes, FieldAttributes, ParseAttributes}; 22 | 23 | #[allow(clippy::missing_panics_doc)] 24 | #[proc_macro_derive(FieldNamesAsArray, attributes(field_names_as_array))] 25 | pub fn derive_field_names_as_array(input: TokenStream) -> TokenStream { 26 | let input = parse_macro_input!(input as DeriveInput); 27 | 28 | let name = &input.ident; 29 | 30 | let (impl_generics, type_generics, where_clause) = &input.generics.split_for_impl(); 31 | 32 | let container_attributes = 33 | ContainerAttributes::parse_attributes("field_names_as_array", &input.attrs).unwrap(); 34 | 35 | let Data::Struct(DataStruct { 36 | fields: Fields::Named(FieldsNamed { named, .. }), 37 | .. 38 | }) = input.data 39 | else { 40 | panic!("Derive(FieldNamesAsArray) only applicable to named structs"); 41 | }; 42 | 43 | let field_names = field_names(named, &container_attributes).unwrap(); 44 | 45 | let len = field_names.len(); 46 | 47 | TokenStream::from(quote! { 48 | impl #impl_generics ::struct_field_names_as_array::FieldNamesAsArray<#len> for #name #type_generics #where_clause { 49 | #[doc=concat!("Generated array of field names for `", stringify!(#name #type_generics), "`.")] 50 | const FIELD_NAMES_AS_ARRAY: [&'static str; #len] = [#(#field_names),*]; 51 | } 52 | }) 53 | } 54 | 55 | #[allow(clippy::missing_panics_doc)] 56 | #[proc_macro_derive(FieldNamesAsSlice, attributes(field_names_as_slice))] 57 | pub fn derive_field_names_as_slice(input: TokenStream) -> TokenStream { 58 | let input = parse_macro_input!(input as DeriveInput); 59 | 60 | let name = &input.ident; 61 | 62 | let (impl_generics, type_generics, where_clause) = &input.generics.split_for_impl(); 63 | 64 | let container_attributes = 65 | ContainerAttributes::parse_attributes("field_names_as_slice", &input.attrs).unwrap(); 66 | 67 | let Data::Struct(DataStruct { 68 | fields: Fields::Named(FieldsNamed { named, .. }), 69 | .. 70 | }) = input.data 71 | else { 72 | panic!("Derive(FieldNamesAsSlice) only applicable to named structs"); 73 | }; 74 | 75 | let field_names = field_names(named, &container_attributes).unwrap(); 76 | 77 | TokenStream::from(quote! { 78 | impl #impl_generics ::struct_field_names_as_array::FieldNamesAsSlice for #name #type_generics #where_clause { 79 | #[doc=concat!("Generated slice of field names for `", stringify!(#name #type_generics), "`.")] 80 | const FIELD_NAMES_AS_SLICE: &'static [&'static str] = &[#(#field_names),*]; 81 | } 82 | }) 83 | } 84 | 85 | fn field_names( 86 | fields: Punctuated, 87 | container_attributes: &ContainerAttributes, 88 | ) -> Result> { 89 | let mut res = Vec::new(); 90 | 91 | for field in fields { 92 | let field_attributes = 93 | FieldAttributes::parse_attributes(container_attributes.attribute(), &field.attrs)?; 94 | 95 | let Some(field) = field.ident else { 96 | return Err(Error::new_spanned(field, "field must be a named field")); 97 | }; 98 | 99 | let field = container_attributes.apply_to_field(&field.to_string()); 100 | 101 | if let Some(field) = field_attributes.apply_to_field(&field) { 102 | res.push(field); 103 | } 104 | } 105 | 106 | Ok(res) 107 | } 108 | -------------------------------------------------------------------------------- /struct-field-names-as-array/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "struct-field-names-as-array" 3 | version = "0.3.0" 4 | authors = ["jofas "] 5 | edition = "2021" 6 | license = "MIT" 7 | readme = "README.md" 8 | description = "Crate for generating the field names of named structs as constants" 9 | keywords = ["reflection", "introspection"] 10 | homepage = "https://github.com/jofas/struct_field_names_as_array" 11 | repository = "https://github.com/jofas/struct_field_names_as_array" 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [package.metadata.docs.rs] 16 | all-features = true 17 | 18 | [features] 19 | default = ["derive"] 20 | derive = ["dep:struct-field-names-as-array-derive"] 21 | 22 | [dependencies] 23 | struct-field-names-as-array-derive = { path = "../struct-field-names-as-array-derive", version = "=0.3.0", optional = true } 24 | -------------------------------------------------------------------------------- /struct-field-names-as-array/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /struct-field-names-as-array/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /struct-field-names-as-array/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | 3 | /// Derives the [`FieldNamesAsArray`] trait. 4 | /// 5 | /// # Panics 6 | /// 7 | /// If the token stream is not coming from a named struct or if 8 | /// the `field_names_as_array` attribute is used wrongfully, deriving 9 | /// this macro will fail. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// use struct_field_names_as_array::FieldNamesAsArray; 15 | /// 16 | /// #[derive(FieldNamesAsArray)] 17 | /// struct Foo { 18 | /// bar: String, 19 | /// baz: String, 20 | /// bat: String, 21 | /// } 22 | /// 23 | /// assert_eq!(Foo::FIELD_NAMES_AS_ARRAY, ["bar", "baz", "bat"]); 24 | /// ``` 25 | /// 26 | #[cfg_attr(doc_cfg, doc(cfg(feature = "derive")))] 27 | pub use struct_field_names_as_array_derive::FieldNamesAsArray; 28 | 29 | /// Derives the [`FieldNamesAsSlice`] trait. 30 | /// 31 | /// # Panics 32 | /// 33 | /// If the token stream is not coming from a named struct or if 34 | /// the `field_names_as_array` attribute is used wrongfully, deriving 35 | /// this macro will fail. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ``` 40 | /// use struct_field_names_as_array::FieldNamesAsSlice; 41 | /// 42 | /// #[derive(FieldNamesAsSlice)] 43 | /// struct Foo { 44 | /// bar: String, 45 | /// baz: String, 46 | /// bat: String, 47 | /// } 48 | /// 49 | /// assert_eq!(Foo::FIELD_NAMES_AS_SLICE, ["bar", "baz", "bat"]); 50 | /// ``` 51 | /// 52 | #[cfg_attr(doc_cfg, doc(cfg(feature = "derive")))] 53 | pub use struct_field_names_as_array_derive::FieldNamesAsSlice; 54 | 55 | /// Exposes the `FIELD_NAMES_AS_ARRAY` constant. 56 | /// 57 | /// This trait is designed to be derived rather than implemented by 58 | /// hand (though that'd be perfectly fine as well). 59 | /// Please refer to the [top-level](crate) documentation for more 60 | /// information. 61 | /// 62 | pub trait FieldNamesAsArray { 63 | const FIELD_NAMES_AS_ARRAY: [&'static str; N]; 64 | } 65 | 66 | /// Exposes the `FIELD_NAMES_AS_SLICE` constant. 67 | /// 68 | /// This trait is designed to be derived rather than implemented by 69 | /// hand (though that'd be perfectly fine as well). 70 | /// Please refer to the [top-level](crate) documentation for more 71 | /// information. 72 | /// 73 | pub trait FieldNamesAsSlice { 74 | const FIELD_NAMES_AS_SLICE: &'static [&'static str]; 75 | } 76 | -------------------------------------------------------------------------------- /struct-field-names-as-array/tests/rename_all.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "derive")] 2 | #![allow(dead_code)] 3 | 4 | use struct_field_names_as_array::{FieldNamesAsArray, FieldNamesAsSlice}; 5 | 6 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 7 | #[field_names_as_array(rename_all = "lowercase")] 8 | #[field_names_as_slice(rename_all = "lowercase")] 9 | struct RenameLowercase { 10 | field_one: bool, 11 | field_two: bool, 12 | field_three: bool, 13 | } 14 | 15 | #[test] 16 | fn test_rename_lowercase() { 17 | assert_eq!( 18 | RenameLowercase::FIELD_NAMES_AS_ARRAY, 19 | ["field_one", "field_two", "field_three"], 20 | ); 21 | assert_eq!( 22 | RenameLowercase::FIELD_NAMES_AS_SLICE, 23 | ["field_one", "field_two", "field_three"], 24 | ); 25 | } 26 | 27 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 28 | #[field_names_as_array(rename_all = "UPPERCASE")] 29 | #[field_names_as_slice(rename_all = "UPPERCASE")] 30 | struct RenameUppercase { 31 | field_one: bool, 32 | field_two: bool, 33 | field_three: bool, 34 | } 35 | 36 | #[test] 37 | fn test_rename_uppercase() { 38 | assert_eq!( 39 | RenameUppercase::FIELD_NAMES_AS_ARRAY, 40 | ["FIELD_ONE", "FIELD_TWO", "FIELD_THREE"], 41 | ); 42 | assert_eq!( 43 | RenameUppercase::FIELD_NAMES_AS_SLICE, 44 | ["FIELD_ONE", "FIELD_TWO", "FIELD_THREE"], 45 | ); 46 | } 47 | 48 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 49 | #[field_names_as_array(rename_all = "PascalCase")] 50 | #[field_names_as_slice(rename_all = "PascalCase")] 51 | struct RenamePascalCase { 52 | field_one: bool, 53 | field_two: bool, 54 | field_three: bool, 55 | } 56 | 57 | #[test] 58 | fn test_rename_pascal_case() { 59 | assert_eq!( 60 | RenamePascalCase::FIELD_NAMES_AS_ARRAY, 61 | ["FieldOne", "FieldTwo", "FieldThree"], 62 | ); 63 | assert_eq!( 64 | RenamePascalCase::FIELD_NAMES_AS_SLICE, 65 | ["FieldOne", "FieldTwo", "FieldThree"], 66 | ); 67 | } 68 | 69 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 70 | #[field_names_as_array(rename_all = "camelCase")] 71 | #[field_names_as_slice(rename_all = "camelCase")] 72 | struct RenameCamelCase { 73 | field_one: bool, 74 | field_two: bool, 75 | field_three: bool, 76 | } 77 | 78 | #[test] 79 | fn test_rename_camel_case() { 80 | assert_eq!( 81 | RenameCamelCase::FIELD_NAMES_AS_ARRAY, 82 | ["fieldOne", "fieldTwo", "fieldThree"], 83 | ); 84 | assert_eq!( 85 | RenameCamelCase::FIELD_NAMES_AS_SLICE, 86 | ["fieldOne", "fieldTwo", "fieldThree"], 87 | ); 88 | } 89 | 90 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 91 | #[field_names_as_array(rename_all = "snake_case")] 92 | #[field_names_as_slice(rename_all = "snake_case")] 93 | struct RenameSnakeCase { 94 | field_one: bool, 95 | field_two: bool, 96 | field_three: bool, 97 | } 98 | 99 | #[test] 100 | fn test_rename_snake_case() { 101 | assert_eq!( 102 | RenameSnakeCase::FIELD_NAMES_AS_ARRAY, 103 | ["field_one", "field_two", "field_three"], 104 | ); 105 | assert_eq!( 106 | RenameSnakeCase::FIELD_NAMES_AS_SLICE, 107 | ["field_one", "field_two", "field_three"], 108 | ); 109 | } 110 | 111 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 112 | #[field_names_as_array(rename_all = "SCREAMING_SNAKE_CASE")] 113 | #[field_names_as_slice(rename_all = "SCREAMING_SNAKE_CASE")] 114 | struct RenameScreamingSnakeCase { 115 | field_one: bool, 116 | field_two: bool, 117 | field_three: bool, 118 | } 119 | 120 | #[test] 121 | fn test_rename_screaming_snake_case() { 122 | assert_eq!( 123 | RenameScreamingSnakeCase::FIELD_NAMES_AS_ARRAY, 124 | ["FIELD_ONE", "FIELD_TWO", "FIELD_THREE"], 125 | ); 126 | assert_eq!( 127 | RenameScreamingSnakeCase::FIELD_NAMES_AS_SLICE, 128 | ["FIELD_ONE", "FIELD_TWO", "FIELD_THREE"], 129 | ); 130 | } 131 | 132 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 133 | #[field_names_as_array(rename_all = "kebab-case")] 134 | #[field_names_as_slice(rename_all = "kebab-case")] 135 | struct RenameKebabCase { 136 | field_one: bool, 137 | field_two: bool, 138 | field_three: bool, 139 | } 140 | 141 | #[test] 142 | fn test_rename_kebab_case() { 143 | assert_eq!( 144 | RenameKebabCase::FIELD_NAMES_AS_ARRAY, 145 | ["field-one", "field-two", "field-three"], 146 | ); 147 | } 148 | 149 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 150 | #[field_names_as_array(rename_all = "SCREAMING-KEBAB-CASE")] 151 | #[field_names_as_slice(rename_all = "SCREAMING-KEBAB-CASE")] 152 | struct RenameScreamingKebabCase { 153 | field_one: bool, 154 | field_two: bool, 155 | field_three: bool, 156 | } 157 | 158 | #[test] 159 | fn test_rename_screaming_kebab_case() { 160 | assert_eq!( 161 | RenameScreamingKebabCase::FIELD_NAMES_AS_ARRAY, 162 | ["FIELD-ONE", "FIELD-TWO", "FIELD-THREE"], 163 | ); 164 | assert_eq!( 165 | RenameScreamingKebabCase::FIELD_NAMES_AS_SLICE, 166 | ["FIELD-ONE", "FIELD-TWO", "FIELD-THREE"], 167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /struct-field-names-as-array/tests/tests.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "derive")] 2 | #![allow(dead_code)] 3 | 4 | use struct_field_names_as_array::{FieldNamesAsArray, FieldNamesAsSlice}; 5 | 6 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 7 | struct Test { 8 | f1: String, 9 | f2: i64, 10 | f3: String, 11 | f4: bool, 12 | } 13 | 14 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 15 | struct TestGenerics { 16 | foo: A, 17 | bar: B, 18 | baz: C, 19 | } 20 | 21 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 22 | struct TestSkip { 23 | a: String, 24 | b: String, 25 | #[field_names_as_array(skip)] 26 | #[field_names_as_slice(skip)] 27 | c: String, 28 | } 29 | 30 | #[derive(FieldNamesAsArray, FieldNamesAsSlice)] 31 | struct TestRename { 32 | a: String, 33 | b: String, 34 | #[field_names_as_array(rename = "last_option")] 35 | #[field_names_as_slice(rename = "last_option")] 36 | c: String, 37 | } 38 | 39 | #[test] 40 | fn test_struct() { 41 | assert_eq!(Test::FIELD_NAMES_AS_ARRAY, ["f1", "f2", "f3", "f4"]); 42 | assert_eq!(Test::FIELD_NAMES_AS_SLICE, ["f1", "f2", "f3", "f4"]); 43 | } 44 | 45 | #[test] 46 | fn test_generics_struct() { 47 | assert_eq!( 48 | TestGenerics::::FIELD_NAMES_AS_ARRAY, 49 | ["foo", "bar", "baz"], 50 | ); 51 | assert_eq!( 52 | TestGenerics::::FIELD_NAMES_AS_SLICE, 53 | ["foo", "bar", "baz"], 54 | ); 55 | } 56 | 57 | #[test] 58 | fn test_skip() { 59 | assert_eq!(TestSkip::FIELD_NAMES_AS_ARRAY, ["a", "b"]); 60 | assert_eq!(TestSkip::FIELD_NAMES_AS_SLICE, ["a", "b"]); 61 | } 62 | 63 | #[test] 64 | fn test_rename() { 65 | assert_eq!(TestRename::FIELD_NAMES_AS_ARRAY, ["a", "b", "last_option"]); 66 | assert_eq!(TestRename::FIELD_NAMES_AS_SLICE, ["a", "b", "last_option"]); 67 | } 68 | --------------------------------------------------------------------------------