├── .gitignore ├── macro ├── README.md ├── Cargo.toml ├── LICENSE └── src │ └── lib.rs ├── example ├── tests │ ├── ui.rs │ └── ui │ │ ├── normal.rs │ │ ├── custom.rs │ │ ├── custom.stderr │ │ └── normal.stderr ├── Cargo.toml └── src │ └── lib.rs ├── .github └── workflows │ ├── release.yaml │ ├── docs.yml │ └── test.yaml ├── rustfmt.toml ├── LICENSE-MIT ├── Cargo.toml ├── src ├── from_partial.rs ├── utils.rs ├── std_impls.rs ├── syn_impls.rs ├── parsing.rs └── lib.rs ├── README.md ├── tests ├── derive-legacy.rs └── derive.rs ├── docs └── traits.html ├── CHANGELOG.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /macro/README.md: -------------------------------------------------------------------------------- 1 | proc-macro crate for [`attribute-derive`](https://lib.rs/attribute-derive) 2 | -------------------------------------------------------------------------------- /example/tests/ui.rs: -------------------------------------------------------------------------------- 1 | #[rustversion::stable] 2 | #[test] 3 | fn ui() { 4 | let t = trybuild::TestCases::new(); 5 | t.compile_fail("tests/ui/*.rs"); 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | changelog: CHANGELOG.md 19 | branch: main 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | condense_wildcard_suffixes = true 2 | format_code_in_doc_comments = true 3 | format_macro_matchers = true 4 | format_strings = true 5 | hex_literal_case = "Upper" 6 | imports_granularity = "Module" 7 | normalize_comments = true 8 | normalize_doc_attributes = true 9 | overflow_delimited_expr = true 10 | reorder_impl_items = true 11 | group_imports = "StdExternalCrate" 12 | unstable_features = true 13 | use_field_init_shorthand = true 14 | version = "Two" 15 | wrap_comments = true 16 | -------------------------------------------------------------------------------- /example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | [dependencies] 11 | attribute-derive = { path = "..", features = ["syn-full"] } 12 | syn = { version = "2", features = ["full", "extra-traits"] } 13 | trybuild = "1" 14 | 15 | [dev-dependencies] 16 | rustversion = "1.0.17" 17 | 18 | [package.metadata.release] 19 | release = false 20 | -------------------------------------------------------------------------------- /example/tests/ui/normal.rs: -------------------------------------------------------------------------------- 1 | use example::Normal; 2 | 3 | #[derive(Normal)] 4 | struct None; 5 | 6 | #[derive(Normal)] 7 | #[ident(example = 2.)] 8 | struct Example; 9 | 10 | #[derive(Normal)] 11 | #[ident( 12 | example = 2., 13 | optional_implicit = 10, 14 | optional_default = 3, 15 | default = 2, 16 | conflict_a = "hello", 17 | )] 18 | struct ExampleOI; 19 | 20 | #[derive(Normal)] 21 | #[ident(example = 1.)] 22 | #[a(conflict_a = "hey")] 23 | #[b(conflict_b = "hi")] 24 | struct Conflict; 25 | 26 | #[derive(Normal)] 27 | #[ident(hello)] 28 | #[single(hello)] 29 | #[empty(hello)] 30 | struct UnknownField; 31 | 32 | fn main() {} 33 | -------------------------------------------------------------------------------- /example/tests/ui/custom.rs: -------------------------------------------------------------------------------- 1 | use example::Custom; 2 | 3 | #[derive(Custom)] 4 | struct None; 5 | 6 | #[derive(Custom)] 7 | #[ident(example = 2.)] 8 | struct Example; 9 | 10 | #[derive(Custom)] 11 | #[ident( 12 | example = 2., 13 | optional_implicit = {10}, 14 | optional_default = 3, 15 | default = 2, 16 | conflict_a = "hello", 17 | )] 18 | struct ExampleOI; 19 | 20 | #[derive(Custom)] 21 | #[ident(example = 1.)] 22 | #[a(conflict_a = "hey")] 23 | #[b(conflict_b = "hi")] 24 | struct Conflict; 25 | 26 | #[derive(Custom)] 27 | #[ident(hello)] 28 | #[single(hello)] 29 | #[empty(hello)] 30 | struct UnknownField; 31 | 32 | fn main() {} 33 | -------------------------------------------------------------------------------- /macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | categories = ["rust-patterns", "development-tools::procedural-macro-helpers", "parsing"] 3 | description = "Clap for proc macro attributes" 4 | documentation = "https://docs.rs/attribute-derive" 5 | include = ["src/**/*", "LICENSE", "README.md"] 6 | keywords = ["derive", "macro"] 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/ModProg/attribute-derive" 10 | version = "0.10.5" 11 | edition = "2021" 12 | name = "attribute-derive-macro" 13 | 14 | [lib] 15 | proc-macro = true 16 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 17 | 18 | [dependencies] 19 | proc-macro2 = "1" 20 | quote = "1" 21 | quote-use = "0.8" 22 | syn = "2" 23 | proc-macro-utils = "0.10.0" 24 | # proc-macro-utils = {path = "../../proc-macro-utils"} 25 | interpolator = { version = "0.5.0", features = ["iter"] } 26 | collection_literals = "1" 27 | manyhow = "0.11" 28 | 29 | [package.metadata.release] 30 | tag = false 31 | shared-version = true 32 | -------------------------------------------------------------------------------- /example/tests/ui/custom.stderr: -------------------------------------------------------------------------------- 1 | error: missing field `example` 2 | 3 | = help: try ident: example=2.5 4 | --> tests/ui/custom.rs:3:10 5 | | 6 | 3 | #[derive(Custom)] 7 | | ^^^^^^ 8 | | 9 | = note: this error originates in the derive macro `Custom` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | 11 | error: conflict_a !!! conflict_b 12 | --> tests/ui/custom.rs:22:5 13 | | 14 | 22 | #[a(conflict_a = "hey")] 15 | | ^^^^^^^^^^ 16 | 17 | error: conflict_b !!! conflict_a 18 | --> tests/ui/custom.rs:23:5 19 | | 20 | 23 | #[b(conflict_b = "hi")] 21 | | ^^^^^^^^^^ 22 | 23 | error: expected one of `optional_implicit`, `optional_explicit`, `optional_default`, `default`, `conflict_a`, `conflict_b`, `example`, `flag` 24 | --> tests/ui/custom.rs:27:9 25 | | 26 | 27 | #[ident(hello)] 27 | | ^^^^^ 28 | 29 | error: expected nothing 30 | --> tests/ui/custom.rs:29:9 31 | | 32 | 29 | #[empty(hello)] 33 | | ^^^^^ 34 | 35 | error: expected field 36 | --> tests/ui/custom.rs:28:10 37 | | 38 | 28 | #[single(hello)] 39 | | ^^^^^ 40 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Roland Fredenhagen 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 | -------------------------------------------------------------------------------- /macro/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Roland Fredenhagen 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 | -------------------------------------------------------------------------------- /example/tests/ui/normal.stderr: -------------------------------------------------------------------------------- 1 | error: required `example` is not specified 2 | 3 | = help: try `#[ident(example = 2.5)]` 4 | --> tests/ui/normal.rs:3:10 5 | | 6 | 3 | #[derive(Normal)] 7 | | ^^^^^^ 8 | | 9 | = note: this error originates in the derive macro `Normal` (in Nightly builds, run with -Z macro-backtrace for more info) 10 | 11 | error: `conflict_a` conflicts with mutually exclusive `conflict_b` 12 | --> tests/ui/normal.rs:22:5 13 | | 14 | 22 | #[a(conflict_a = "hey")] 15 | | ^^^^^^^^^^ 16 | 17 | error: `conflict_b` conflicts with mutually exclusive `conflict_a` 18 | --> tests/ui/normal.rs:23:5 19 | | 20 | 23 | #[b(conflict_b = "hi")] 21 | | ^^^^^^^^^^ 22 | 23 | error: supported fields are `optional_implicit`, `optional_explicit`, `optional_default`, `default`, `conflict_a`, `conflict_b`, `example` and `flag` 24 | --> tests/ui/normal.rs:27:9 25 | | 26 | 27 | #[ident(hello)] 27 | | ^^^^^ 28 | 29 | error: expected empty attribute 30 | --> tests/ui/normal.rs:29:9 31 | | 32 | 29 | #[empty(hello)] 33 | | ^^^^^ 34 | 35 | error: expected supported field `field` 36 | --> tests/ui/normal.rs:28:10 37 | | 38 | 28 | #[single(hello)] 39 | | ^^^^^ 40 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Git Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | concurrency: 15 | group: pages 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | docs: 20 | environment: 21 | name: github-pages 22 | url: ${{ steps.deployment.outputs.page_url }} 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/cache@v3 27 | with: 28 | path: | 29 | ~/.cargo/ 30 | target/ 31 | key: docs-${{ hashFiles('Cargo.toml') }} 32 | restore-keys: | 33 | docs- 34 | - uses: hecrj/setup-rust-action@v1 35 | with: 36 | rust-version: nightly 37 | - name: Generate Docs (reference docs.rs) 38 | run: | 39 | cargo rustdoc -- --cfg docsrs -Z unstable-options $(cargo metadata --format-version 1 | jq --raw-output '.packages | map("--extern-html-root-url=\(.name)=https://docs.rs/\(.name)/\(.version)") | join(" ")') 40 | - uses: actions/upload-pages-artifact@v4 41 | with: 42 | path: 'target/doc' 43 | - id: deployment 44 | uses: actions/deploy-pages@v4 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: null 5 | pull_request: null 6 | schedule: 7 | - cron: '0 12 * * *' 8 | 9 | jobs: 10 | test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: 15 | - ubuntu-latest 16 | - windows-latest 17 | - macos-latest 18 | rust: 19 | - stable 20 | - nightly 21 | include: 22 | - rust: nightly 23 | cargo_flags: -Z minimal-versions 24 | 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v2 28 | - uses: actions/cache@v3 29 | with: 30 | path: | 31 | ~/.cargo/ 32 | target 33 | key: ${{ matrix.os }}-${{ matrix.rust }}-${{ hashFiles('**/Cargo.toml') }} 34 | restore-keys: | 35 | ${{ matrix.os }}-${{ matrix.rust }}- 36 | - uses: hecrj/setup-rust-action@v1 37 | with: 38 | rust-version: ${{ matrix.rust }} 39 | - uses: bruxisma/setup-cargo-hack@v1 40 | with: 41 | cargo-hack-version: "0.5" 42 | - name: Build 43 | run: cargo hack build --feature-powerset ${{ matrix.cargo_flags }} 44 | - name: Test 45 | run: cargo hack test --feature-powerset --all-targets --no-fail-fast --workspace 46 | - name: Doc Test 47 | run: cargo test --all-features --doc --no-fail-fast --workspace 48 | - name: Build Docs 49 | run: cargo doc --all-features --workspace 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["example"] 3 | 4 | [package] 5 | categories = ["rust-patterns", "development-tools::procedural-macro-helpers", "parsing"] 6 | description = "Clap like parsing for attributes in proc-macros" 7 | documentation = "https://docs.rs/attribute-derive" 8 | include = ["src/**/*", "LICENSE-*", "README.md", "docs/**/*"] 9 | keywords = ["derive", "macro", "attribute", "arguments", "syn"] 10 | license = "MIT OR Apache-2.0" 11 | readme = "README.md" 12 | repository = "https://github.com/ModProg/attribute-derive" 13 | name = "attribute-derive" 14 | version = "0.10.5" 15 | edition = "2021" 16 | 17 | [lib] 18 | 19 | [dependencies] 20 | derive-where = "1.2.7" 21 | manyhow = "0.11.3" 22 | proc-macro2 = "1" 23 | quote = "1" 24 | syn = "2" 25 | 26 | [features] 27 | # default = ["syn-full"] 28 | syn-full = ["syn/full"] 29 | 30 | [dependencies.attribute-derive-macro] 31 | version = "0.10.5" 32 | path = "macro" 33 | 34 | [dev-dependencies] 35 | insta = "1.39.0" 36 | quote = "1" 37 | static_assertions = "1.1.0" 38 | syn = { version = "2", features = ["extra-traits"] } 39 | 40 | [package.metadata.release] 41 | shared-version = true 42 | 43 | [[package.metadata.release.pre-release-replacements]] 44 | file = "CHANGELOG.md" 45 | search = '## \[Unreleased\]' 46 | replace = """ 47 | ## [Unreleased] 48 | 49 | ## [{{version}}] - {{date}}\ 50 | """ 51 | [[package.metadata.release.pre-release-replacements]] 52 | file = "CHANGELOG.md" 53 | search = '\[unreleased\]: (.*)/(v.*)\.\.\.HEAD' 54 | replace = """ 55 | [unreleased]: $1/{{tag_name}}...HEAD 56 | [{{version}}]: $1/$2...{{tag_name}}\ 57 | """ 58 | -------------------------------------------------------------------------------- /src/from_partial.rs: -------------------------------------------------------------------------------- 1 | //! Contains utilities for implementing [`FromPartial`]. 2 | use crate::*; 3 | 4 | /// Converts from a [`Partial`](AttributeBase::Partial) value. 5 | pub trait FromPartial: Sized { 6 | /// Creates `Self` from `T`. 7 | /// 8 | /// # Errors 9 | /// Returns a [`syn::Error`] when `T` does not represent a valid `Self`, 10 | /// e.g., due to missing or conflicting fields. 11 | fn from(partial: T) -> Result; 12 | 13 | /// Creates `Self` from optional `T`. 14 | /// 15 | /// # Errors 16 | /// The default implementation errors with `error_missing` when `partial` is 17 | /// [`None`], or when `Self::from` errors. 18 | /// 19 | /// Implementors might override this for types with expected default values. 20 | fn from_option(partial: Option, error_missing: &str) -> Result { 21 | // Pass in surrounding span 22 | partial 23 | .map(Self::from) 24 | .transpose()? 25 | .ok_or_else(|| syn::Error::new(Span::call_site(), error_missing)) 26 | } 27 | 28 | /// Defines how two arguments for the same parameter should be handled. 29 | /// 30 | /// # Errors 31 | /// The default implementation errors if `first` is already present and 32 | /// `specified_twice_error` is returned with the correct spans. 33 | fn join( 34 | first: Option>, 35 | second: SpannedValue, 36 | specified_twice_error: &str, 37 | ) -> Result>> { 38 | if let Some(first) = first { 39 | if let Some(span) = first 40 | .span() 41 | .span_joined() 42 | .and_then(|s| Some((s, second.span().span_joined()?))) 43 | .and_then(|(a, b)| a.join(b)) 44 | { 45 | Err(Error::new(span, specified_twice_error)) 46 | } else { 47 | let mut error = Error::new(first.span().start, specified_twice_error); 48 | error.combine(Error::new(second.span().start, specified_twice_error)); 49 | Err(error) 50 | } 51 | } else { 52 | Ok(Some(second)) 53 | } 54 | } 55 | } 56 | 57 | impl FromPartial for T { 58 | fn from(partial: T) -> Result { 59 | Ok(partial) 60 | } 61 | } 62 | 63 | /// [`FromPartial`] wrapper that uses [`Default`] value when not specified. 64 | #[derive(Clone)] 65 | pub struct Defaulting(pub T); 66 | 67 | impl> FromPartial> for T { 68 | fn from(partial: Defaulting

) -> Result { 69 | Self::from(partial.0) 70 | } 71 | 72 | fn from_option(partial: Option>, _error: &str) -> Result { 73 | partial 74 | .map(|d| Self::from(d.0)) 75 | .transpose() 76 | .map(Option::unwrap_or_default) 77 | } 78 | } 79 | 80 | /// Utility struct to avoid duplicate trait definition when using `Self` for 81 | /// ` as BaseAttribute>::Partial`. 82 | pub struct Partial(pub T); 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # attribute-derive 2 | 3 | [![docs.rs](https://img.shields.io/docsrs/attribute-derive)](https://docs.rs/attribute-derive/latest/attribute_derive/) 4 | [![lib.rs](https://img.shields.io/crates/v/attribute-derive)](https://lib.rs/crates/attribute-derive) 5 | [![MIT](https://img.shields.io/crates/l/attribute-derive)](LICENSE) 6 | [![Documentation for `main`](https://img.shields.io/badge/docs-main-informational)](https://modprog.github.io/attribute-derive/attribute_derive/) 7 | 8 | Basically clap for attribute macros: 9 | ```rust 10 | use attribute_derive::Attribute; 11 | use syn::Type; 12 | 13 | #[derive(Attribute)] 14 | #[attribute(ident = collection)] 15 | #[attribute(error(missing_field = "`{field}` was not specified"))] 16 | struct CollectionAttribute { 17 | // Options are optional by default (will be set to None if not specified) 18 | authority: Option, 19 | name: String, 20 | // Any type implementing default can be flagged as default 21 | // This will be set to Vec::default() when not specified 22 | #[attribute(optional)] 23 | views: Vec, 24 | // Booleans can be used without assigning a value, i.e., as a flag. 25 | // If omitted they are set to false 26 | some_flag: bool, 27 | } 28 | ``` 29 | 30 | Will be able to parse an attribute like this: 31 | ```rust 32 | #[collection(authority="Some String", name = r#"Another string"#, views = [Option, ()])] 33 | ``` 34 | 35 | ## Limitations 36 | 37 | There are some limitations in syntax parsing that will be lifted future releases. 38 | 39 | - literals in top level (meaning something like `#[attr(42, 3.14, "hi")]` 40 | - function like arguments (something like `#[attr(view(a = "test"))]` 41 | - other syntaxes, maybe something like `key: value` 42 | 43 | ## Parse methods 44 | 45 | There are multiple ways of parsing a struct deriving [`Attribute`](https://docs.rs/attribute-derive/latest/attribute_derive/trait.Attribute.html). 46 | 47 | For helper attributes there is: 48 | - [`Attribute::from_attributes`](https://docs.rs/attribute-derive/latest/attribute_derive/trait.Attribute.html#tymethod.from_attributes) which takes in an [`IntoIterator`](https://docs.rs/syn/latest/syn/struct.Attribute.html)). Most useful for derive macros. 51 | - [`Attribute::remove_attributes`](https://docs.rs/attribute-derive/latest/attribute_derive/trait.Attribute.html#tymethod.remove_attributes) which takes an [`&mut Vec`](https://docs.rs/syn/latest/syn/struct.Attribute.html) 52 | and does not only parse the [`Attribute`](https://docs.rs/attribute-derive/latest/attribute_derive/trait.Attribute.html#tymethod.from_attributes) but also removes those matching. Useful for helper 53 | attributes for proc macros, where the helper attributes need to be removed. 54 | 55 | For parsing a single [`TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) e.g. for parsing the proc macro input there a two ways: 56 | 57 | - [`Attribute::from_args`](https://docs.rs/attribute-derive/latest/attribute_derive/trait.Attribute.html#tymethod.from_args) taking in a [`TokenStream`](https://docs.rs/proc-macro2/latest/proc_macro2/struct.TokenStream.html) 58 | - As `derive(Attribute)` also derives [`Parse`](https://docs.rs/syn/latest/syn/parse/trait.Parse.html) so you can use the [parse](https://docs.rs/syn/latest/syn/parse/index.html) API, 59 | e.g. with [`parse_macro_input!(tokens as Attribute)`](https://docs.rs/syn/latest/syn/macro.parse_macro_input.html). 60 | -------------------------------------------------------------------------------- /example/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | use std::fmt::Debug; 3 | 4 | use attribute_derive::{AttributeIdent, FromAttr}; 5 | use proc_macro::TokenStream; 6 | use syn::{Block, DeriveInput, Result}; 7 | 8 | // #[derive(FromAttr)] 9 | // #[attribute(ident = "positional")] 10 | // struct PositionalAttr { 11 | // #[attribute(positional)] 12 | // a: u8, 13 | // #[attribute(positional)] 14 | // b: String, 15 | // #[attribute(positional)] 16 | // c: bool, 17 | // } 18 | // 19 | // #[proc_macro_derive(Positional, attributes(attribute))] 20 | // pub fn positional_derive(input: TokenStream) -> proc_macro::TokenStream { 21 | // all_attrs::(input).unwrap_or_else(|e| 22 | // e.to_compile_error().into()) } 23 | #[derive(FromAttr)] 24 | #[attribute(ident = empty)] 25 | struct Empty {} 26 | 27 | #[derive(FromAttr)] 28 | #[attribute(ident = single)] 29 | struct Single { 30 | field: bool, 31 | } 32 | 33 | #[derive(FromAttr, Debug)] 34 | #[attribute(ident = ident, aliases = [a, b])] 35 | struct Normal { 36 | optional_implicit: Option, 37 | #[attribute(optional)] 38 | optional_explicit: u8, 39 | #[attribute(optional, default = 2 * 5)] 40 | optional_default: u8, 41 | #[attribute(default = 33)] 42 | default: u8, 43 | #[attribute(conflicts = [conflict_b])] 44 | conflict_a: Option, 45 | conflict_b: Option, 46 | #[attribute(example = "2.5")] 47 | example: f32, 48 | flag: bool, 49 | } 50 | #[proc_macro_derive(Normal, attributes(ident, a, b, empty, single))] 51 | pub fn normal_derive(input: TokenStream) -> proc_macro::TokenStream { 52 | let mut tokens = 53 | all_attrs::(input.clone()).unwrap_or_else(|e| e.to_compile_error().into()); 54 | tokens 55 | .extend(all_attrs::(input.clone()).unwrap_or_else(|e| e.to_compile_error().into())); 56 | tokens.extend(all_attrs::(input).unwrap_or_else(|e| e.to_compile_error().into())); 57 | tokens 58 | } 59 | 60 | #[derive(FromAttr, Debug)] 61 | #[attribute(ident = ident, aliases = [a, b])] 62 | #[attribute(error( 63 | unknown_field = "expected one of {expected_fields:i(`{}`)(, )}", 64 | duplicate_field = "duplicate `{field}`", 65 | missing_field = "missing field `{field}`", 66 | field_help = "try {attribute}: {field}={example}", 67 | conflict = "{first} !!! {second}" 68 | ))] 69 | struct Custom { 70 | optional_implicit: Option, 71 | #[attribute(optional)] 72 | optional_explicit: u8, 73 | #[attribute(optional, default = 2 * 5)] 74 | optional_default: u8, 75 | #[attribute(default = 33)] 76 | default: u8, 77 | #[attribute(conflicts = [conflict_b])] 78 | conflict_a: Option, 79 | conflict_b: Option, 80 | #[attribute(example = "2.5")] 81 | example: f32, 82 | flag: bool, 83 | } 84 | #[derive(FromAttr)] 85 | #[attribute(ident = empty, error(unknown_field_empty = "expected nothing"))] 86 | struct EmptyCustom {} 87 | 88 | #[derive(FromAttr)] 89 | #[attribute(ident = single, error(unknown_field_single = "expected {expected_field}"))] 90 | struct SingleCustom { 91 | field: bool, 92 | } 93 | 94 | #[proc_macro_derive(Custom, attributes(ident, a, b, empty, single))] 95 | pub fn custom_derive(input: TokenStream) -> proc_macro::TokenStream { 96 | let mut tokens = 97 | all_attrs::(input.clone()).unwrap_or_else(|e| e.to_compile_error().into()); 98 | tokens.extend( 99 | all_attrs::(input.clone()).unwrap_or_else(|e| e.to_compile_error().into()), 100 | ); 101 | tokens.extend(all_attrs::(input).unwrap_or_else(|e| e.to_compile_error().into())); 102 | tokens 103 | } 104 | 105 | fn all_attrs(input: TokenStream) -> Result { 106 | let DeriveInput { attrs, data, .. } = syn::parse(input)?; 107 | T::from_attributes(&attrs)?; 108 | match data { 109 | syn::Data::Struct(data) => { 110 | for field in data.fields { 111 | T::from_attributes(&field.attrs)?; 112 | } 113 | } 114 | syn::Data::Enum(data) => { 115 | for variant in data.variants { 116 | T::from_attributes(&variant.attrs)?; 117 | for field in variant.fields { 118 | T::from_attributes(&field.attrs)?; 119 | } 120 | } 121 | } 122 | syn::Data::Union(data) => { 123 | for field in data.fields.named { 124 | T::from_attributes(&field.attrs)?; 125 | } 126 | } 127 | } 128 | Ok(TokenStream::new()) 129 | } 130 | -------------------------------------------------------------------------------- /tests/derive-legacy.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use attribute_derive::Attribute; 3 | use syn::parse_quote; 4 | 5 | #[cfg(feature = "syn-full")] 6 | #[test] 7 | fn test() { 8 | use proc_macro2::TokenStream; 9 | use quote::quote; 10 | use syn::{parse2, Expr, LitStr, Type}; 11 | 12 | #[derive(Attribute)] 13 | #[attribute(ident = test)] 14 | struct Test { 15 | // #[attribute(positional)] 16 | // a: u8, 17 | b: LitStr, 18 | c: String, 19 | oc: Option, 20 | od: Option, 21 | d: Type, 22 | e: Expr, 23 | f: Vec, 24 | g: bool, 25 | h: bool, 26 | i: TokenStream, 27 | } 28 | 29 | let parsed = Test::from_attributes([ 30 | parse_quote!(#[test(/* 8, */ b="hi", c="ho", oc="xD", d=(), e=if true { "a" } else { "b" }, f= [(), Debug], g, i(smth::hello + 24/3'a', b = c))]), 31 | ].iter()) 32 | .unwrap(); 33 | // assert_eq!(parsed.a, 8); 34 | assert_eq!(parsed.b.value(), "hi"); 35 | assert_eq!(parsed.c, "ho"); 36 | assert_eq!(parsed.oc, Some("xD".to_owned())); 37 | assert!(parsed.od.is_none()); 38 | assert!(matches!(parsed.d, Type::Tuple(_))); 39 | assert!(matches!(parsed.e, Expr::If(_))); 40 | assert!(parsed.f.len() == 2); 41 | assert!(parsed.g); 42 | assert!(!parsed.h); 43 | assert_eq!(parsed.i.to_string(), "smth :: hello + 24 / 3 'a' , b = c"); 44 | 45 | let parsed = Test::from_args( 46 | quote!(/* 8, */ b="hi", c="ho", oc="xD", d=(), e=if true{ "a" } else { "b" }, f= [(), Debug], g, i(smth::hello + 24/3'a', b = c)) 47 | ) 48 | .unwrap(); 49 | // assert_eq!(parsed.a, 8); 50 | assert_eq!(parsed.b.value(), "hi"); 51 | assert_eq!(parsed.c, "ho"); 52 | assert_eq!(parsed.oc, Some("xD".to_owned())); 53 | assert!(parsed.od.is_none()); 54 | assert!(matches!(parsed.d, Type::Tuple(_))); 55 | assert!(matches!(parsed.e, Expr::If(_))); 56 | assert!(parsed.f.len() == 2); 57 | assert!(parsed.g); 58 | assert!(!parsed.h); 59 | assert_eq!(parsed.i.to_string(), "smth :: hello + 24 / 3 'a' , b = c"); 60 | 61 | let mut attrs = vec![ 62 | parse_quote!(#[something]), 63 | parse_quote!(#[test(/* 8, */ b="hi", c="ho", oc="xD", d=(), e=if true{ "a" } else { "b" }, f= [(), Debug], g, i(smth::hello + 24/3'a', b = c))]), 64 | parse_quote!(#[another(smth)]), 65 | ]; 66 | let parsed = Test::remove_attributes(&mut attrs).unwrap(); 67 | // assert_eq!(parsed.a, 8); 68 | assert_eq!(parsed.b.value(), "hi"); 69 | assert_eq!(parsed.c, "ho"); 70 | assert_eq!(parsed.oc, Some("xD".to_owned())); 71 | assert!(parsed.od.is_none()); 72 | assert!(matches!(parsed.d, Type::Tuple(_))); 73 | assert!(matches!(parsed.e, Expr::If(_))); 74 | assert!(parsed.f.len() == 2); 75 | assert!(parsed.g); 76 | assert!(!parsed.h); 77 | assert_eq!(parsed.i.to_string(), "smth :: hello + 24 / 3 'a' , b = c"); 78 | assert_eq!(attrs.len(), 2); 79 | 80 | let parsed: Test = parse2( 81 | quote!(/* 8, */ b="hi", c="ho", oc="xD", d=(), e=if true{ "a" } else { "b" }, f= [(), Debug], g, i(smth::hello + 24/3'a', b = c)) 82 | ) 83 | .unwrap(); 84 | // assert_eq!(parsed.a, 8); 85 | assert_eq!(parsed.b.value(), "hi"); 86 | assert_eq!(parsed.c, "ho"); 87 | assert_eq!(parsed.oc, Some("xD".to_owned())); 88 | assert!(parsed.od.is_none()); 89 | assert!(matches!(parsed.d, Type::Tuple(_))); 90 | assert!(matches!(parsed.e, Expr::If(_))); 91 | assert!(parsed.f.len() == 2); 92 | assert!(parsed.g); 93 | assert!(!parsed.h); 94 | assert_eq!(parsed.i.to_string(), "smth :: hello + 24 / 3 'a' , b = c"); 95 | } 96 | 97 | #[test] 98 | fn default() { 99 | #[derive(Attribute, Debug, PartialEq)] 100 | #[attribute(ident = test)] 101 | struct Test { 102 | #[attribute(optional)] 103 | hi: f32, 104 | #[attribute(default = 10)] 105 | ho: usize, 106 | } 107 | assert_eq!(Test::from_attributes::([]).unwrap(), Test { 108 | hi: 0., 109 | ho: 10 110 | }); 111 | } 112 | 113 | #[test] 114 | fn aggregate() { 115 | #[derive(Attribute, Debug)] 116 | #[attribute(ident = test)] 117 | struct Test { 118 | strings: Vec, 119 | } 120 | 121 | assert_eq!( 122 | Test::from_attributes(&[ 123 | parse_quote!(#[test(strings=["a"], strings=["b"])]), 124 | parse_quote!(#[test(strings=["c"])]) 125 | ]) 126 | .unwrap() 127 | .strings, 128 | ["a", "b", "c"].map(ToOwned::to_owned) 129 | ) 130 | } 131 | 132 | #[test] 133 | fn without_ident() { 134 | #[derive(Attribute)] 135 | struct Test { 136 | a: u8, 137 | } 138 | 139 | let parsed: Test = parse_quote!(a = 5); 140 | assert_eq!(parsed.a, 5); 141 | } 142 | 143 | #[test] 144 | fn empty() { 145 | #[derive(Attribute)] 146 | #[attribute(ident = test)] 147 | struct Test {} 148 | } 149 | -------------------------------------------------------------------------------- /docs/traits.html: -------------------------------------------------------------------------------- 1 |

2 | 48 |
49 |

FromAttr

50 |

Main entry point. Derived via macro. Anything that can be parsed from one or multiple attributes.

51 |
52 |
53 |

AttributeNamed

54 |

Values that can be parsed named, e.g. name(<value>), name = <value>, name (as flag).

55 |

This is the default parsing mode used for fields in derived FromAttr implementations.

56 |
57 |
58 |

AttributePositional

59 |

Values that can be parsed positionally, i.e., without a name, e.g. "literal", a + b, true.

60 |

When deriving FromAttr this is enabled via putting #[attr(positional)] on the field.

61 |
62 |
63 |
64 |
65 | impl <T: AttributeValue> FromAttr for T 66 |
67 |
68 |
69 |
70 |
71 | impl <T: AttributeValue> AttributeNamed for T 72 |
73 |
74 |
75 |
76 |
77 | impl <T: AttributeValue + PositionalValue> AttributePositional for T 78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |

AttributeValue

90 | Any attribute that has the concept of a value, e.g., "positional", meta(<value>), key = <value>. 91 |
92 |
93 |

PositionalValue

94 | Empty marker trait, defining which AttributeValue implement AttributePositional. 95 |
96 |
97 |
98 |
99 | impl <T: AttributeMeta> AttributeValue for T 100 |
101 |
102 |
103 |

AttributeMeta

104 |

Values in function or meta style attributes, i.e., meta(<value>).

105 |
106 |
107 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.10.5] - 2025-10-02 10 | ### Fixed 11 | - ensured updated version of macro crate is selected in main crate 12 | 13 | ## [0.10.4] - 2025-10-02 14 | ### Fixed 15 | - fix nondeterminism in order of conflicting fields 16 | 17 | ## [0.10.3] - 2024-11-17 18 | ### Fixed 19 | - fix imports in `impl_Attribute_for_Parse_and_ToTokens!` 20 | ### Added 21 | - allow `#[from_attr]` on top of `#[attribute]` 22 | 23 | ## [0.10.2] - 2024-10-30 24 | ### Added 25 | - published `impl_Attribute_for_Parse_and_ToTokens!` to "derive" `AttributeValue` traits via existing `Parse` and `ToTokens` implementation. 26 | 27 | ## [0.10.1] - 2024-08-27 28 | ### Fixed 29 | - removed accidental debug prints 30 | 31 | ## [0.10.0] - 2024-08-13 32 | ### Added 33 | - derive `AttributeIdent` by default, converting the `StructIdent` to `snake_case` 34 | - `AttributeIdent` implementation for `Option` 35 | 36 | ## [0.9.2] - 2024-06-22 37 | ### Fixed 38 | - Partial type copies visibility from attribute struct 39 | 40 | ## [0.9.1] - 2024-03-23 41 | ### Added 42 | - `from_attribute` and `from_attribue_partial` to `FromAttr`. 43 | - Support for tuple structs. 44 | 45 | ### Fixed 46 | - `FromAttr` did not support `#[flag]` or `#[name = value]` attribute styles at the root. 47 | 48 | ## [0.9.0] - 2024-03-17 49 | ### Added 50 | - Attributes can now be nested, i.e. `#[outer(inner(key = value))]`. 51 | 52 | ### Changed 53 | - **Breaking Change** added `span` to `IdentValue`, as this is mostly an implementation detail, 54 | most users need not worry about this change. 55 | - **Breaking Change** changed `FromAttr::from_attributes` to take `[Borrow]`, only relevant for type inference. 56 | - **Breaking Change** `TokenStream`, when used as field in `FromAttr` now only supports `()` syntax, `=` was removed. 57 | 58 | ### Removed 59 | - **Breaking Change** mandatory flags 60 | - **Breaking Change** removed `found_field` in custom error messages 61 | 62 | ## [0.8.1] - 2023-09-27 63 | ### Added 64 | - `FlagOrValue::{is_none, is_flag, is_value, into_value, as_value}` 65 | 66 | ## [0.8.0] - 2023-09-18 67 | ### Changed 68 | - Renamed `Attribute` to `FromAttr` to not conflict with `syn::Attribute`. 69 | The old path is still exported (`#[doc(hidden)]` and deprecated). Existing usages should not break. 70 | 71 | ## [0.7.1] - 2023-09-17 72 | ### Fixed 73 | - `FlagOrValue` only supported some `syn` types. 74 | 75 | ## [0.7.0] - 2023-09-17 76 | - Updated dependencies 77 | 78 | ### Added 79 | - `FlagOrValue` to support a value both as a Flag and a Value 80 | 81 | ### Changed 82 | - `bool` now allows specifying a flag multiple times. 83 | 84 | ### Fixed 85 | - Specifying a parameter multiple times in the same attribute was ignored. 86 | 87 | ## [0.6.1] - 2023-05-21 88 | - Updated dependencies 89 | 90 | ## [0.6.0] - 2023-03-20 91 | ### Changed 92 | - Updated `syn` to v2 93 | 94 | ## [0.5.0] - 2023-03-02 95 | ### Added 96 | - `IdentValue` to keep hold of both value and the source ident 97 | 98 | ### Changed 99 | - **Breaking Change**: `ConvertParsed::aggregate()` now takes/returns 100 | `IdentValue` 101 | - Improved span for conflicting values 102 | 103 | ## [0.4.0] - 2023-03-02 104 | ### Changed 105 | - **Breaking Change**: Moved some syn types behind feature `full` 106 | - **Breaking Change**: Refactored attributes 107 | - Use [interpolator](https://docs.rs/interpolator) for error messages 108 | - **Breaking Change**: Compile time verify if ident is given through helper 109 | trait 110 | 111 | [unreleased]: https://github.com/ModProg/attribute-derive/compare/v0.10.5...HEAD 112 | [0.10.5]: https://github.com/ModProg/attribute-derive/compare/v0.10.4...v0.10.5 113 | [0.10.4]: https://github.com/ModProg/attribute-derive/compare/v0.10.3...v0.10.4 114 | [0.10.3]: https://github.com/ModProg/attribute-derive/compare/v0.10.2...v0.10.3 115 | [0.10.2]: https://github.com/ModProg/attribute-derive/compare/v0.10.1...v0.10.2 116 | [0.10.1]: https://github.com/ModProg/attribute-derive/compare/v0.10.0...v0.10.1 117 | [0.10.0]: https://github.com/ModProg/attribute-derive/compare/v0.9.2...v0.10.0 118 | [0.9.2]: https://github.com/ModProg/attribute-derive/compare/v0.9.1...v0.9.2 119 | [0.9.1]: https://github.com/ModProg/attribute-derive/compare/v0.9.0...v0.9.1 120 | [0.9.0]: https://github.com/ModProg/attribute-derive/compare/v0.8.1...v0.9.0 121 | [0.8.1]: https://github.com/ModProg/attribute-derive/compare/v0.8.0...v0.8.1 122 | [0.8.0]: https://github.com/ModProg/attribute-derive/compare/v0.7.1...v0.8.0 123 | [0.7.1]: https://github.com/ModProg/attribute-derive/compare/v0.7.0...v0.7.1 124 | [0.7.0]: https://github.com/ModProg/attribute-derive/compare/v0.6.1...v0.7.0 125 | [0.6.1]: https://github.com/ModProg/attribute-derive/compare/v0.6.0...v0.6.1 126 | [0.6.0]: https://github.com/ModProg/attribute-derive/compare/v0.5.0...v0.6.0 127 | [0.5.0]: https://github.com/ModProg/attribute-derive/compare/v0.3.1...v0.5.0 128 | [0.4.0]: https://github.com/ModProg/attribute-derive/compare/v0.3.1...v0.4.0 129 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities implementing useful patterns for fields inside an attribute. 2 | use from_partial::{FromPartial, Partial}; 3 | use manyhow::SpanRanged; 4 | use parsing::{AttributeNamed, AttributeValue}; 5 | use syn::token::Paren; 6 | use syn::Token; 7 | 8 | use self::parsing::parse_name; 9 | use crate::parsing::Named; 10 | use crate::{SpannedValue, *}; 11 | 12 | /// [`FromAttr`] value that can be used both as a flag and with a value. 13 | /// 14 | /// When parameter is specified both as flag and as value, the value will 15 | /// dominate. 16 | /// 17 | /// ``` 18 | /// # use attribute_derive::{FromAttr, utils::FlagOrValue}; 19 | /// # use quote::quote; 20 | /// #[derive(FromAttr)] 21 | /// struct Test { 22 | /// param: FlagOrValue, 23 | /// } 24 | /// 25 | /// assert_eq!( 26 | /// Test::from_args(quote!(param)).unwrap().param, 27 | /// FlagOrValue::Flag 28 | /// ); 29 | /// assert_eq!( 30 | /// Test::from_args(quote!(param = "value")).unwrap().param, 31 | /// FlagOrValue::Value("value".into()) 32 | /// ); 33 | /// assert_eq!( 34 | /// Test::from_args(quote!(param, param = "value", param)) 35 | /// .unwrap() 36 | /// .param, 37 | /// FlagOrValue::Value("value".into()) 38 | /// ); 39 | /// assert_eq!(Test::from_args(quote!()).unwrap().param, FlagOrValue::None); 40 | /// ``` 41 | #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] 42 | pub enum FlagOrValue { 43 | /// Was not specified. 44 | #[default] 45 | None, 46 | /// Was specified as a flag, i.e., without a value. 47 | Flag, 48 | /// Was specified with a value. 49 | Value(T), 50 | } 51 | 52 | impl FlagOrValue { 53 | /// Was not specified. 54 | pub fn is_none(&self) -> bool { 55 | matches!(self, Self::None) 56 | } 57 | 58 | /// Was specified as a flag, i.e., without a value. 59 | pub fn is_flag(&self) -> bool { 60 | matches!(self, Self::Flag,) 61 | } 62 | 63 | /// Was specified with a value. 64 | pub fn is_value(&self) -> bool { 65 | matches!(self, Self::Value(_),) 66 | } 67 | 68 | /// Returns value if set. 69 | pub fn into_value(self) -> Option { 70 | match self { 71 | FlagOrValue::Value(value) => Some(value), 72 | _ => None, 73 | } 74 | } 75 | 76 | /// Returns value if set. 77 | pub fn as_value(&self) -> Option<&T> { 78 | match self { 79 | FlagOrValue::Value(value) => Some(value), 80 | _ => None, 81 | } 82 | } 83 | 84 | /// Maps the `value` if present. 85 | pub fn map_value(self, map: impl FnOnce(T) -> I) -> FlagOrValue { 86 | match self { 87 | FlagOrValue::None => FlagOrValue::None, 88 | FlagOrValue::Flag => FlagOrValue::Flag, 89 | FlagOrValue::Value(value) => FlagOrValue::Value(map(value)), 90 | } 91 | } 92 | } 93 | 94 | /// Enables the `transpose` function on [`FlagOrValue`] containing or being 95 | /// contained in [`Option`] or [`Result`](std::result::Result). 96 | pub trait Transpose { 97 | /// Should behave equivalent to the built-in `transpose` functions available 98 | /// on [`Result