├── .cargo └── config.toml ├── .github ├── dependabot.yml └── workflows │ ├── hygiene.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── clippy.toml ├── nnn-macros ├── Cargo.toml ├── LICENSE ├── README.md └── src │ ├── argument.rs │ ├── argument │ ├── associated_const.rs │ ├── cfg.rs │ ├── default.rs │ ├── derive.rs │ ├── new_unchecked.rs │ ├── nnn_derive.rs │ ├── sanitizer.rs │ └── validator.rs │ ├── codegen.rs │ ├── codegen │ ├── impl_item.rs │ └── test_fn.rs │ ├── ctx.rs │ ├── lib.rs │ ├── utils.rs │ └── utils │ ├── closure.rs │ ├── regex_input.rs │ └── syn_ext.rs ├── rustfmt.toml ├── src └── lib.rs └── tests ├── derives ├── as_ref.rs ├── borrow.rs ├── deref.rs ├── from.rs ├── from_str.rs ├── into.rs ├── into_iterator.rs ├── serde.rs └── try_from.rs ├── lib.rs ├── sanitizers ├── custom.rs ├── dedup.rs ├── each.rs ├── lowercase.rs ├── sort.rs ├── trim.rs └── uppercase.rs ├── ui ├── conditionals │ ├── invalid_compile_time_regex.rs │ └── invalid_compile_time_regex.stderr ├── fail │ ├── custom_derive_empty_generics_parameters.rs │ ├── custom_derive_empty_generics_parameters.stderr │ ├── derive_default.rs │ ├── derive_default.stderr │ ├── derive_from.rs │ ├── derive_from.stderr │ ├── float_ord_eq_without_NAN_exclusion.rs │ └── float_ord_eq_without_NAN_exclusion.stderr └── pass │ ├── float_eq_with_finite.rs │ ├── multiple_validators.rs │ ├── sign_validators_on_all_numbers.rs │ └── validators_reuse.rs └── validators ├── custom.rs ├── each.rs ├── exactly.rs ├── finite.rs ├── length.rs ├── max.rs ├── max_length.rs ├── max_length_or_eq.rs ├── max_or_eq.rs ├── min.rs ├── min_length.rs ├── min_length_or_eq.rs ├── min_or_eq.rs ├── negative.rs ├── not_empty.rs ├── not_infinite.rs ├── not_nan.rs ├── positive.rs ├── predicate.rs └── regex.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [ 3 | "-Dclippy::all", 4 | ## Conflicts ## 5 | "-Aclippy::blanket_clippy_restriction_lints", # clippy::restriction - can't enable clippy::restriction if this is enabled 6 | "-Aclippy::implicit_return", # clippy::needless-return - remove return keyword when not needed 7 | "-Aclippy::pub-with-shorthand", # clippy::pub-without-shorthand - pub() instead of pub(in ) 8 | "-Aclippy::self_named_module_files", # clippy::mod_module_files 9 | "-Aclippy::mod_module_files", # clippy::self_named_module_files 10 | "-Aclippy::separated_literal_suffix", # clippy::unseparated_literal_suffix - 1.0_f64 vs 1.0f64 11 | ## Allowed ## 12 | "-Aclippy::missing_trait_methods", # convenience 13 | "-Aclippy::question_mark_used", # convenience 14 | "-Aclippy::redundant_pub_crate", # prefer explicitness 15 | "-Aclippy::renamed_function_params", # I do what I want damn 16 | "-Aclippy::single_call_fn", # convenience 17 | "-Aclippy::ref_patterns", 18 | "-Aclippy::arbitrary_source_item_ordering", # Too much even for me 19 | ## Maybe remove one day ## 20 | "-Aclippy::missing_docs_in_private_items", 21 | ] 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: "cargo" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | groups: 9 | production-dependencies: 10 | dependency-type: "production" 11 | development-dependencies: 12 | dependency-type: "development" 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "monthly" 18 | groups: 19 | actions-deps: 20 | patterns: 21 | - "*" 22 | -------------------------------------------------------------------------------- /.github/workflows/hygiene.yml: -------------------------------------------------------------------------------- 1 | name: Hygiene 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - ready_for_review 12 | workflow_dispatch: 13 | 14 | env: 15 | MSRV: 1.82.0 16 | 17 | permissions: {} 18 | 19 | jobs: 20 | code-hygiene: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4.2.2 24 | with: 25 | persist-credentials: false 26 | 27 | - name: Install Rust 28 | uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 29 | with: 30 | components: rustfmt, clippy 31 | toolchain: ${{ env.MSRV }} 32 | 33 | - name: Rustfmt 34 | run: cargo fmt --all --check 35 | 36 | - name: Clippy 37 | run: cargo clippy --all-features 38 | 39 | - name: Check for typos 40 | uses: crate-ci/typos@v1.30.1 41 | 42 | tests: 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4.2.2 46 | with: 47 | submodules: true 48 | persist-credentials: false 49 | 50 | - name: Install Rust 51 | uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 52 | with: 53 | components: rust-src # https://github.com/dtolnay/trybuild?tab=readme-ov-file#troubleshooting 54 | toolchain: ${{ env.MSRV }} 55 | 56 | - name: "Tests (features: default)" 57 | run: cargo test --no-fail-fast 58 | 59 | - name: "Tests (features: none)" 60 | run: cargo test --no-default-features --no-fail-fast 61 | 62 | - name: "Tests (features: all)" 63 | run: cargo test --all-features --no-fail-fast 64 | 65 | zizmor: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v4.2.2 69 | with: 70 | submodules: true 71 | persist-credentials: false 72 | 73 | - name: Install Zizmor 74 | uses: taiki-e/install-action@v2.49.18 75 | with: 76 | tool: zizmor 77 | 78 | - name: Zizmor 79 | run: zizmor . 80 | env: 81 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release & Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | ensure-doc: 13 | name: Ensure documentation builds 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4.2.2 17 | with: 18 | submodules: true 19 | persist-credentials: false 20 | 21 | - name: Install Rust 22 | uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 23 | with: 24 | toolchain: nightly 25 | cache: false 26 | 27 | - name: Install cargo-doc-rs 28 | uses: taiki-e/install-action@v2.49.18 29 | with: 30 | tool: cargo-docs-rs 31 | 32 | - run: cargo docs-rs 33 | 34 | release: 35 | runs-on: ubuntu-latest 36 | needs: ensure-doc 37 | steps: 38 | - uses: actions/checkout@v4.2.2 39 | with: 40 | submodules: true 41 | persist-credentials: false 42 | 43 | - name: Install Rust (nightly) 44 | uses: actions-rust-lang/setup-rust-toolchain@v1.11.0 45 | with: 46 | toolchain: nightly 47 | cache: false 48 | 49 | - name: Publish to crates.io 50 | run: cargo +nightly -Z package-workspace publish --workspace --locked 51 | env: 52 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project are documented in this file. 4 | 5 | ## [v1.1.0] - 2025-03-10 6 | 7 | - **Feature:** add `AsRef` derive ([34e07733](https://github.com/vic1707/nnn/commit/34e07733)). 8 | - **Feature:** add `Deref` derive ([3e9c7fa1](https://github.com/vic1707/nnn/commit/3e9c7fa1)). 9 | - **Feature:** allow some derives to take generic inputs as targets ([bd29f3f1](https://github.com/vic1707/nnn/commit/bd29f3f1)) 10 | - **Improvement:**: Add `CHANGELOG.md` ([3fc6aff7](https://github.com/vic1707/nnn/commit/3fc6aff7)). 11 | 12 | ## [v1.0.2] - 2025-03-09 13 | 14 | ### Fixed 15 | 16 | - **Fix:** Add compile tests for `nnn_derives` and fix `From` ([cb628ad](https://github.com/vic1707/nnn/commit/cb628ad)) 17 | - **Chore:** Code formatting update ([5b6d08c](https://github.com/vic1707/nnn/commit/5b6d08c)) 18 | 19 | ## [v1.0.1] - 2025-03-09 20 | 21 | - **Fix:** Correct missing use statements and syntax in generated code ([5365393](https://github.com/vic1707/nnn/commit/5365393)) 22 | - **Docs:** Improve and add missing documentation ([21af9cb](https://github.com/vic1707/nnn/commit/21af9cb)) 23 | - **Chore:** Update dependencies ([0f2d744](https://github.com/vic1707/nnn/commit/0f2d744)) 24 | 25 | ## [v1.0.0] - 2025-03-09 26 | 27 | - **Feature:** Add `IntoIterator` derive for nnn ([4564665](https://github.com/vic1707/nnn/commit/4564665)) 28 | - **Feature:** Merge in newtype trait changes ([ef3453d](https://github.com/vic1707/nnn/commit/ef3453d)) 29 | - **Feature:** Add test for `FromStr` derive ([6877dd9](https://github.com/vic1707/nnn/commit/6877dd9)) 30 | - **Chore:** Update dependencies ([6ee2c6e](https://github.com/vic1707/nnn/commit/6ee2c6e)) 31 | - **Chore:** Prepare for 2024 edition ([593cff8](https://github.com/vic1707/nnn/commit/593cff8)) 32 | 33 | ## [v0.1.2] - 2025-01-11 34 | 35 | - **Chore:** Macro is now `#![no_std]` ([c49fa04](https://github.com/vic1707/nnn/commit/c49fa04)) 36 | - **Chore:** Update dependencies ([7abbdb9](https://github.com/vic1707/nnn/commit/7abbdb9)) 37 | 38 | ## [v0.1.1] - 2025-01-01 39 | 40 | - **Fix:** Remove `BorrowMut` derive as it can break constraints ([19c9543](https://github.com/vic1707/nnn/commit/19c9543)) 41 | 42 | ## [v0.1.0] - 2025-01-01 43 | 44 | - **Fix:** Resolve validators (`positive` & `negative`) issues for `f32` & `f64` ([9c90d9e](https://github.com/vic1707/nnn/commit/9c90d9e)) 45 | - **Chore:** Address potential clippy error ([f4d2b0f](https://github.com/vic1707/nnn/commit/f4d2b0f)) 46 | 47 | ## [v0.0.2] - 2024-12-30 48 | 49 | - **Fix:** `docs.rs` badge display ([147b2c3](https://github.com/vic1707/nnn/commit/147b2c3)) 50 | - **Chore:** `Default` implementation needlessly used `try_new` ([4694041](https://github.com/vic1707/nnn/commit/4694041)) 51 | 52 | ## [v0.0.1] - 2024-12-29 53 | 54 | Initial Release 55 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.4.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 19 | 20 | [[package]] 21 | name = "cfg-if" 22 | version = "1.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 25 | 26 | [[package]] 27 | name = "equivalent" 28 | version = "1.0.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 31 | 32 | [[package]] 33 | name = "futures-core" 34 | version = "0.3.31" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 37 | 38 | [[package]] 39 | name = "futures-macro" 40 | version = "0.3.31" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" 43 | dependencies = [ 44 | "proc-macro2", 45 | "quote", 46 | "syn", 47 | ] 48 | 49 | [[package]] 50 | name = "futures-task" 51 | version = "0.3.31" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" 54 | 55 | [[package]] 56 | name = "futures-timer" 57 | version = "3.0.3" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" 60 | 61 | [[package]] 62 | name = "futures-util" 63 | version = "0.3.31" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" 66 | dependencies = [ 67 | "futures-core", 68 | "futures-macro", 69 | "futures-task", 70 | "pin-project-lite", 71 | "pin-utils", 72 | "slab", 73 | ] 74 | 75 | [[package]] 76 | name = "glob" 77 | version = "0.3.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 80 | 81 | [[package]] 82 | name = "hashbrown" 83 | version = "0.15.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" 86 | 87 | [[package]] 88 | name = "indexmap" 89 | version = "2.7.1" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" 92 | dependencies = [ 93 | "equivalent", 94 | "hashbrown", 95 | ] 96 | 97 | [[package]] 98 | name = "itoa" 99 | version = "1.0.15" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 102 | 103 | [[package]] 104 | name = "memchr" 105 | version = "2.7.4" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 108 | 109 | [[package]] 110 | name = "nnn" 111 | version = "1.1.0" 112 | dependencies = [ 113 | "nnn-macros", 114 | "paste", 115 | "regex", 116 | "rstest", 117 | "serde", 118 | "serde_json", 119 | "trybuild", 120 | ] 121 | 122 | [[package]] 123 | name = "nnn-macros" 124 | version = "1.1.0" 125 | dependencies = [ 126 | "proc-macro2", 127 | "quote", 128 | "regex", 129 | "syn", 130 | ] 131 | 132 | [[package]] 133 | name = "paste" 134 | version = "1.0.15" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 137 | 138 | [[package]] 139 | name = "pin-project-lite" 140 | version = "0.2.16" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" 143 | 144 | [[package]] 145 | name = "pin-utils" 146 | version = "0.1.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 149 | 150 | [[package]] 151 | name = "proc-macro-crate" 152 | version = "3.3.0" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" 155 | dependencies = [ 156 | "toml_edit", 157 | ] 158 | 159 | [[package]] 160 | name = "proc-macro2" 161 | version = "1.0.94" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" 164 | dependencies = [ 165 | "unicode-ident", 166 | ] 167 | 168 | [[package]] 169 | name = "quote" 170 | version = "1.0.39" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" 173 | dependencies = [ 174 | "proc-macro2", 175 | ] 176 | 177 | [[package]] 178 | name = "regex" 179 | version = "1.11.1" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 182 | dependencies = [ 183 | "aho-corasick", 184 | "memchr", 185 | "regex-automata", 186 | "regex-syntax", 187 | ] 188 | 189 | [[package]] 190 | name = "regex-automata" 191 | version = "0.4.9" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 194 | dependencies = [ 195 | "aho-corasick", 196 | "memchr", 197 | "regex-syntax", 198 | ] 199 | 200 | [[package]] 201 | name = "regex-syntax" 202 | version = "0.8.5" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 205 | 206 | [[package]] 207 | name = "relative-path" 208 | version = "1.9.3" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" 211 | 212 | [[package]] 213 | name = "rstest" 214 | version = "0.25.0" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" 217 | dependencies = [ 218 | "futures-timer", 219 | "futures-util", 220 | "rstest_macros", 221 | "rustc_version", 222 | ] 223 | 224 | [[package]] 225 | name = "rstest_macros" 226 | version = "0.25.0" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" 229 | dependencies = [ 230 | "cfg-if", 231 | "glob", 232 | "proc-macro-crate", 233 | "proc-macro2", 234 | "quote", 235 | "regex", 236 | "relative-path", 237 | "rustc_version", 238 | "syn", 239 | "unicode-ident", 240 | ] 241 | 242 | [[package]] 243 | name = "rustc_version" 244 | version = "0.4.1" 245 | source = "registry+https://github.com/rust-lang/crates.io-index" 246 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 247 | dependencies = [ 248 | "semver", 249 | ] 250 | 251 | [[package]] 252 | name = "ryu" 253 | version = "1.0.20" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 256 | 257 | [[package]] 258 | name = "semver" 259 | version = "1.0.26" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 262 | 263 | [[package]] 264 | name = "serde" 265 | version = "1.0.218" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 268 | dependencies = [ 269 | "serde_derive", 270 | ] 271 | 272 | [[package]] 273 | name = "serde_derive" 274 | version = "1.0.218" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 277 | dependencies = [ 278 | "proc-macro2", 279 | "quote", 280 | "syn", 281 | ] 282 | 283 | [[package]] 284 | name = "serde_json" 285 | version = "1.0.140" 286 | source = "registry+https://github.com/rust-lang/crates.io-index" 287 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 288 | dependencies = [ 289 | "itoa", 290 | "memchr", 291 | "ryu", 292 | "serde", 293 | ] 294 | 295 | [[package]] 296 | name = "serde_spanned" 297 | version = "0.6.8" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" 300 | dependencies = [ 301 | "serde", 302 | ] 303 | 304 | [[package]] 305 | name = "slab" 306 | version = "0.4.9" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 309 | dependencies = [ 310 | "autocfg", 311 | ] 312 | 313 | [[package]] 314 | name = "syn" 315 | version = "2.0.99" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" 318 | dependencies = [ 319 | "proc-macro2", 320 | "quote", 321 | "unicode-ident", 322 | ] 323 | 324 | [[package]] 325 | name = "target-triple" 326 | version = "0.1.4" 327 | source = "registry+https://github.com/rust-lang/crates.io-index" 328 | checksum = "1ac9aa371f599d22256307c24a9d748c041e548cbf599f35d890f9d365361790" 329 | 330 | [[package]] 331 | name = "termcolor" 332 | version = "1.4.1" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 335 | dependencies = [ 336 | "winapi-util", 337 | ] 338 | 339 | [[package]] 340 | name = "toml" 341 | version = "0.8.20" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" 344 | dependencies = [ 345 | "serde", 346 | "serde_spanned", 347 | "toml_datetime", 348 | "toml_edit", 349 | ] 350 | 351 | [[package]] 352 | name = "toml_datetime" 353 | version = "0.6.8" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 356 | dependencies = [ 357 | "serde", 358 | ] 359 | 360 | [[package]] 361 | name = "toml_edit" 362 | version = "0.22.24" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" 365 | dependencies = [ 366 | "indexmap", 367 | "serde", 368 | "serde_spanned", 369 | "toml_datetime", 370 | "winnow", 371 | ] 372 | 373 | [[package]] 374 | name = "trybuild" 375 | version = "1.0.104" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "6ae08be68c056db96f0e6c6dd820727cca756ced9e1f4cc7fdd20e2a55e23898" 378 | dependencies = [ 379 | "glob", 380 | "serde", 381 | "serde_derive", 382 | "serde_json", 383 | "target-triple", 384 | "termcolor", 385 | "toml", 386 | ] 387 | 388 | [[package]] 389 | name = "unicode-ident" 390 | version = "1.0.18" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 393 | 394 | [[package]] 395 | name = "winapi-util" 396 | version = "0.1.9" 397 | source = "registry+https://github.com/rust-lang/crates.io-index" 398 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 399 | dependencies = [ 400 | "windows-sys", 401 | ] 402 | 403 | [[package]] 404 | name = "windows-sys" 405 | version = "0.59.0" 406 | source = "registry+https://github.com/rust-lang/crates.io-index" 407 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 408 | dependencies = [ 409 | "windows-targets", 410 | ] 411 | 412 | [[package]] 413 | name = "windows-targets" 414 | version = "0.52.6" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 417 | dependencies = [ 418 | "windows_aarch64_gnullvm", 419 | "windows_aarch64_msvc", 420 | "windows_i686_gnu", 421 | "windows_i686_gnullvm", 422 | "windows_i686_msvc", 423 | "windows_x86_64_gnu", 424 | "windows_x86_64_gnullvm", 425 | "windows_x86_64_msvc", 426 | ] 427 | 428 | [[package]] 429 | name = "windows_aarch64_gnullvm" 430 | version = "0.52.6" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 433 | 434 | [[package]] 435 | name = "windows_aarch64_msvc" 436 | version = "0.52.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 439 | 440 | [[package]] 441 | name = "windows_i686_gnu" 442 | version = "0.52.6" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 445 | 446 | [[package]] 447 | name = "windows_i686_gnullvm" 448 | version = "0.52.6" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 451 | 452 | [[package]] 453 | name = "windows_i686_msvc" 454 | version = "0.52.6" 455 | source = "registry+https://github.com/rust-lang/crates.io-index" 456 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 457 | 458 | [[package]] 459 | name = "windows_x86_64_gnu" 460 | version = "0.52.6" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 463 | 464 | [[package]] 465 | name = "windows_x86_64_gnullvm" 466 | version = "0.52.6" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 469 | 470 | [[package]] 471 | name = "windows_x86_64_msvc" 472 | version = "0.52.6" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 475 | 476 | [[package]] 477 | name = "winnow" 478 | version = "0.7.3" 479 | source = "registry+https://github.com/rust-lang/crates.io-index" 480 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" 481 | dependencies = [ 482 | "memchr", 483 | ] 484 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nnn" 3 | version = "1.1.0" 4 | edition = "2021" 5 | authors = ["Victor LEFEBVRE "] 6 | description = "Generate your newtypes from a single macro." 7 | documentation = "https://docs.rs/nnn" 8 | homepage = "https://github.com/vic1707/nnn" 9 | license = "WTFPL" 10 | readme = "README.md" 11 | repository = "https://github.com/vic1707/nnn" 12 | categories = ["data-structures", "rust-patterns"] 13 | keywords = ["newtype", "validation", "sanitization", "derive", "invariant"] 14 | rust-version = "1.82.0" 15 | 16 | [workspace] 17 | resolver = "2" 18 | members = ["nnn-macros"] 19 | 20 | [dependencies] 21 | nnn-macros = { version = "1.1.0", path = "./nnn-macros" } 22 | 23 | [features] 24 | default = [] 25 | regex_validation = ["nnn-macros/regex_validation"] 26 | 27 | [dev-dependencies] 28 | paste = "1.0.15" 29 | regex = "1.11.1" 30 | rstest = "0.25.0" 31 | serde = { version = "1.0.218", features = ["derive"] } 32 | serde_json = "1.0.140" 33 | trybuild = "1.0.104" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 2 | Version 2, December 2004 3 | 4 | Copyright (C) 2004 Sam Hocevar 5 | 6 | Everyone is permitted to copy and distribute verbatim or modified 7 | copies of this license document, and changing it is allowed as long 8 | as the name is changed. 9 | 10 | DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE 11 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 12 | 13 | 0. You just DO WHAT THE FUCK YOU WANT TO. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nnn 2 | 3 | [github](https://github.com/vic1707/nnn) 4 | [crates.io](https://crates.io/crates/nnn) 5 | [build status](https://github.com/vic1707/nnn/actions?query=branch%3Amain) 6 | [docs.rs](https://docs.rs/nnn) 7 | [lines](https://github.com/vic1707/nnn) 8 | [maintenance](https://github.com/vic1707/nnn) 9 | 10 | ### nnn Crate Documentation 11 | 12 | The `nnn` crate provides a procedural macro to help create [`newtype`](https://doc.rust-lang.org/rust-by-example/generics/new_types.html)s with validation and sanitization based on a specified set of rules. Its design focuses on being as slim and non-intrusive as possible. 13 | 14 | #### Philosophy 15 | 16 | The primary goal of `nnn` is to provide tools, not guardrails, the only errors returned by `nnn` are parsing/syntax errors and footguns. 17 | (e.g., `nnn` allows using a `finite` validator on a `String`—though this will not compile because `String` lacks `.is_finite()`. The same applies to the `each` validator and sanitizer, which are only available on inner with `.iter()`.) 18 | 19 | By design, `nnn` doesn’t “hold hands” or attempt to protect users from all possible mistakes. Instead, it prioritizes flexibility and assumes user expertise. 20 | 21 | --- 22 | 23 | #### Inspirations 24 | 25 | The `nnn` crate draws heavy inspiration from the excellent [`nutype`](https://docs.rs/nutype/), borrowing much of its syntax and approach. While `nutype` offers robust features, its internal complexity motivated this project. 26 | 27 | This crate was developed as a fun, 3-week challenge to explore whether `nutype`'s functionality could be reimagined in a simpler form. The result is `nnn`, which aims to provide a more streamlined experience without sacrificing power. 28 | 29 | --- 30 | 31 | #### Complete example 32 | 33 | ```rs 34 | use nnn::nnn; 35 | 36 | #[nnn( 37 | derive(Debug, PartialEq, Eq, PartialOrd, Ord), 38 | nnn_derive(TryFrom), 39 | consts( 40 | ZERO = 0.0_f64, 41 | pub ONE = 1.0_f64, 42 | ), 43 | default = 5.0_f64, 44 | validators(finite, positive), 45 | sanitizers(custom = |v: f64| v.abs()), 46 | // Serialize & Deserialize are only available in test env. 47 | cfg(test, derive(Serialize, Deserialize)), 48 | attrs( 49 | repr(transparent), 50 | ), 51 | )] 52 | struct PositiveFiniteFloat(f64); 53 | ``` 54 | 55 | I encourage you to see what code is generated using [`cargo-expand`](https://github.com/dtolnay/cargo-expand). 56 | 57 | #### Usage 58 | 59 | Every argument in a `nnn` declaration must be provided in a single macro invocation, so 60 | 61 | ```rs 62 | #[nnn(derive(Debug))] 63 | #[nnn(default = 5.0_f64)] 64 | struct Dummy(f64); 65 | ``` 66 | 67 | is invalid. 68 | 69 | Note that `derive` and other attributes must be passed via `nnn` to make it clear that `nnn` manages them. 70 | 71 | ##### Arguments 72 | 73 | Below is a complete list of supported arguments for the `nnn` macro: 74 | 75 | - **`cfg(, )`**: Adds conditional compilation to the provided arguments. 76 | 77 | - **`consts(pub EIGHT = 8, pub(in crate) NINE = 9, ...)`**: Defines associated constants for the newtype. 78 | _Note:_ `nnn` will generate unit tests ensuring these values are correct. 79 | 80 | - **`derive(, ...)`**: Specifies standard Rust derives for the newtype. 81 | 82 | - **`nnn_derive(, ...)`**: Declares specific derives re-implemented by `nnn` that ensure validation and sanitization are applied where appropriate. 83 | 84 | - **`default` or `default = ...`**: Defines a default value for the newtype. Can be either: 85 | 86 | - `#[nnn(default)]`: Uses the inner type's default value. 87 | - `#[nnn(default = ...)]`: Specifies a custom default value. 88 | 89 | _Note:_ `nnn` will generate a unit test ensuring the default value is correct. 90 | 91 | - **`new_unchecked`**: Enables the generation of the unsafe method `const fn new_unchecked(v: ) -> Self` that bypasses validation and sanitization. 92 | 93 | - **`sanitizers()`**: Declares a list of sanitizers to apply to the input. 94 | _Note:_ Sanitization is executed **before** validation. 95 | 96 | - **`validators()`**: Declares a list of validators to ensure the input meets the desired conditions. 97 | 98 | - **`attrs()`**: Specifies additional attributes for the newtype, such as `#[repr(C)]` or `#[non_exhaustive]`. 99 | 100 | ##### Derive Handling 101 | 102 | While most derives are passed through transparently, there are exceptions: 103 | 104 | 1. **`Eq` & `Ord`** 105 | 106 | Automatically implemented except when the `finite` or `not_nan` validator is provided, in which case a manual implementation is generated. 107 | 108 | 2. **`Deserialize`** 109 | 110 | Passed transparently, with `nnn` injecting `#[serde(try_from = "")]` to ensure validation and sanitization during deserialization. 111 | 112 | **Note:** Some derives are disallowed as they bypass validation. For these cases, `nnn` provides a custom `nnn_derive` to replace standard derives while ensuring validation and sanitization are preserved. 113 | 114 | ##### Custom derives 115 | 116 | `nnn` provides custom implementations of some common derives to implement `nnn`'s guarantees. 117 | 118 | 1. **`Into`/`From`/`Borrow`/`AsRef`/`Deref`** 119 | 120 | These derives their respective traits to convert from a new-type to its inner type. 121 | These derives can take generic inputs as parameters, `#[nnn_derive(Into)]` will generate derives for `Into`/`Into`/`Into` for the new_type. `#[nnn_derive(Into)]` still defaults to deriving `Into`. 122 | 123 | _Note:_ `_`as a generic parameter will be translated to``, e.g: 124 | 125 | ```rs 126 | #[nnn_derive(From<_>)] 127 | struct A(i8); 128 | ``` 129 | 130 | will implement `From` for the new-type. 131 | 132 | 2. **`TryFrom`** 133 | 134 | Implements `TryFrom` and calls the `try_new` methods. 135 | `TryFrom` can take generic parameters as parameters, `#[nnn_derive(TryFrom)]` will generate derives for `TryFrom`/`TryFrom`/`TryFrom`. `#[nnn_derive(TryFrom)]` still defaults to deriving `TryFrom`. 136 | 137 | _Note:_ `_` as a generic parameter will be translated to ``, e.g: 138 | 139 | ```rs 140 | #[nnn_derive(TryFrom<_>)] 141 | struct A(i8); 142 | ``` 143 | 144 | will implement `TryFrom` for the new-type. 145 | 146 | 3. **`FromStr`** 147 | 148 | Generates an implementation using the inner `FromStr` implementation and passing parsed value through sanitization and validation. 149 | It generates the following error enum implementing `Debug`, `Clone`, `PartialEq`, `Eq` and `Display`. 150 | 151 | ```rs 152 | enum /* new_type's name */ParseError { 153 | InnerParse(::Err), 154 | Validation(/* new_type's name */Error), 155 | } 156 | ``` 157 | 158 | 4. `IntoIterator` 159 | 160 | Implements `IntoIterator` for the new-type and a reference to it, the iterator yields items from the inner type's iterator implementation. 161 | 162 | --- 163 | 164 | #### Sanitizers 165 | 166 | _To see examples, each sanitizer is tested in the [test folder](./tests/sanitizers)._ 167 | 168 | The `sanitizers` argument accepts the following options: 169 | 170 | - **Iterables**: 171 | 172 | - `each(...)`: Applies sanitizers to each element in a collection. 173 | - `sort`: Sorts the elements in the collection. 174 | - `dedup`: Removes duplicate elements from the collection using rust's `dedup` which only removes consecutive duplicates. 175 | 176 | - **Strings**: 177 | 178 | - `trim`: Removes leading and trailing whitespace. 179 | - `lowercase`: Converts the string to lowercase. 180 | - `uppercase`: Converts the string to uppercase. 181 | 182 | - **Common**: 183 | - `custom = ...`: Specifies a custom sanitization function. 184 | 185 | --- 186 | 187 | #### Validators 188 | 189 | _To see examples, each validator is tested in the [test folder](./tests/validators)._ 190 | 191 | Each validator generates a specific variant for the corresponding error type which has the same visibility as the `new_type`. 192 | This error enum implements the traits: `Debug`, `Error`, `Display`, `Clone`, `PartialEq`, `Eq`. 193 | 194 | ```rs 195 | enum /* new_type's name */Error { 196 | Positive, 197 | Finite, 198 | /// idx of the problematic element, inner error 199 | Each(usize, Box), 200 | /* ... */ 201 | } 202 | ``` 203 | 204 | The `validators` argument accepts the following options: 205 | 206 | - **Iterables**: 207 | 208 | - `not_empty`: Ensures an iterable is not empty. 209 | - `each(...)`: Applies validators to each element in an iterable. 210 | - `min_length = ...`: Ensures the iterable has a minimum length. 211 | - `min_length_or_eq = ...`: Ensures the iterable has a minimum length or is equal to the specified length. 212 | - `length = ...`: Ensures the iterable has an exact length. 213 | - `max_length = ...`: Ensures the iterable has a maximum length. 214 | - `max_length_or_eq = ...`: Ensures the iterable has a maximum length or is equal to the specified length. 215 | 216 | - **Numerics**: 217 | 218 | - `min = ...`: Ensures the value is greater than this value. 219 | - `min_or_eq = ...`: Ensures the value is greater than or equal to this value. 220 | - `max = ...`: Ensures the value is less than this value. 221 | - `max_or_eq = ...`: Ensures the value is less than or equal to this value. 222 | - `positive`: Ensures the value is positive (excluding 0/-0/NAN). 223 | - `negative`: Ensures the value is negative (excluding 0/-0/NAN). 224 | 225 | - **Float specifics**: 226 | 227 | - `finite`: Ensures the value is finite. 228 | - `not_infinite`: Ensures the value is not infinite. 229 | - `not_nan`: Ensures the value is not NaN. 230 | 231 | - **String specifics**: 232 | 233 | - `regex = ...`: Ensures the string matches a regular expression. You can pass a raw string or a variable. 234 | _Note:_ A test is generated to ensure the pattern is valid. 235 | _Note2:_ raw string regex can be checked at compile time with an optional feature (see: [Optional Features](#Optional-Features)). 236 | 237 | - **Common**: 238 | 239 | - `exactly = ...`: Ensures the value is equal to the specified value. 240 | - `custom(with = ..., error = ...)`: Validates using a custom function, specifying an error path. 241 | - `predicate(with = ..., error_name = ...)`: Uses a predicate function with an optional custom error variant name (defaults to `Predicate`). 242 | 243 | ##### _Note:_ The `with =` argument to the `custom` and `predicate` validator/sanitizer can be of 3 forms: 244 | 245 | - **inlined closure:** `with = |str: &String| f64::from_str(str)` 246 | - **function path:** `with = f64::from_str` 247 | - **inlined block:** 248 | These must use the variable `mut value: `. 249 | - _For validators:_ `with = { f64::from_str(&value) }` 250 | - _For sanitizers:_ `with = { value = value.to_uppercase(); }` 251 | 252 | --- 253 | 254 | #### Optional Features 255 | 256 | - **Compile-Time Regex**: When `regex_validation` is enabled, raw literal regex patterns are validated at compile time, so you don't have to run the generated test every time. 257 | 258 | --- 259 | 260 | #### Why that name 261 | 262 | For those who wonder, the name `nnn` reflects a 3-week, carefree adventure with no expectations — it's simply 'n' for 'newtype', tapped a random number of times. 263 | 264 | ![](https://i.makeagif.com/media/9-14-2024/8wcpfp.gif) 265 | 266 | Ladies and Gentelmens, welcome to n-n-n-newtypes. 267 | 268 | #### License 269 | 270 | This project is licensed under the **[WTFPL](./LICENSE)**. 271 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | allow-dbg-in-tests = true 2 | allow-expect-in-tests = true 3 | allow-unwrap-in-tests = true -------------------------------------------------------------------------------- /nnn-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nnn-macros" 3 | version = "1.1.0" 4 | edition = "2021" 5 | authors = ["Victor LEFEBVRE "] 6 | description = "Generate your newtypes from a single macro." 7 | documentation = "https://docs.rs/nnn" 8 | homepage = "https://github.com/vic1707/nnn" 9 | license = "WTFPL" 10 | readme = "README.md" 11 | repository = "https://github.com/vic1707/nnn" 12 | categories = ["data-structures", "rust-patterns"] 13 | keywords = ["newtype", "validation", "sanitization", "derive", "invariant"] 14 | rust-version = "1.82.0" 15 | 16 | [lib] 17 | proc-macro = true 18 | 19 | [features] 20 | default = [] 21 | regex_validation = ["dep:regex"] 22 | 23 | [dependencies] 24 | proc-macro2 = "1.0.94" 25 | quote = "1.0.39" 26 | regex = { version = "1.11.1", optional = true } 27 | syn = { version = "2.0.99", features = ["full"] } 28 | -------------------------------------------------------------------------------- /nnn-macros/LICENSE: -------------------------------------------------------------------------------- 1 | ../LICENSE -------------------------------------------------------------------------------- /nnn-macros/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /nnn-macros/src/argument.rs: -------------------------------------------------------------------------------- 1 | /* Modules */ 2 | mod associated_const; 3 | mod cfg; 4 | mod default; 5 | mod derive; 6 | mod new_unchecked; 7 | mod nnn_derive; 8 | mod sanitizer; 9 | mod validator; 10 | /* Built-in imports */ 11 | extern crate alloc; 12 | use alloc::{string::ToString as _, vec, vec::Vec}; 13 | /* Crate imports */ 14 | use self::{ 15 | associated_const::AssociatedConst, cfg::Cfg, default::Default, 16 | derive::Derive, new_unchecked::NewUnchecked, nnn_derive::NNNDerive, 17 | sanitizer::Sanitizer, validator::Validator, 18 | }; 19 | use crate::{ 20 | codegen::{self, Gen as _}, 21 | utils::syn_ext::SynParseBufferExt as _, 22 | }; 23 | /* Dependencies */ 24 | use syn::{ 25 | parse::{Parse, ParseStream}, 26 | parse_quote, 27 | punctuated::Punctuated, 28 | token::Comma, 29 | }; 30 | 31 | #[derive(Default)] 32 | pub(crate) struct Arguments { 33 | nnn_derives: Vec, 34 | consts: Vec, 35 | derives: Vec, 36 | default: Option, 37 | new_unchecked: Option, 38 | sanitizers: Vec, 39 | validators: Vec, 40 | cfgs: Vec, 41 | transparents: Vec, 42 | } 43 | 44 | impl Arguments { 45 | pub(crate) fn get_impls( 46 | &self, 47 | ctx: &crate::Context, 48 | ) -> Vec { 49 | (self.nnn_derives.iter().flat_map(|der| der.gen_impl(ctx))) 50 | .chain(self.cfgs.iter().flat_map(|cfg| cfg.gen_impl(ctx))) 51 | .chain(self.consts.iter().flat_map(|cst| cst.gen_impl(ctx))) 52 | .chain(self.derives.iter().flat_map(|der| der.gen_impl(ctx))) 53 | .chain(self.default.iter().flat_map(|def| def.gen_impl(ctx))) 54 | .chain(self.new_unchecked.iter().flat_map(|nu| nu.gen_impl(ctx))) 55 | .chain(self.sanitizers.iter().flat_map(|san| san.gen_impl(ctx))) 56 | .chain(self.validators.iter().flat_map(|val| val.gen_impl(ctx))) 57 | .chain( 58 | self.transparents 59 | .iter() 60 | .map(|meta| parse_quote! { #[#meta] }) 61 | .map(|attr| codegen::Implementation::Attribute(vec![attr])), 62 | ) 63 | .collect() 64 | } 65 | 66 | pub(crate) fn get_tests( 67 | &self, 68 | ctx: &crate::Context, 69 | ) -> Vec { 70 | (self.nnn_derives.iter().flat_map(|der| der.gen_tests(ctx))) 71 | .chain(self.cfgs.iter().flat_map(|cfg| cfg.gen_tests(ctx))) 72 | .chain(self.consts.iter().flat_map(|cst| cst.gen_tests(ctx))) 73 | .chain(self.derives.iter().flat_map(|der| der.gen_tests(ctx))) 74 | .chain(self.default.iter().flat_map(|def| def.gen_tests(ctx))) 75 | .chain(self.new_unchecked.iter().flat_map(|nu| nu.gen_tests(ctx))) 76 | .chain(self.sanitizers.iter().flat_map(|san| san.gen_tests(ctx))) 77 | .chain(self.validators.iter().flat_map(|val| val.gen_tests(ctx))) 78 | .collect() 79 | } 80 | } 81 | 82 | impl From> for Arguments { 83 | fn from(punctuated_args: Punctuated) -> Self { 84 | let mut args = Self::default(); 85 | for arg in punctuated_args { 86 | match arg { 87 | Argument::NNNDerive(derives) => { 88 | args.nnn_derives.extend(derives); 89 | }, 90 | Argument::Cfg(cfg) => args.cfgs.push(cfg), 91 | Argument::Consts(consts) => args.consts.extend(consts), 92 | Argument::Derive(derives) => args.derives.extend(derives), 93 | Argument::Default(default) => args.default = Some(default), 94 | Argument::NewUnchecked(nu) => args.new_unchecked = Some(nu), 95 | Argument::Sanitizers(sanitizers) => { 96 | args.sanitizers.extend(sanitizers); 97 | }, 98 | Argument::Validators(validators) => { 99 | args.validators.extend(validators); 100 | }, 101 | Argument::Transparent(metas) => { 102 | args.transparents.extend(metas); 103 | }, 104 | } 105 | } 106 | args 107 | } 108 | } 109 | 110 | pub(crate) enum Argument { 111 | NNNDerive(Punctuated), 112 | Cfg(Cfg), 113 | Consts(Punctuated), 114 | Derive(Punctuated), 115 | Default(Default), 116 | NewUnchecked(NewUnchecked), 117 | Sanitizers(Punctuated), 118 | Validators(Punctuated), 119 | Transparent(Punctuated), 120 | } 121 | 122 | impl Parse for Argument { 123 | fn parse(input: ParseStream) -> syn::Result { 124 | let ident = input.parse::()?; 125 | let arg = match ident.to_string().as_str() { 126 | "nnn_derive" => { 127 | Self::NNNDerive(input.parse_parenthesized::()?) 128 | }, 129 | "cfg" => Self::Cfg(input.parse::()?), 130 | "consts" => { 131 | Self::Consts(input.parse_parenthesized::()?) 132 | }, 133 | "derive" => Self::Derive(input.parse_parenthesized::()?), 134 | "default" => { 135 | Self::Default(match input.parse_equal::() { 136 | Ok(expr) => Default::WithValue(expr), 137 | Err(_err) => Default::WithInnerDefault, 138 | }) 139 | }, 140 | "new_unchecked" => Self::NewUnchecked(NewUnchecked), 141 | "sanitizers" => { 142 | Self::Sanitizers(input.parse_parenthesized::()?) 143 | }, 144 | "validators" => { 145 | Self::Validators(input.parse_parenthesized::()?) 146 | }, 147 | "attrs" => { 148 | Self::Transparent(input.parse_parenthesized::()?) 149 | }, 150 | _ => { 151 | return Err(syn::Error::new_spanned(ident, "Unknown argument.")) 152 | }, 153 | }; 154 | 155 | if !input.peek(syn::Token![,]) && !input.is_empty() { 156 | return Err(input.error("Unexpected token(s).")); 157 | } 158 | 159 | Ok(arg) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/associated_const.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::format; 4 | use core::iter; 5 | /* Crate imports */ 6 | use crate::{codegen, utils::syn_ext::SynParseBufferExt as _}; 7 | /* Dependencies */ 8 | use quote::format_ident; 9 | use syn::{ 10 | parse::{Parse, ParseStream}, 11 | parse_quote, 12 | }; 13 | 14 | pub(crate) struct AssociatedConst { 15 | visibility: syn::Visibility, 16 | name: syn::Ident, 17 | value: syn::Expr, 18 | } 19 | 20 | impl Parse for AssociatedConst { 21 | fn parse(input: ParseStream) -> syn::Result { 22 | let visibility = input.parse::()?; 23 | let (name, value) = input.parse_assign::()?; 24 | Ok(Self { 25 | visibility, 26 | name, 27 | value, 28 | }) 29 | } 30 | } 31 | 32 | impl codegen::Gen for AssociatedConst { 33 | fn gen_impl( 34 | &self, 35 | _: &crate::Context, 36 | ) -> impl Iterator { 37 | let visibility = &self.visibility; 38 | let const_name = &self.name; 39 | let value = &self.value; 40 | 41 | iter::once(codegen::Implementation::ImplItem(codegen::ImplItem::Const( 42 | parse_quote! { 43 | #visibility const #const_name: Self = Self(#value); 44 | }, 45 | ))) 46 | } 47 | 48 | fn gen_tests( 49 | &self, 50 | ctx: &crate::Context, 51 | ) -> impl Iterator { 52 | let const_name = &self.name; 53 | let type_name = ctx.type_name(); 54 | 55 | let err_msg = format!( 56 | "Type `{type_name}` has invalid value for associated const `{const_name}`.", 57 | ); 58 | let test_name = 59 | format_ident!("const_{const_name}_should_have_valid_value"); 60 | 61 | iter::once(parse_quote! { 62 | #[test] 63 | fn #test_name() { 64 | use nnn::NNNewType as _; 65 | 66 | let inner_value = <#type_name>::#const_name.into_inner(); 67 | <#type_name>::try_new(inner_value).expect(#err_msg); 68 | } 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/cfg.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use super::{Argument, Arguments}; 3 | use crate::codegen; 4 | /* Dependencies */ 5 | use syn::parse::{Parse, ParseStream}; 6 | 7 | pub(crate) struct Cfg { 8 | condition: syn::Expr, 9 | args: Arguments, 10 | } 11 | 12 | impl Parse for Cfg { 13 | fn parse(input: ParseStream) -> syn::Result { 14 | let content; 15 | syn::parenthesized!(content in input); 16 | let condition = content.parse()?; 17 | content.parse::()?; 18 | let args = Arguments::from( 19 | content.parse_terminated(Argument::parse, syn::Token![,])?, 20 | ); 21 | 22 | Ok(Self { condition, args }) 23 | } 24 | } 25 | 26 | impl codegen::Gen for Cfg { 27 | fn gen_impl( 28 | &self, 29 | ctx: &crate::Context, 30 | ) -> impl Iterator { 31 | self.args.get_impls(ctx).into_iter().map(|mut r#impl| { 32 | r#impl.make_conditional(&self.condition); 33 | r#impl 34 | }) 35 | } 36 | 37 | fn gen_tests( 38 | &self, 39 | ctx: &crate::Context, 40 | ) -> impl Iterator { 41 | self.args.get_tests(ctx).into_iter().map(|mut test| { 42 | test.make_conditional(&self.condition); 43 | test 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/default.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::format; 4 | use core::iter; 5 | /* Crate imports */ 6 | use crate::codegen; 7 | /* Dependencies */ 8 | use quote::quote; 9 | use syn::parse_quote; 10 | 11 | pub(crate) enum Default { 12 | WithInnerDefault, 13 | WithValue(syn::Expr), 14 | } 15 | 16 | impl codegen::Gen for Default { 17 | fn gen_impl( 18 | &self, 19 | ctx: &crate::Context, 20 | ) -> impl Iterator { 21 | let type_name = ctx.type_name(); 22 | let (impl_generics, ty_generics, where_clause) = 23 | ctx.generics().split_for_impl(); 24 | 25 | let default_value = match *self { 26 | Self::WithInnerDefault => { 27 | quote! { ::Error::default() } 28 | }, 29 | Self::WithValue(ref expr) => quote! { #expr }, 30 | }; 31 | 32 | iter::once(codegen::Implementation::ItemImpl(parse_quote! { 33 | impl #impl_generics ::core::default::Default for #type_name #ty_generics #where_clause { 34 | fn default() -> Self { 35 | #[doc = "Is checked by automatically generated test."] 36 | Self(#default_value) 37 | } 38 | } 39 | })) 40 | } 41 | 42 | fn gen_tests( 43 | &self, 44 | ctx: &crate::Context, 45 | ) -> impl Iterator { 46 | let type_name = ctx.type_name(); 47 | let err_msg = format!("Type `{type_name}` has invalid default value.",); 48 | 49 | iter::once(parse_quote! { 50 | #[test] 51 | fn should_have_valid_default_value() { 52 | use nnn::NNNewType as _; 53 | 54 | let default_inner = <#type_name>::default().into_inner(); 55 | let rebuilt_default = <#type_name>::try_new(default_inner).expect(#err_msg); 56 | assert_eq!( 57 | default_inner, 58 | rebuilt_default.into_inner(), 59 | "Default and rebuilt default are different, maybe you didn't sanitize your default?" 60 | ); 61 | } 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/derive.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::format; 4 | use core::iter; 5 | /* Crate imports */ 6 | use super::Validator; 7 | use crate::{codegen, utils::syn_ext::SynPathExt as _}; 8 | /* Dependencies */ 9 | use syn::{ 10 | parse::{Parse, ParseStream}, 11 | parse_quote, 12 | }; 13 | 14 | pub(crate) enum Derive { 15 | Eq(syn::Path), 16 | Ord(syn::Path), 17 | Deserialize(syn::Path), 18 | /// A derive that will be passed down transparently. 19 | Transparent(syn::Path), 20 | } 21 | 22 | impl Parse for Derive { 23 | fn parse(input: ParseStream) -> syn::Result { 24 | let trait_path = syn::Path::parse(input)?; 25 | match trait_path.item_name()?.as_str() 26 | { 27 | // Special cases 28 | "Eq" => Ok(Self::Eq(trait_path)), 29 | "Ord" => Ok(Self::Ord(trait_path)), 30 | "Deserialize" => Ok(Self::Deserialize(trait_path)), 31 | // Forbidden derives 32 | derive_more @ ( 33 | "Add" | "AddAssign" | "Constructor" | "Div" | "DivAssign" | "From" | "FromStr" | "Mul" | "MulAssign" | "Neg" | "Rem" | "RemAssign" | "Shl" | "ShlAssign" | "Shr" | "ShrAssign" | "Sub" | "SubAssign" | "Sum" 34 | ) => Err(syn::Error::new_spanned( 35 | trait_path, 36 | format!("Deriving `{derive_more}` results in a possible bypass of the validators and sanitizers and is therefore forbidden.") 37 | )), 38 | "Default" => Err(syn::Error::new_spanned( 39 | trait_path, 40 | "To derive the `Default` trait, use the `default` or `default = ..` argument." 41 | )), 42 | _ => Ok(Self::Transparent(trait_path)), 43 | } 44 | } 45 | } 46 | 47 | impl codegen::Gen for Derive { 48 | fn gen_impl( 49 | &self, 50 | ctx: &crate::Context, 51 | ) -> impl Iterator { 52 | let is_nan_excluded_for_floats = ctx 53 | .args() 54 | .validators 55 | .iter() 56 | .any(Validator::excludes_float_nan); 57 | 58 | iter::once(match *self { 59 | Self::Eq(ref path) => { 60 | if is_nan_excluded_for_floats { 61 | let type_name = ctx.type_name(); 62 | let (impl_generics, ty_generics, where_clause) = 63 | ctx.generics().split_for_impl(); 64 | codegen::Implementation::ItemImpl(parse_quote! { 65 | impl #impl_generics #path for #type_name #ty_generics #where_clause {} 66 | }) 67 | } else { 68 | codegen::Implementation::Attribute( 69 | parse_quote! { #[derive(#path)] }, 70 | ) 71 | } 72 | }, 73 | Self::Ord(ref path) => { 74 | if is_nan_excluded_for_floats { 75 | let type_name = ctx.type_name(); 76 | let (impl_generics, ty_generics, where_clause) = 77 | ctx.generics().split_for_impl(); 78 | let panic_msg = format!("{type_name}::cmp() panicked, because partial_cmp() returned None. Could it be that you're using unsafe {type_name}::new_unchecked() ?"); 79 | codegen::Implementation::ItemImpl(parse_quote! { 80 | #[expect(clippy::derive_ord_xor_partial_ord, reason = "Manual impl when involving floats.")] 81 | impl #impl_generics #path for #type_name #ty_generics #where_clause { 82 | fn cmp(&self, other: &Self) -> ::core::cmp::Ordering { 83 | self.partial_cmp(other) 84 | .unwrap_or_else(|| panic!(#panic_msg)) 85 | } 86 | } 87 | }) 88 | } else { 89 | codegen::Implementation::Attribute( 90 | parse_quote! { #[derive(#path)] }, 91 | ) 92 | } 93 | }, 94 | Self::Deserialize(ref path) => { 95 | codegen::Implementation::Attribute(parse_quote! { 96 | #[derive(#path)] 97 | #[serde(try_from = "::Inner")] 98 | }) 99 | }, 100 | Self::Transparent(ref path) => codegen::Implementation::Attribute( 101 | parse_quote! { #[derive(#path)] }, 102 | ), 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/new_unchecked.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | use core::iter; 3 | /* Crate imports */ 4 | use crate::codegen; 5 | /* Dependencies */ 6 | use syn::parse_quote; 7 | 8 | #[derive(Debug)] 9 | pub(crate) struct NewUnchecked; 10 | 11 | impl codegen::Gen for NewUnchecked { 12 | fn gen_impl( 13 | &self, 14 | _: &crate::Context, 15 | ) -> impl Iterator { 16 | iter::once(codegen::Implementation::ImplItem(codegen::ImplItem::Fn( 17 | parse_quote! { 18 | #[inline] 19 | #[must_use] 20 | pub const unsafe fn new_unchecked(inner: ::Inner) -> Self { 21 | Self(inner) 22 | } 23 | }, 24 | ))) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/nnn_derive.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::vec; 4 | /* Crate imports */ 5 | use crate::{codegen, utils::syn_ext::SynPathExt as _}; 6 | /* Dependencies */ 7 | use quote::format_ident; 8 | use syn::{ 9 | parse::{Parse, ParseStream}, 10 | parse_quote, 11 | }; 12 | 13 | /// Derives provided by the crate. 14 | /// Most of them are also available via crates like `derive_more`. 15 | /// Providing them so users aren't required to install other crates for trivial derives. 16 | pub(crate) enum NNNDerive { 17 | Into(Option), 18 | From(Option), 19 | TryFrom(Option), 20 | Borrow(Option), 21 | FromStr, 22 | IntoIterator, 23 | AsRef(Option), 24 | Deref(Option), 25 | } 26 | 27 | impl NNNDerive { 28 | fn default_target( 29 | &self, 30 | ctx: &crate::Context, 31 | ) -> syn::AngleBracketedGenericArguments { 32 | let type_name = ctx.type_name(); 33 | match *self { 34 | Self::Into(_) 35 | | Self::TryFrom(_) 36 | | Self::Borrow(_) 37 | | Self::AsRef(_) 38 | | Self::Deref(_) => { 39 | parse_quote! { <::Inner> } 40 | }, 41 | Self::From(_) => { 42 | parse_quote! { <<#type_name as nnn::NNNewType>::Inner> } 43 | }, 44 | _ => unreachable!(), 45 | } 46 | } 47 | } 48 | 49 | impl Parse for NNNDerive { 50 | fn parse(input: ParseStream) -> syn::Result { 51 | let trait_path = syn::Path::parse(input)?; 52 | match trait_path.item_name()?.as_str() { 53 | "Into" => { 54 | let targets = extract_generics_targets(&trait_path)?; 55 | Ok(Self::Into(targets)) 56 | }, 57 | "From" => { 58 | let targets = extract_generics_targets(&trait_path)?; 59 | Ok(Self::From(targets)) 60 | }, 61 | "TryFrom" => { 62 | let targets = extract_generics_targets(&trait_path)?; 63 | Ok(Self::TryFrom(targets)) 64 | }, 65 | "Borrow" => { 66 | let targets = extract_generics_targets(&trait_path)?; 67 | Ok(Self::Borrow(targets)) 68 | }, 69 | "FromStr" => { 70 | assert_no_generics_params(&trait_path)?; 71 | Ok(Self::FromStr) 72 | }, 73 | "IntoIterator" => { 74 | assert_no_generics_params(&trait_path)?; 75 | Ok(Self::IntoIterator) 76 | }, 77 | "AsRef" => { 78 | let targets = extract_generics_targets(&trait_path)?; 79 | Ok(Self::AsRef(targets)) 80 | }, 81 | "Deref" => { 82 | let targets = extract_generics_targets(&trait_path)?; 83 | Ok(Self::Deref(targets)) 84 | }, 85 | _ => Err(syn::Error::new_spanned( 86 | trait_path, 87 | "Unknown `nnn_derive`.", 88 | )), 89 | } 90 | } 91 | } 92 | 93 | impl codegen::Gen for NNNDerive { 94 | fn gen_impl( 95 | &self, 96 | ctx: &crate::Context, 97 | ) -> impl Iterator { 98 | let type_name = ctx.type_name(); 99 | let (impl_generics, ty_generics, where_clause) = 100 | ctx.generics().split_for_impl(); 101 | 102 | let impls = match *self { 103 | Self::Into(ref targets) => { 104 | targets 105 | .clone() 106 | .unwrap_or(self.default_target(ctx)) 107 | .args 108 | .into_iter() 109 | // use `_` as if it were `` 110 | .map(|arg| { 111 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arg { 112 | self.default_target(ctx).args[0].clone() 113 | } else{ arg } 114 | }) 115 | .map(|target| { 116 | codegen::Implementation::ItemImpl(parse_quote! { 117 | impl #impl_generics ::core::convert::Into<#target> for #type_name #ty_generics #where_clause { 118 | fn into(self) -> #target { 119 | self.0.into() 120 | } 121 | } 122 | }) 123 | }) 124 | .collect() 125 | }, 126 | Self::From(ref targets) => { 127 | targets 128 | .clone() 129 | .unwrap_or(self.default_target(ctx)) 130 | .args 131 | .into_iter() 132 | // use `_` as if it were `` 133 | .map(|arg| { 134 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arg { 135 | self.default_target(ctx).args[0].clone() 136 | } else{ arg } 137 | }) 138 | .map(|target| { 139 | codegen::Implementation::ItemImpl(parse_quote! { 140 | impl #impl_generics ::core::convert::From<#type_name #ty_generics> for #target #where_clause { 141 | fn from(value: #type_name #ty_generics) -> #target { 142 | value.0.into() 143 | } 144 | } 145 | }) 146 | }) 147 | .collect() 148 | }, 149 | Self::Borrow(ref targets) => { 150 | targets 151 | .clone() 152 | .unwrap_or(self.default_target(ctx)) 153 | .args 154 | .into_iter() 155 | // use `_` as if it were `` 156 | .map(|arg| { 157 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arg { 158 | self.default_target(ctx).args[0].clone() 159 | } else{ arg } 160 | }) 161 | .map(|target| { 162 | codegen::Implementation::ItemImpl(parse_quote! { 163 | impl #impl_generics ::core::borrow::Borrow<#target> for #type_name #ty_generics #where_clause { 164 | fn borrow(&self) -> &#target { 165 | &self.0 166 | } 167 | } 168 | }) 169 | }) 170 | .collect() 171 | }, 172 | Self::TryFrom(ref targets) => { 173 | targets 174 | .clone() 175 | .unwrap_or(self.default_target(ctx)) 176 | .args 177 | .into_iter() 178 | // use `_` as if it were `` 179 | .map(|arg| { 180 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arg { 181 | self.default_target(ctx).args[0].clone() 182 | } else{ arg } 183 | }) 184 | .map(|target| { 185 | codegen::Implementation::ItemImpl(parse_quote! { 186 | impl #impl_generics ::core::convert::TryFrom<#target> for #type_name #ty_generics #where_clause { 187 | type Error = ::Error; 188 | fn try_from(value: #target) -> Result { 189 | ::try_new(value.into()) 190 | } 191 | } 192 | }) 193 | }) 194 | .collect() 195 | }, 196 | Self::FromStr => { 197 | let parse_err_name = format_ident!("{type_name}ParseError"); 198 | vec![ 199 | codegen::Implementation::Enum(parse_quote! { 200 | #[derive(Debug, Clone, PartialEq, Eq)] 201 | #[non_exhaustive] 202 | pub enum #impl_generics #parse_err_name #where_clause { 203 | InnerParse(<<#type_name as nnn::NNNewType>::Inner as ::core::str::FromStr>::Err), 204 | Validation(<#type_name as nnn::NNNewType>::Error), 205 | } 206 | }), 207 | codegen::Implementation::ItemImpl(parse_quote! { 208 | impl #impl_generics ::core::fmt::Display for #parse_err_name #ty_generics #where_clause { 209 | fn fmt(&self, fmt: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { 210 | match *self { 211 | #parse_err_name::InnerParse(ref err) => { 212 | write!(fmt, "Failed to parse {}'s inner: {err:?}.", stringify!(#type_name)) 213 | }, 214 | #parse_err_name::Validation(ref err) => { 215 | write!(fmt, "Failed to validate parsed: {err}.") 216 | }, 217 | } 218 | } 219 | } 220 | }), 221 | codegen::Implementation::ItemImpl(parse_quote! { 222 | impl #impl_generics ::core::str::FromStr for #type_name #ty_generics #where_clause { 223 | type Err = #parse_err_name; 224 | 225 | fn from_str(input: &str) -> ::core::result::Result { 226 | ::try_new( 227 | input.parse().map_err(#parse_err_name::InnerParse)? 228 | ).map_err(#parse_err_name::Validation) 229 | } 230 | } 231 | }), 232 | ] 233 | }, 234 | Self::IntoIterator => { 235 | let lifetime: syn::GenericParam = parse_quote! { '__iter_ref }; 236 | let generics_with_lifetime = { 237 | let mut generics = ctx.generics().clone(); 238 | generics.params.push(lifetime.clone()); 239 | generics 240 | }; 241 | 242 | vec![ 243 | codegen::Implementation::ItemImpl(parse_quote! { 244 | impl #impl_generics ::core::iter::IntoIterator for #type_name #ty_generics #where_clause { 245 | type Item = <::Inner as ::core::iter::IntoIterator>::Item; 246 | type IntoIter = <::Inner as ::core::iter::IntoIterator>::IntoIter; 247 | 248 | fn into_iter(self) -> Self::IntoIter { 249 | self.0.into_iter() 250 | } 251 | } 252 | }), 253 | codegen::Implementation::ItemImpl(parse_quote! { 254 | impl #generics_with_lifetime ::core::iter::IntoIterator for &#lifetime #type_name #ty_generics #where_clause { 255 | type Item = <&#lifetime <#type_name #ty_generics as nnn::NNNewType>::Inner as ::core::iter::IntoIterator>::Item; 256 | type IntoIter = <&#lifetime <#type_name #ty_generics as nnn::NNNewType>::Inner as ::core::iter::IntoIterator>::IntoIter; 257 | 258 | fn into_iter(self) -> Self::IntoIter { 259 | self.0.iter() 260 | } 261 | } 262 | }), 263 | ] 264 | }, 265 | Self::AsRef(ref targets) => { 266 | targets.clone().unwrap_or(self.default_target(ctx)) 267 | .args 268 | .into_iter() 269 | // use `_` as if it were `` 270 | .map(|arg| { 271 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arg { 272 | self.default_target(ctx).args[0].clone() 273 | } else{ arg } 274 | }) 275 | .map(|target| { 276 | codegen::Implementation::ItemImpl(parse_quote! { 277 | impl #impl_generics ::core::convert::AsRef<#target> for #type_name #ty_generics #where_clause { 278 | fn as_ref(&self) -> &#target { 279 | &self.0 280 | } 281 | } 282 | }) 283 | }) 284 | .collect() 285 | }, 286 | Self::Deref(ref targets) => { 287 | targets 288 | .clone() 289 | .unwrap_or(self.default_target(ctx)) 290 | .args 291 | .into_iter() 292 | // use `_` as if it were `` 293 | .map(|arg| { 294 | if let syn::GenericArgument::Type(syn::Type::Infer(_)) = arg { 295 | self.default_target(ctx).args[0].clone() 296 | } else{ arg } 297 | }) 298 | .map(|target| { 299 | codegen::Implementation::ItemImpl(parse_quote! { 300 | impl #impl_generics ::core::ops::Deref for #type_name #ty_generics #where_clause { 301 | type Target = #target; 302 | fn deref(&self) -> &Self::Target { 303 | &self.0 304 | } 305 | } 306 | }) 307 | }) 308 | .collect() 309 | }, 310 | }; 311 | 312 | impls.into_iter() 313 | } 314 | } 315 | 316 | fn extract_generics_targets( 317 | trait_path: &syn::Path, 318 | ) -> syn::Result> { 319 | match trait_path.trait_segment().cloned()?.arguments { 320 | // If no arguments were given to the trait, e.g., "Into" instead of "Into", 321 | // we insert the new-type's inner type as the target. 322 | syn::PathArguments::None => Ok(None), 323 | syn::PathArguments::AngleBracketed(args) if args.args.is_empty() => Err(syn::Error::new_spanned( 324 | args, 325 | "Please provide generics arguments, or omit the '<>' for the default derive.", 326 | )), 327 | syn::PathArguments::AngleBracketed(args) => Ok(Some(args)), 328 | syn::PathArguments::Parenthesized(args) => Err(syn::Error::new_spanned( 329 | args, 330 | "Trait isn't allowed to take parenthesized generics arguments.", 331 | )), 332 | } 333 | } 334 | 335 | fn assert_no_generics_params(trait_path: &syn::Path) -> syn::Result<()> { 336 | let args = trait_path.trait_segment().cloned()?.arguments; 337 | match args { 338 | syn::PathArguments::None => Ok(()), 339 | _ => Err(syn::Error::new_spanned( 340 | args, 341 | "Trait isn't allowed to take generics arguments.", 342 | )), 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/sanitizer.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::string::ToString as _; 4 | use core::iter; 5 | /* Crate imports */ 6 | use crate::{ 7 | codegen, 8 | utils::{closure::CustomFunction, syn_ext::SynParseBufferExt as _}, 9 | }; 10 | /* Dependencies */ 11 | use syn::{ 12 | parse::{Parse, ParseStream}, 13 | parse_quote, 14 | punctuated::Punctuated, 15 | token::Comma, 16 | }; 17 | 18 | pub(crate) enum Sanitizer { 19 | // Containers 20 | Each(Punctuated), 21 | Sort, 22 | Dedup, 23 | // Strings 24 | Trim, 25 | Lowercase, 26 | Uppercase, 27 | // Commons 28 | Custom(CustomFunction), 29 | } 30 | 31 | impl Sanitizer { 32 | fn step(&self) -> syn::Block { 33 | match *self { 34 | // Containers 35 | Self::Each(ref steps) => { 36 | let inner_steps = steps.iter().map(Self::step); 37 | parse_quote! {{ 38 | value = value.into_iter().map(|mut value| { 39 | #(#inner_steps;)* 40 | value 41 | }).collect(); 42 | }} 43 | }, 44 | Self::Sort => parse_quote! {{ value.sort(); }}, 45 | Self::Dedup => parse_quote! {{ value.dedup(); }}, 46 | // Strings 47 | Self::Trim => parse_quote! {{ value = value.trim().into(); }}, 48 | Self::Lowercase => parse_quote! {{ value = value.to_lowercase(); }}, 49 | Self::Uppercase => parse_quote! {{ value = value.to_uppercase(); }}, 50 | // Common 51 | Self::Custom(ref custom) => match *custom { 52 | CustomFunction::Block(ref block) => parse_quote! { #block }, 53 | CustomFunction::Path(ref path) => { 54 | parse_quote! {{ value = #path(value); }} 55 | }, 56 | CustomFunction::Closure(ref expr_closure) => { 57 | parse_quote! {{ value = (#expr_closure)(value); }} 58 | }, 59 | }, 60 | } 61 | } 62 | } 63 | 64 | impl codegen::Gen for Sanitizer { 65 | fn gen_impl( 66 | &self, 67 | _: &crate::Context, 68 | ) -> impl Iterator { 69 | iter::once(codegen::Implementation::SanitizationStep(self.step())) 70 | } 71 | } 72 | 73 | impl Parse for Sanitizer { 74 | fn parse(input: ParseStream) -> syn::Result { 75 | let name = input.parse::()?; 76 | let validator = match name.to_string().as_str() { 77 | // Containers 78 | "each" => Self::Each(input.parse_parenthesized::()?), 79 | "sort" => Self::Sort, 80 | "dedup" => Self::Dedup, 81 | // Strings 82 | "trim" => Self::Trim, 83 | "lowercase" => Self::Lowercase, 84 | "uppercase" => Self::Uppercase, 85 | // Common 86 | "custom" => Self::Custom(input.parse_equal()?), 87 | _ => { 88 | return Err(syn::Error::new_spanned(name, "Unknown sanitizer.")) 89 | }, 90 | }; 91 | 92 | if !input.peek(syn::Token![,]) && !input.is_empty() { 93 | return Err(input.error("Unexpected token(s).")); 94 | } 95 | 96 | Ok(validator) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /nnn-macros/src/argument/validator.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::{format, string::ToString as _, vec, vec::Vec}; 4 | /* Crate imports */ 5 | use crate::{ 6 | codegen, 7 | utils::{ 8 | closure::CustomFunction, 9 | regex_input::RegexInput, 10 | syn_ext::{SynParseBufferExt as _, SynPathExt as _}, 11 | }, 12 | }; 13 | /* Dependencies */ 14 | use quote::{format_ident, ToTokens as _}; 15 | use syn::{ 16 | parse::{Parse, ParseStream}, 17 | parse_quote, 18 | punctuated::Punctuated, 19 | token::Comma, 20 | }; 21 | 22 | pub(crate) enum Validator { 23 | // Containers 24 | NotEmpty, 25 | Each(Punctuated), 26 | MinLength(syn::Expr), 27 | MinLengthOrEq(syn::Expr), 28 | Length(syn::Expr), 29 | MaxLength(syn::Expr), 30 | MaxLengthOrEq(syn::Expr), 31 | // Numerics 32 | Min(syn::Expr), 33 | MinOrEq(syn::Expr), 34 | Max(syn::Expr), 35 | MaxOrEq(syn::Expr), 36 | Positive, 37 | Negative, 38 | // Float specifics 39 | Finite, 40 | NotInfinite, 41 | NotNAN, 42 | // String specifics 43 | Regex(RegexInput), 44 | // Commons 45 | Exactly(syn::Expr), 46 | Custom { 47 | check: CustomFunction, 48 | error: syn::Path, 49 | /// derived from error path 50 | error_name: syn::Ident, 51 | }, 52 | Predicate { 53 | check: CustomFunction, 54 | // if optional and defaults to `Predicate`. 55 | variant: syn::Ident, 56 | }, 57 | } 58 | 59 | impl codegen::Gen for Validator { 60 | fn gen_impl( 61 | &self, 62 | ctx: &crate::Context, 63 | ) -> impl Iterator { 64 | [ 65 | codegen::Implementation::ErrorVariant(self.variant()), 66 | codegen::Implementation::ErrorDisplayArm(self.display_arm(ctx)), 67 | codegen::Implementation::ValidityCheck(self.check(ctx)), 68 | ] 69 | .into_iter() 70 | } 71 | 72 | fn gen_tests( 73 | &self, 74 | _: &crate::Context, 75 | ) -> impl Iterator { 76 | if let Self::Regex(ref regex) = *self { 77 | let check: syn::Stmt = match *regex { 78 | RegexInput::Path(ref path) => { 79 | parse_quote! { ::regex::Regex::new(#path.as_str()).unwrap(); } 80 | }, 81 | RegexInput::StringLiteral(ref lit) => { 82 | parse_quote! { ::regex::Regex::new(&#lit).unwrap(); } 83 | }, 84 | }; 85 | 86 | vec![parse_quote! { 87 | #[test] 88 | fn regex_validator_should_be_valid() { 89 | #check 90 | } 91 | }] 92 | } else { 93 | vec![] 94 | }.into_iter() 95 | } 96 | } 97 | 98 | #[expect(clippy::too_many_lines, reason = "Lots of validators.")] 99 | impl Validator { 100 | pub(crate) fn excludes_float_nan(&self) -> bool { 101 | #[expect(clippy::wildcard_enum_match_arm, reason = "_")] 102 | // Note: Self::Cfg will always be false. See the readme. 103 | match *self { 104 | Self::Each(ref steps) => steps.iter().any(Self::excludes_float_nan), 105 | Self::Finite | Self::NotNAN => true, 106 | _ => false, 107 | } 108 | } 109 | 110 | pub(crate) fn variant(&self) -> Punctuated { 111 | match *self { 112 | // Containers 113 | Self::NotEmpty => parse_quote! { NotEmpty }, 114 | Self::Each(ref steps) => { 115 | let steps_variants = steps.iter().map(Self::variant); 116 | parse_quote! { 117 | Each(usize, Box), 118 | #(#steps_variants),* 119 | } 120 | }, 121 | Self::MinLength(_) => parse_quote! { MinLength }, 122 | Self::MinLengthOrEq(_) => parse_quote! { MinLengthOrEq }, 123 | Self::Length(_) => parse_quote! { Length }, 124 | Self::MaxLength(_) => parse_quote! { MaxLength }, 125 | Self::MaxLengthOrEq(_) => parse_quote! { MaxLengthOrEq }, 126 | // Numerics 127 | Self::Min(_) => parse_quote! { Min }, 128 | Self::MinOrEq(_) => parse_quote! { MinOrEq }, 129 | Self::Max(_) => parse_quote! { Max }, 130 | Self::MaxOrEq(_) => parse_quote! { MaxOrEq }, 131 | Self::Positive => parse_quote! { Positive }, 132 | Self::Negative => parse_quote! { Negative }, 133 | // Float specifics 134 | Self::Finite => parse_quote! { Finite }, 135 | Self::NotInfinite => parse_quote! { NotInfinite }, 136 | Self::NotNAN => parse_quote! { NotNAN }, 137 | // String specifics 138 | Self::Regex(_) => parse_quote! { Regex }, 139 | // Commons 140 | Self::Exactly(_) => parse_quote! { Exactly }, 141 | Self::Custom { 142 | ref error, 143 | ref error_name, 144 | .. 145 | } => { 146 | parse_quote! { #error_name(#error) } 147 | }, 148 | Self::Predicate { ref variant, .. } => { 149 | parse_quote! { #variant } 150 | }, 151 | } 152 | } 153 | 154 | pub(crate) fn check(&self, _ctx: &crate::Context) -> syn::Block { 155 | match *self { 156 | // Containers 157 | Self::NotEmpty => { 158 | parse_quote! {{ if value.is_empty() { return Err(::Error::NotEmpty) } }} 159 | }, 160 | Self::Each(ref checks) => { 161 | let inner_branches = checks.iter().map(|val| val.check(_ctx)); 162 | parse_quote! {{ 163 | // TODO: is .cloned() really the solution ? 164 | value.iter().cloned().enumerate().try_for_each( 165 | |(idx, value)| { 166 | // Used to avoid short circuits from `return` statements in branches 167 | let check = || { 168 | #(#inner_branches;)* 169 | Ok(()) 170 | }; 171 | check().map_err(|err| ::Error::Each(idx, Box::new(err))) 172 | } 173 | )? 174 | }} 175 | }, 176 | Self::MinLength(ref val) => { 177 | parse_quote! {{ if !(value.len() > #val) { return Err(::Error::MinLength) } }} 178 | }, 179 | Self::MinLengthOrEq(ref val) => { 180 | parse_quote! {{ if !(value.len() >= #val) { return Err(::Error::MinLengthOrEq) } }} 181 | }, 182 | Self::Length(ref val) => { 183 | parse_quote! {{ if value.len() != #val { return Err(::Error::Length) } }} 184 | }, 185 | Self::MaxLength(ref val) => { 186 | parse_quote! {{ if !(value.len() < #val) { return Err(::Error::MaxLength) } }} 187 | }, 188 | Self::MaxLengthOrEq(ref val) => { 189 | parse_quote! {{ if !(value.len() <= #val) { return Err(::Error::MaxLengthOrEq) } }} 190 | }, 191 | // Numerics 192 | Self::Min(ref val) => { 193 | parse_quote! {{ if !(value > #val) { return Err(::Error::Min) } }} 194 | }, 195 | Self::MinOrEq(ref val) => { 196 | parse_quote! {{ if !(value >= #val) { return Err(::Error::MinOrEq) } }} 197 | }, 198 | Self::Max(ref val) => { 199 | parse_quote! {{ if !(value < #val) { return Err(::Error::Max) } }} 200 | }, 201 | Self::MaxOrEq(ref val) => { 202 | parse_quote! {{ if !(value <= #val) { return Err(::Error::MaxOrEq) } }} 203 | }, 204 | Self::Positive => { 205 | parse_quote! {{ 206 | // Terrible hack since 0.into() doesn't work for floats 207 | if !(value > false.into()) { return Err(::Error::Positive) } 208 | }} 209 | }, 210 | Self::Negative => { 211 | parse_quote! {{ 212 | // Terrible hack since 0.into() doesn't work for floats 213 | if ! (value < false.into()) { return Err(::Error::Negative) } 214 | }} 215 | }, 216 | // Float specifics 217 | Self::Finite => { 218 | parse_quote! {{ if ! value.is_finite() { return Err(::Error::Finite) } }} 219 | }, 220 | Self::NotInfinite => { 221 | parse_quote! {{ if value.is_infinite() { return Err(::Error::NotInfinite) } }} 222 | }, 223 | Self::NotNAN => { 224 | parse_quote! {{ if value.is_nan() { return Err(::Error::NotNAN) } }} 225 | }, 226 | // String specifics 227 | Self::Regex(ref regex) => { 228 | let condition: syn::Block = match *regex { 229 | RegexInput::Path(ref path) => { 230 | parse_quote! {{ ! #path.is_match(&value) }} 231 | }, 232 | RegexInput::StringLiteral(ref lit) => { 233 | let err = format!("Invalid Regex`{}`.", lit.value()); 234 | parse_quote! {{ 235 | static REGEX_TO_MATCH: ::std::sync::LazyLock<::regex::Regex> = ::std::sync::LazyLock::new(|| ::regex::Regex::new(&#lit).expect(#err)); 236 | ! REGEX_TO_MATCH.is_match(&value) 237 | }} 238 | }, 239 | }; 240 | parse_quote! {{ 241 | if #condition { return Err(::Error::Regex) } 242 | }} 243 | }, 244 | // Commons 245 | Self::Exactly(ref val) => { 246 | parse_quote! {{ 247 | #[allow(clippy::float_cmp, reason = "Allows transparency between signed numbers and floats.")] 248 | if value != #val { return Err(::Error::Exactly) } 249 | }} 250 | }, 251 | Self::Custom { 252 | ref check, 253 | ref error_name, 254 | .. 255 | } => match *check { 256 | CustomFunction::Block(ref block) => parse_quote! {{ 257 | if let Err(err) = #block { return Err(::Error::#error_name(err)) } 258 | }}, 259 | CustomFunction::Path(ref func) => parse_quote! {{ 260 | if let Err(err) = #func(&value) { return Err(::Error::#error_name(err)) } 261 | }}, 262 | CustomFunction::Closure(ref expr_closure) => parse_quote! {{ 263 | if let Err(err) = (#expr_closure)(&value) { return Err(::Error::#error_name(err)) } 264 | }}, 265 | }, 266 | Self::Predicate { 267 | ref check, 268 | ref variant, 269 | .. 270 | } => match *check { 271 | CustomFunction::Block(ref block) => parse_quote! {{ 272 | if !#block { return Err(::Error::#variant) } 273 | }}, 274 | CustomFunction::Path(ref func) => parse_quote! {{ 275 | if !#func(&value) { return Err(::Error::#variant) } 276 | }}, 277 | CustomFunction::Closure(ref expr_closure) => { 278 | parse_quote! {{ 279 | if !(#expr_closure)(&value) { return Err(::Error::#variant) } 280 | }} 281 | }, 282 | }, 283 | } 284 | } 285 | 286 | pub(crate) fn display_arm(&self, ctx: &crate::Context) -> Vec { 287 | let type_name = ctx.type_name(); 288 | match *self { 289 | // Containers 290 | Self::NotEmpty => { 291 | let msg = format!("[{type_name}] Value should not empty."); 292 | parse_quote! { Self::NotEmpty => write!(fmt, #msg), } 293 | }, 294 | Self::Each(ref steps) => { 295 | let steps_fmt = 296 | steps.iter().flat_map(|step| step.display_arm(ctx)); 297 | parse_quote! { 298 | Self::Each(ref idx, ref inner_err) => write!(fmt, "[{}] Error: '{inner_err}', at index {idx}.", stringify!(#type_name)), 299 | #(#steps_fmt)* 300 | } 301 | }, 302 | Self::MinLength(ref val) => { 303 | let msg = format!( 304 | "[{type_name}] Length should be greater than {}.", 305 | val.to_token_stream() 306 | ); 307 | parse_quote! { Self::MinLength => write!(fmt, #msg), } 308 | }, 309 | Self::MinLengthOrEq(ref val) => { 310 | let msg = format!( 311 | "[{type_name}] Length should be greater or equal to {}.", 312 | val.to_token_stream() 313 | ); 314 | parse_quote! { Self::MinLengthOrEq => write!(fmt, #msg), } 315 | }, 316 | Self::Length(ref val) => { 317 | let msg = format!( 318 | "[{type_name}] Length should be exactly {}.", 319 | val.to_token_stream() 320 | ); 321 | parse_quote! { Self::Length => write!(fmt, #msg), } 322 | }, 323 | Self::MaxLength(ref val) => { 324 | let msg = format!( 325 | "[{type_name}] Length should be lesser than {}.", 326 | val.to_token_stream() 327 | ); 328 | parse_quote! { Self::MaxLength => write!(fmt, #msg), } 329 | }, 330 | Self::MaxLengthOrEq(ref val) => { 331 | let msg = format!( 332 | "[{type_name}] Length should be lesser or equal to {}.", 333 | val.to_token_stream() 334 | ); 335 | parse_quote! { Self::MaxLengthOrEq => write!(fmt, #msg), } 336 | }, 337 | // Numerics 338 | Self::Min(ref val) => { 339 | let msg = format!( 340 | "[{type_name}] Value should be greater than {}.", 341 | val.to_token_stream() 342 | ); 343 | parse_quote! { Self::Min => write!(fmt, #msg), } 344 | }, 345 | Self::MinOrEq(ref val) => { 346 | let msg = format!( 347 | "[{type_name}] Value should be greater or equal to {}.", 348 | val.to_token_stream() 349 | ); 350 | parse_quote! { Self::MinOrEq => write!(fmt, #msg), } 351 | }, 352 | Self::Max(ref val) => { 353 | let msg = format!( 354 | "[{type_name}] Value should be lesser than {}.", 355 | val.to_token_stream() 356 | ); 357 | parse_quote! { Self::Max => write!(fmt, #msg), } 358 | }, 359 | Self::MaxOrEq(ref val) => { 360 | let msg = format!( 361 | "[{type_name}] Value should be lesser or equal to {}.", 362 | val.to_token_stream() 363 | ); 364 | parse_quote! { Self::MaxOrEq => write!(fmt, #msg), } 365 | }, 366 | Self::Positive => { 367 | let msg = format!("[{type_name}] Value should be positive."); 368 | parse_quote! { Self::Positive => write!(fmt, #msg), } 369 | }, 370 | Self::Negative => { 371 | let msg = format!("[{type_name}] Value should be negative."); 372 | parse_quote! { Self::Negative => write!(fmt, #msg), } 373 | }, 374 | // Float specifics 375 | Self::Finite => { 376 | let msg = format!( 377 | "[{type_name}] Value should not be NAN nor infinite." 378 | ); 379 | parse_quote! { Self::Finite => write!(fmt, #msg), } 380 | }, 381 | Self::NotInfinite => { 382 | let msg = 383 | format!("[{type_name}] Value should not be infinite."); 384 | parse_quote! { Self::NotInfinite => write!(fmt, #msg), } 385 | }, 386 | Self::NotNAN => { 387 | let msg = format!("[{type_name}] Value should not be NAN."); 388 | parse_quote! { Self::NotNAN => write!(fmt, #msg), } 389 | }, 390 | // String specifics 391 | Self::Regex(ref regex) => { 392 | let regex_expression_display = match *regex { 393 | RegexInput::StringLiteral(ref lit) => { 394 | quote::quote! { #lit } 395 | }, 396 | RegexInput::Path(ref path) => { 397 | quote::quote! { #path.as_str() } 398 | }, 399 | }; 400 | parse_quote! { 401 | Self::Regex => write!(fmt, "[{}] Value should match `{}`.", stringify!(#type_name), #regex_expression_display), 402 | } 403 | }, 404 | // Commons 405 | Self::Exactly(ref val) => { 406 | let msg = format!( 407 | "[{type_name}] Value should be exactly {}.", 408 | val.to_token_stream() 409 | ); 410 | parse_quote! { Self::Exactly => write!(fmt, #msg), } 411 | }, 412 | Self::Custom { ref error_name, .. } => { 413 | let msg = 414 | format!("[{type_name}] Value should be exactly {{}}."); 415 | parse_quote! { Self::#error_name(ref inner_err) => write!(fmt, #msg, inner_err), } 416 | }, 417 | Self::Predicate { ref variant, .. } => { 418 | let msg = format!("[{type_name}] {variant} violated."); 419 | parse_quote! { Self::#variant => write!(fmt, #msg), } 420 | }, 421 | } 422 | } 423 | } 424 | 425 | impl Parse for Validator { 426 | fn parse(input: ParseStream) -> syn::Result { 427 | let name = input.parse::()?; 428 | let validator = match name.to_string().as_str() { 429 | // Containers 430 | "not_empty" => Self::NotEmpty, 431 | "each" => Self::Each(input.parse_parenthesized::()?), 432 | "min_length" => Self::MinLength(input.parse_equal()?), 433 | "min_length_or_eq" => Self::MinLengthOrEq(input.parse_equal()?), 434 | "length" => Self::Length(input.parse_equal()?), 435 | "max_length" => Self::MaxLength(input.parse_equal()?), 436 | "max_length_or_eq" => Self::MaxLengthOrEq(input.parse_equal()?), 437 | // Numerics 438 | "min" => Self::Min(input.parse_equal()?), 439 | "min_or_eq" => Self::MinOrEq(input.parse_equal()?), 440 | "exactly" => Self::Exactly(input.parse_equal()?), 441 | "max" => Self::Max(input.parse_equal()?), 442 | "max_or_eq" => Self::MaxOrEq(input.parse_equal()?), 443 | "positive" => Self::Positive, 444 | "negative" => Self::Negative, 445 | // Float specifics 446 | "finite" => Self::Finite, 447 | "not_infinite" => Self::NotInfinite, 448 | "not_nan" => Self::NotNAN, 449 | // String specifics 450 | "regex" => Self::Regex(input.parse_equal()?), 451 | "custom" => { 452 | let content; 453 | syn::parenthesized!(content in input); 454 | 455 | content.require_ident("with")?; 456 | let check = content.parse_equal()?; 457 | 458 | content.parse::()?; 459 | 460 | content.require_ident("error")?; 461 | let error = content.parse_equal::()?; 462 | let error_name = error.as_ident(); 463 | 464 | Self::Custom { 465 | check, 466 | error, 467 | error_name, 468 | } 469 | }, 470 | "predicate" => { 471 | let content; 472 | syn::parenthesized!(content in input); 473 | 474 | content.require_ident("with")?; 475 | let check = content.parse_equal()?; 476 | 477 | let variant = if content.parse::().is_ok() { 478 | content.require_ident("error_name")?; 479 | content.parse_equal::()? 480 | } else { 481 | format_ident!("Predicate") 482 | }; 483 | 484 | Self::Predicate { check, variant } 485 | }, 486 | _ => { 487 | return Err(syn::Error::new_spanned(name, "Unknown validator.")) 488 | }, 489 | }; 490 | 491 | if !input.peek(syn::Token![,]) && !input.is_empty() { 492 | return Err(input.error("Unexpected token(s).")); 493 | } 494 | 495 | Ok(validator) 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /nnn-macros/src/codegen.rs: -------------------------------------------------------------------------------- 1 | /* Modules */ 2 | mod impl_item; 3 | mod test_fn; 4 | /* Built-in imports */ 5 | extern crate alloc; 6 | use alloc::vec::Vec; 7 | /* Dependencies */ 8 | use syn::{parse_quote, punctuated::Punctuated, token::Comma}; 9 | /* Re-exports */ 10 | pub(crate) use self::{impl_item::ImplItem, test_fn::TestFn}; 11 | 12 | pub(crate) trait Gen { 13 | fn gen_tests(&self, _: &crate::Context) -> impl Iterator { 14 | [].into_iter() 15 | } 16 | 17 | fn gen_impl( 18 | &self, 19 | ctx: &crate::Context, 20 | ) -> impl Iterator; 21 | } 22 | 23 | pub(crate) enum Implementation { 24 | /// an impl block 25 | ItemImpl(syn::ItemImpl), 26 | /// an item within an impl block 27 | ImplItem(ImplItem), 28 | /// A macro attribute for the generated [`crate::NNNType`] 29 | Attribute(Vec), 30 | 31 | ErrorVariant(Punctuated), 32 | ValidityCheck(syn::Block), 33 | ErrorDisplayArm(Vec), 34 | 35 | SanitizationStep(syn::Block), 36 | 37 | Enum(syn::ItemEnum), 38 | } 39 | 40 | impl Implementation { 41 | pub(crate) fn make_conditional(&mut self, condition: &syn::Expr) { 42 | let cfg_attr: syn::Attribute = parse_quote! { #[cfg(#condition)] }; 43 | 44 | match *self { 45 | Self::ItemImpl(ref mut item_impl) => item_impl.attrs.push(cfg_attr), 46 | Self::ImplItem(ref mut item_impl) => { 47 | item_impl.attrs_mut().push(cfg_attr); 48 | }, 49 | Self::Attribute(ref mut attrs) => { 50 | for attr in attrs.iter_mut() { 51 | let inner = &attr.meta; 52 | *attr = parse_quote! { #[cfg_attr(#condition, #inner)]}; 53 | } 54 | }, 55 | Self::ErrorVariant(ref mut punctuated) => { 56 | punctuated 57 | .iter_mut() 58 | .for_each(|variant| variant.attrs.push(cfg_attr.clone())); 59 | }, 60 | Self::ValidityCheck(ref mut block) 61 | | Self::SanitizationStep(ref mut block) => { 62 | *block = parse_quote! {{ 63 | #cfg_attr 64 | #block 65 | }}; 66 | }, 67 | Self::ErrorDisplayArm(ref mut arms) => { 68 | arms.iter_mut() 69 | .for_each(|arm| arm.attrs.push(cfg_attr.clone())); 70 | }, 71 | Self::Enum(ref mut item) => item.attrs.push(cfg_attr), 72 | } 73 | } 74 | 75 | #[expect(clippy::wildcard_enum_match_arm, reason = "Specific extractions.")] 76 | pub(crate) fn separate_variants( 77 | impls: &[Self], 78 | ) -> ( 79 | impl Iterator, 80 | impl Iterator, 81 | impl Iterator, 82 | impl Iterator, 83 | impl Iterator, 84 | impl Iterator, 85 | impl Iterator, 86 | impl Iterator, 87 | ) { 88 | let impl_blocks = impls.iter().filter_map(|item| match *item { 89 | Self::ItemImpl(ref el) => Some(el), 90 | _ => None, 91 | }); 92 | let impl_items = impls.iter().filter_map(|item| match *item { 93 | Self::ImplItem(ref el) => Some(el), 94 | _ => None, 95 | }); 96 | let proc_macro_attrs = impls 97 | .iter() 98 | .filter_map(|item| match *item { 99 | Self::Attribute(ref el) => Some(el), 100 | _ => None, 101 | }) 102 | .flatten(); 103 | 104 | let err_variants = impls 105 | .iter() 106 | .filter_map(|item| match *item { 107 | Self::ErrorVariant(ref el) => Some(el), 108 | _ => None, 109 | }) 110 | .flatten(); 111 | let validity_checks = impls.iter().filter_map(|item| match *item { 112 | Self::ValidityCheck(ref el) => Some(el), 113 | _ => None, 114 | }); 115 | let err_display_arm = impls 116 | .iter() 117 | .filter_map(|item| match *item { 118 | Self::ErrorDisplayArm(ref el) => Some(el), 119 | _ => None, 120 | }) 121 | .flatten(); 122 | 123 | let sanitization_steps = impls.iter().filter_map(|item| match *item { 124 | Self::SanitizationStep(ref el) => Some(el), 125 | _ => None, 126 | }); 127 | 128 | let enums = impls.iter().filter_map(|item| match *item { 129 | Self::Enum(ref el) => Some(el), 130 | _ => None, 131 | }); 132 | 133 | ( 134 | impl_blocks, 135 | impl_items, 136 | proc_macro_attrs, 137 | err_variants, 138 | validity_checks, 139 | err_display_arm, 140 | sanitization_steps, 141 | enums, 142 | ) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /nnn-macros/src/codegen/impl_item.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::vec::Vec; 4 | /* Dependencies */ 5 | use syn::Attribute; 6 | 7 | /// Basically [`syn::ImplItem`] without the `Verbatim` variant. 8 | pub(crate) enum ImplItem { 9 | Const(syn::ImplItemConst), 10 | Fn(syn::ImplItemFn), 11 | Macro(syn::ImplItemMacro), 12 | Type(syn::ImplItemType), 13 | } 14 | 15 | impl ImplItem { 16 | pub(crate) fn attrs_mut(&mut self) -> &mut Vec { 17 | match *self { 18 | Self::Const(ref mut impl_item_const) => &mut impl_item_const.attrs, 19 | Self::Fn(ref mut impl_item_fn) => &mut impl_item_fn.attrs, 20 | Self::Macro(ref mut impl_item_macro) => &mut impl_item_macro.attrs, 21 | Self::Type(ref mut impl_item_type) => &mut impl_item_type.attrs, 22 | } 23 | } 24 | } 25 | 26 | impl quote::ToTokens for ImplItem { 27 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 28 | match *self { 29 | Self::Const(ref r#const) => r#const.to_tokens(tokens), 30 | Self::Fn(ref r#fn) => r#fn.to_tokens(tokens), 31 | Self::Macro(ref r#macro) => r#macro.to_tokens(tokens), 32 | Self::Type(ref r#type) => r#type.to_tokens(tokens), 33 | } 34 | } 35 | } 36 | 37 | impl From for ImplItem { 38 | fn from(value: syn::ImplItemConst) -> Self { 39 | Self::Const(value) 40 | } 41 | } 42 | 43 | impl From for ImplItem { 44 | fn from(value: syn::ImplItemFn) -> Self { 45 | Self::Fn(value) 46 | } 47 | } 48 | 49 | impl From for ImplItem { 50 | fn from(value: syn::ImplItemMacro) -> Self { 51 | Self::Macro(value) 52 | } 53 | } 54 | 55 | impl From for ImplItem { 56 | fn from(value: syn::ImplItemType) -> Self { 57 | Self::Type(value) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /nnn-macros/src/codegen/test_fn.rs: -------------------------------------------------------------------------------- 1 | /* Dependencies */ 2 | use syn::{ 3 | parse::{Parse, ParseStream}, 4 | parse_quote, 5 | }; 6 | 7 | pub(crate) struct TestFn(syn::ItemFn); 8 | 9 | impl TestFn { 10 | pub(crate) fn make_conditional(&mut self, condition: &syn::Expr) { 11 | self.0.attrs.push(parse_quote! { #[cfg(#condition)] }); 12 | } 13 | } 14 | 15 | impl Parse for TestFn { 16 | fn parse(input: ParseStream) -> syn::Result { 17 | let item_fn: syn::ItemFn = input.parse()?; 18 | 19 | if !item_fn 20 | .attrs 21 | .iter() 22 | .any(|attr| attr.path().is_ident("test")) 23 | { 24 | return Err(syn::Error::new_spanned( 25 | item_fn, 26 | "Missing #[test] attribute on test function.", 27 | )); 28 | } 29 | 30 | Ok(Self(item_fn)) 31 | } 32 | } 33 | 34 | impl quote::ToTokens for TestFn { 35 | fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { 36 | self.0.to_tokens(tokens); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /nnn-macros/src/ctx.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use crate::{argument::Arguments, utils::syn_ext::SynDataExt as _}; 3 | 4 | pub(crate) struct Context { 5 | generics: syn::Generics, 6 | type_name: syn::Ident, 7 | arguments: Arguments, 8 | } 9 | 10 | impl Context { 11 | pub(crate) const fn generics(&self) -> &syn::Generics { 12 | &self.generics 13 | } 14 | 15 | pub(crate) const fn type_name(&self) -> &syn::Ident { 16 | &self.type_name 17 | } 18 | 19 | pub(crate) const fn args(&self) -> &Arguments { 20 | &self.arguments 21 | } 22 | } 23 | 24 | impl TryFrom<(syn::DeriveInput, Arguments)> for Context { 25 | type Error = syn::Error; 26 | 27 | fn try_from( 28 | (input, arguments): (syn::DeriveInput, Arguments), 29 | ) -> Result { 30 | if let Some(attr) = input.attrs.first() { 31 | return Err(syn::Error::new_spanned( 32 | attr, 33 | "Attributes are not supported; pass additional parameters via `nnn` instead.", 34 | )); 35 | } 36 | 37 | let syn::DeriveInput { 38 | data, 39 | ident: type_name, 40 | generics, 41 | .. 42 | } = input; 43 | 44 | let syn::Data::Struct(data_struct) = data else { 45 | return Err(syn::Error::new( 46 | data.decl_span(), 47 | "nnn is only supported on structs.", 48 | )); 49 | }; 50 | 51 | let syn::Fields::Unnamed(syn::FieldsUnnamed { 52 | unnamed: fields, .. 53 | }) = data_struct.fields 54 | else { 55 | return Err(syn::Error::new_spanned( 56 | data_struct.fields, 57 | "`nnn` can only be used on structs with unnamed fields.", 58 | )); 59 | }; 60 | 61 | let mut fields_iter = fields.iter(); 62 | let Some(inner_field) = fields_iter.next() else { 63 | return Err(syn::Error::new_spanned( 64 | fields, 65 | "Cannot use `nnn` on empty structs.", 66 | )); 67 | }; 68 | 69 | if !matches!(inner_field.vis, syn::Visibility::Inherited) { 70 | return Err(syn::Error::new_spanned( 71 | &inner_field.vis, 72 | "You can only have a private field here.", 73 | )); 74 | } 75 | 76 | if let Some(extra_field) = fields_iter.next() { 77 | return Err(syn::Error::new_spanned( 78 | extra_field, 79 | "You cannot have more than one field.", 80 | )); 81 | } 82 | 83 | Ok(Self { 84 | generics, 85 | type_name, 86 | arguments, 87 | }) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /nnn-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![no_std] 3 | /* Modules */ 4 | mod argument; 5 | mod codegen; 6 | mod ctx; 7 | mod utils; 8 | /* Built-in imports */ 9 | extern crate alloc; 10 | use alloc::collections::BTreeMap; 11 | /* Crate imports */ 12 | use argument::{Argument, Arguments}; 13 | use ctx::Context; 14 | /* Dependencies imports */ 15 | use quote::quote; 16 | use syn::{parse::Parser as _, punctuated::Punctuated}; 17 | use utils::syn_ext::SynDataExt as _; 18 | 19 | #[proc_macro_attribute] 20 | pub fn nnn( 21 | nnn_args: proc_macro::TokenStream, 22 | type_definition: proc_macro::TokenStream, 23 | ) -> proc_macro::TokenStream { 24 | expand(nnn_args, type_definition) 25 | .unwrap_or_else(|err| err.to_compile_error()) 26 | .into() 27 | } 28 | 29 | fn expand( 30 | nnn_args: proc_macro::TokenStream, 31 | type_definition: proc_macro::TokenStream, 32 | ) -> syn::Result { 33 | let input: syn::DeriveInput = syn::parse(type_definition)?; 34 | let original_visibility = input.vis.clone(); 35 | 36 | let args = Arguments::from( 37 | Punctuated::::parse_terminated 38 | .parse(nnn_args)?, 39 | ); 40 | 41 | let (type_name, inner_type, generics) = split_derive_input(input.clone())?; 42 | let ctx = Context::try_from((input, args))?; 43 | let (impl_generics, ty_generics, where_clause) = 44 | ctx.generics().split_for_impl(); 45 | 46 | let tests = ctx.args().get_tests(&ctx); 47 | let impls = ctx.args().get_impls(&ctx); 48 | let ( 49 | impl_blocks, 50 | bare_impls, 51 | macro_attrs, 52 | err_variants, 53 | validity_checks, 54 | err_display_arm, 55 | sanitization_steps, 56 | new_enums, 57 | ) = codegen::Implementation::separate_variants(&impls); 58 | 59 | let dedup_err_variants = err_variants 60 | .map(|variant| (variant.ident.clone(), variant)) 61 | .collect::>() 62 | .into_values(); 63 | 64 | let error_type = quote::format_ident!("{type_name}Error",); 65 | let mod_name = quote::format_ident!("__private_{type_name}",); 66 | 67 | Ok(quote! { 68 | #[doc(hidden)] 69 | #[allow(non_snake_case, reason = "Includes NNNType name which is probably CamelCase.")] 70 | #[allow(clippy::module_name_repetitions, reason = "Includes NNNType which is probably the name of the file.")] 71 | mod #mod_name { 72 | use super::*; 73 | 74 | #(#macro_attrs)* 75 | pub struct #type_name #generics (#inner_type) #where_clause; 76 | 77 | #[derive(Debug, Clone, PartialEq, Eq)] 78 | #[non_exhaustive] 79 | pub enum #error_type { 80 | #(#dedup_err_variants),* 81 | } 82 | 83 | impl ::core::error::Error for #error_type {} 84 | 85 | impl ::core::fmt::Display for #error_type { 86 | fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 87 | match *self { 88 | #(#err_display_arm)* 89 | } 90 | } 91 | } 92 | 93 | impl #impl_generics nnn::NNNewType for #type_name #ty_generics #where_clause { 94 | type Inner = #inner_type; 95 | type Error = #error_type; 96 | 97 | fn sanitize(mut value: Self::Inner) -> Self::Inner { 98 | #(#sanitization_steps;)* 99 | value 100 | } 101 | 102 | fn try_new(mut value: Self::Inner) -> Result { 103 | value = Self::sanitize(value); 104 | #(#validity_checks;)* 105 | Ok(Self(value)) 106 | } 107 | 108 | fn into_inner(self) -> Self::Inner { 109 | self.0 110 | } 111 | } 112 | 113 | impl #impl_generics #type_name #ty_generics #where_clause { 114 | #(#bare_impls)* 115 | } 116 | 117 | #(#impl_blocks)* 118 | 119 | #(#new_enums)* 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | 125 | #(#tests)* 126 | } 127 | } 128 | 129 | #[allow(clippy::pub_use, reason = "pub use can happen if struct is meant to be public.")] 130 | #original_visibility use #mod_name::*; 131 | }) 132 | } 133 | 134 | fn split_derive_input( 135 | input: syn::DeriveInput, 136 | ) -> Result<(syn::Ident, syn::Type, syn::Generics), syn::Error> { 137 | if let Some(attr) = input.attrs.first() { 138 | return Err(syn::Error::new_spanned( 139 | attr, 140 | "Attributes are not supported; pass additional parameters via `nnn` instead.", 141 | )); 142 | } 143 | 144 | let syn::DeriveInput { 145 | data, 146 | ident: type_name, 147 | generics, 148 | .. 149 | } = input; 150 | 151 | let syn::Data::Struct(data_struct) = data else { 152 | return Err(syn::Error::new( 153 | data.decl_span(), 154 | "nnn is only supported on structs.", 155 | )); 156 | }; 157 | 158 | let syn::Fields::Unnamed(syn::FieldsUnnamed { 159 | unnamed: fields, .. 160 | }) = data_struct.fields 161 | else { 162 | return Err(syn::Error::new_spanned( 163 | data_struct.fields, 164 | "`nnn` can only be used on structs with unnamed fields.", 165 | )); 166 | }; 167 | 168 | let mut fields_iter = fields.iter(); 169 | let Some(inner_field) = fields_iter.next() else { 170 | return Err(syn::Error::new_spanned( 171 | fields, 172 | "Cannot use `nnn` on empty structs.", 173 | )); 174 | }; 175 | 176 | if !matches!(inner_field.vis, syn::Visibility::Inherited) { 177 | return Err(syn::Error::new_spanned( 178 | &inner_field.vis, 179 | "You can only have a private field here.", 180 | )); 181 | } 182 | 183 | if let Some(extra_field) = fields_iter.next() { 184 | return Err(syn::Error::new_spanned( 185 | extra_field, 186 | "You cannot have more than one field.", 187 | )); 188 | } 189 | 190 | Ok((type_name, inner_field.ty.clone(), generics)) 191 | } 192 | -------------------------------------------------------------------------------- /nnn-macros/src/utils.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::{format, string::String}; 4 | /* Modules */ 5 | pub(crate) mod closure; 6 | pub(crate) mod regex_input; 7 | pub(crate) mod syn_ext; 8 | 9 | pub(crate) fn capitalize(str: &str) -> String { 10 | let (head, tail) = str.split_at(1); 11 | format!("{}{}", head.to_uppercase(), tail) 12 | } 13 | -------------------------------------------------------------------------------- /nnn-macros/src/utils/closure.rs: -------------------------------------------------------------------------------- 1 | /* Dependencies */ 2 | use syn::parse::{Parse, ParseStream}; 3 | 4 | /// Either 5 | /// custom = `path::to::fn` 6 | /// custom = |input: Type| { ...; return .. } 7 | /// custom = { instructions; } 8 | pub(crate) enum CustomFunction { 9 | /// Path to the function to run of signature 10 | /// Fn(mut Inner) -> Inner 11 | /// mut being optional 12 | Path(syn::Path), 13 | /// Closure of type 14 | /// Fn(mut Inner) -> Inner 15 | /// mut being optional 16 | Closure(syn::ExprClosure), 17 | /// Depends on `value` being the input value to sanitize. 18 | /// Block can be `{ value.sort(); }` or `{ value = value....; }` 19 | Block(syn::Block), 20 | } 21 | 22 | impl Parse for CustomFunction { 23 | fn parse(input: ParseStream) -> syn::Result { 24 | let closure = if let Ok(path) = input.parse::() { 25 | Self::Path(path) 26 | } else if let Ok(closure) = input.parse::() { 27 | Self::Closure(closure) 28 | } else if let Ok(block) = input.parse::() { 29 | Self::Block(block) 30 | } else { 31 | return Err(syn::Error::new( 32 | input.span(), 33 | "Invalid `custom` argument input.", 34 | )); 35 | }; 36 | 37 | if !input.peek(syn::Token![,]) && !input.is_empty() { 38 | return Err(input.error("Unexpected token(s).")); 39 | } 40 | 41 | Ok(closure) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /nnn-macros/src/utils/regex_input.rs: -------------------------------------------------------------------------------- 1 | /* Dependencies */ 2 | use syn::parse::{Parse, ParseStream}; 3 | 4 | pub(crate) enum RegexInput { 5 | StringLiteral(syn::LitStr), 6 | Path(syn::Path), 7 | } 8 | 9 | impl Parse for RegexInput { 10 | fn parse(input: ParseStream) -> syn::Result { 11 | let regex = if let Ok(lit_str) = input.parse::() { 12 | #[cfg(feature = "regex_validation")] 13 | { 14 | extern crate alloc; 15 | // Compile time check for literal regex 16 | regex::Regex::new(&lit_str.value()).map_err(|err| { 17 | syn::Error::new_spanned( 18 | &lit_str, 19 | ::alloc::format!("Incorrect Regex {err}"), 20 | ) 21 | })?; 22 | }; 23 | Self::StringLiteral(lit_str) 24 | } else { 25 | Self::Path(input.parse::()?) 26 | }; 27 | 28 | if !input.peek(syn::Token![,]) && !input.is_empty() { 29 | return Err(input.error("Unexpected token(s).")); 30 | } 31 | 32 | Ok(regex) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /nnn-macros/src/utils/syn_ext.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | extern crate alloc; 3 | use alloc::{ 4 | format, 5 | string::{String, ToString}, 6 | }; 7 | /* Crate imports */ 8 | use crate::utils; 9 | /* Dependencies */ 10 | use quote::ToTokens as _; 11 | use syn::{ 12 | parse::{Parse, ParseBuffer}, 13 | punctuated::Punctuated, 14 | token::Comma, 15 | PathSegment, 16 | }; 17 | 18 | pub(crate) trait SynDataExt { 19 | fn decl_span(&self) -> proc_macro2::Span; 20 | } 21 | 22 | impl SynDataExt for syn::Data { 23 | fn decl_span(&self) -> proc_macro2::Span { 24 | match *self { 25 | Self::Struct(ref data_struct) => data_struct.struct_token.span, 26 | Self::Enum(ref data_enum) => data_enum.enum_token.span, 27 | Self::Union(ref data_union) => data_union.union_token.span, 28 | } 29 | } 30 | } 31 | 32 | pub(crate) trait SynParseBufferExt { 33 | fn parse_equal(&self) -> syn::Result; 34 | fn parse_assign(&self) -> syn::Result<(syn::Ident, T)>; 35 | fn parse_parenthesized( 36 | &self, 37 | ) -> syn::Result>; 38 | fn require_ident(&self, name: &str) -> syn::Result; 39 | } 40 | 41 | impl SynParseBufferExt for ParseBuffer<'_> { 42 | fn parse_equal(&self) -> syn::Result { 43 | self.parse::()?; 44 | T::parse(self) 45 | } 46 | 47 | fn parse_assign(&self) -> syn::Result<(syn::Ident, T)> { 48 | let name = self.parse::()?; 49 | let value = self.parse_equal::()?; 50 | Ok((name, value)) 51 | } 52 | 53 | fn parse_parenthesized( 54 | &self, 55 | ) -> syn::Result> { 56 | let content; 57 | syn::parenthesized!(content in self); 58 | content.parse_terminated(T::parse, syn::Token![,]) 59 | } 60 | 61 | fn require_ident(&self, name: &str) -> syn::Result { 62 | let ident = self.parse::()?; 63 | if ident != name { 64 | return Err(syn::Error::new_spanned( 65 | ident, 66 | format!("Expected ident to be `{name}`."), 67 | )); 68 | } 69 | Ok(ident) 70 | } 71 | } 72 | 73 | pub(crate) trait SynPathExt { 74 | fn as_ident(&self) -> syn::Ident; 75 | fn trait_segment(&self) -> syn::Result<&PathSegment>; 76 | fn item_name(&self) -> syn::Result; 77 | } 78 | 79 | impl SynPathExt for syn::Path { 80 | /// turns `std::io::Error` into `StdIoError` 81 | fn as_ident(&self) -> syn::Ident { 82 | quote::format_ident!( 83 | "{}", 84 | self.to_token_stream() 85 | .to_string() 86 | .to_ascii_lowercase() 87 | .replace(':', "") 88 | .split_ascii_whitespace() 89 | .map(utils::capitalize) 90 | .collect::() 91 | ) 92 | } 93 | 94 | fn trait_segment(&self) -> syn::Result<&PathSegment> { 95 | self.segments.last().ok_or_else(|| { 96 | syn::Error::new_spanned(self, "Trait doesn't have a name ??") 97 | }) 98 | } 99 | 100 | fn item_name(&self) -> syn::Result { 101 | self.trait_segment() 102 | .map(|seg| &seg.ident) 103 | .map(ToString::to_string) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | force_explicit_abi = true 3 | hard_tabs = false 4 | match_block_trailing_comma = true 5 | max_width = 80 6 | merge_derives = true 7 | newline_style = "Unix" 8 | remove_nested_parens = true 9 | reorder_imports = true 10 | reorder_modules = true 11 | tab_spaces = 4 12 | use_field_init_shorthand = true 13 | use_small_heuristics = "Default" 14 | use_try_shorthand = false 15 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![no_std] 3 | 4 | pub use nnn_macros::nnn; 5 | 6 | pub trait NNNewType: Sized { 7 | type Inner; 8 | type Error; 9 | 10 | fn try_new(value: Self::Inner) -> Result; 11 | fn into_inner(self) -> Self::Inner; 12 | fn sanitize(value: Self::Inner) -> Self::Inner { 13 | value 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/derives/as_ref.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(AsRef, AsRef))] 5 | struct MyString(String); 6 | 7 | #[nnn(nnn_derive(AsRef<_, [u8]>))] 8 | struct MyVec(Vec); 9 | -------------------------------------------------------------------------------- /tests/derives/borrow.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(Borrow, Borrow))] 5 | struct MyString(String); 6 | 7 | #[nnn(nnn_derive(Borrow<_, [u8]>))] 8 | struct MyVec(Vec); 9 | -------------------------------------------------------------------------------- /tests/derives/deref.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(Deref))] 5 | struct MyString(String); 6 | 7 | #[nnn(nnn_derive(Deref))] 8 | struct MyVec(Vec); 9 | 10 | #[nnn(nnn_derive(Deref<_>))] 11 | struct MyVec2(Vec); 12 | -------------------------------------------------------------------------------- /tests/derives/from.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(From, From))] 5 | struct Float(f32); 6 | 7 | #[nnn(nnn_derive(From<_, i64>))] 8 | struct Num(i32); 9 | -------------------------------------------------------------------------------- /tests/derives/from_str.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Built-in imports */ 4 | use core::str::FromStr; 5 | /* Dependencies */ 6 | use rstest::rstest; 7 | 8 | #[nnn(nnn_derive(FromStr), validators(positive))] 9 | struct PositiveFloat(f64); 10 | 11 | #[rstest] 12 | #[case("1")] 13 | #[case("1.0")] 14 | fn valid_not_nan_test(#[case] input: &str) { 15 | PositiveFloat::from_str(input).unwrap(); 16 | } 17 | 18 | #[rstest] 19 | #[case("toto")] 20 | fn invalid_not_float_parse_test(#[case] input: &str) { 21 | assert!(matches!( 22 | PositiveFloat::from_str(input), 23 | Err(PositiveFloatParseError::InnerParse(_)) 24 | )); 25 | } 26 | 27 | #[rstest] 28 | #[case("-10.0")] 29 | fn invalid_negative_float_parse_test(#[case] input: &str) { 30 | assert!(matches!( 31 | PositiveFloat::from_str(input), 32 | Err(PositiveFloatParseError::Validation( 33 | PositiveFloatError::Positive 34 | )) 35 | )); 36 | } 37 | -------------------------------------------------------------------------------- /tests/derives/into.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(Into))] 5 | struct Float(f64); 6 | 7 | #[nnn(nnn_derive(Into<_, u16, u32>))] 8 | struct Num(u8); 9 | -------------------------------------------------------------------------------- /tests/derives/into_iterator.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(IntoIterator))] 5 | struct FloatVec(Vec); 6 | -------------------------------------------------------------------------------- /tests/derives/serde.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | #[nnn( 8 | derive(Debug, Serialize, Deserialize, PartialEq), 9 | nnn_derive(TryFrom), 10 | validators(positive) 11 | )] 12 | struct Foo(f64); 13 | 14 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 15 | struct Data { 16 | pub foo: Foo, 17 | } 18 | 19 | #[rstest] 20 | #[case(r#"{ "foo": 0.1 }"#, 0.1_f64)] 21 | #[case(r#"{ "foo": 3.0 }"#, 3.0_f64)] 22 | fn data_deserialization_valid(#[case] input: &str, #[case] expected: f64) { 23 | let deserialized = 24 | serde_json::from_str::(input).expect("Deserialization failed."); 25 | let expected_data = Data { 26 | foo: Foo::try_new(expected).expect("Should have a valid input."), 27 | }; 28 | assert_eq!(deserialized, expected_data); 29 | } 30 | 31 | #[rstest] 32 | #[case(r#"{ "foo": -3.0 }"#)] 33 | #[case(r#"{ "foo": -0.0 }"#)] 34 | #[case(r#"{ "foo": "0.0" }"#)] 35 | #[case(r#"{ "foo": "coucou" }"#)] 36 | fn data_deserialization_invalid(#[case] input: &str) { 37 | serde_json::from_str::(input).unwrap_err(); 38 | } 39 | -------------------------------------------------------------------------------- /tests/derives/try_from.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::nnn; 3 | 4 | #[nnn(nnn_derive(TryFrom, TryFrom))] 5 | struct Float(f64); 6 | 7 | #[nnn(nnn_derive(TryFrom<_, i32>))] 8 | struct Num(i64); 9 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, unused_imports, reason = "Tests, dead_code is expected.")] 2 | /* Modules */ 3 | #[rustfmt::skip] // wants to reorder modules 4 | mod validators { 5 | // Containers 6 | mod not_empty; 7 | mod each; 8 | mod min_length; 9 | mod min_length_or_eq; 10 | mod length; 11 | mod max_length; 12 | mod max_length_or_eq; 13 | // Numerics 14 | mod min; 15 | mod min_or_eq; 16 | mod max; 17 | mod max_or_eq; 18 | mod positive; 19 | mod negative; 20 | // Float specifics 21 | mod finite; 22 | mod not_infinite; 23 | mod not_nan; 24 | // String specifics 25 | mod regex; 26 | // Common 27 | mod exactly; 28 | mod custom; 29 | mod predicate; 30 | } 31 | 32 | mod derives { 33 | mod as_ref; 34 | mod borrow; 35 | mod deref; 36 | mod from; 37 | mod from_str; 38 | mod into; 39 | mod into_iterator; 40 | mod serde; 41 | mod try_from; 42 | } 43 | 44 | #[rustfmt::skip] // wants to reorder modules 45 | mod sanitizers { 46 | // Containers 47 | mod each; 48 | mod sort; 49 | mod dedup; 50 | // Strings 51 | mod trim; 52 | mod lowercase; 53 | mod uppercase; 54 | // Common 55 | mod custom; 56 | } 57 | 58 | #[cfg(test)] 59 | mod ui { 60 | #[test] 61 | fn ui_pass() { 62 | trybuild::TestCases::new().pass("tests/ui/pass/*.rs"); 63 | #[cfg(not(feature = "regex_validation"))] 64 | trybuild::TestCases::new() 65 | .pass("tests/ui/conditionals/invalid_compile_time_regex.rs"); 66 | } 67 | 68 | #[test] 69 | fn ui_fail() { 70 | trybuild::TestCases::new().compile_fail("tests/ui/fail/*.rs"); 71 | #[cfg(feature = "regex_validation")] 72 | trybuild::TestCases::new().compile_fail( 73 | "tests/ui/conditionals/invalid_compile_time_regex.rs", 74 | ); 75 | } 76 | } 77 | 78 | #[doc(hidden)] 79 | pub mod utils { 80 | macro_rules! sign_tests { 81 | ($sign_test:ident, $($ty:ty, valids = [$($valid:literal),*], invalids = [$($invalid:expr),*]),*) => { 82 | $( 83 | paste::paste! { 84 | mod [< $sign_test _ $ty >] { 85 | use rstest::rstest; 86 | use nnn::{nnn, NNNewType as _}; 87 | 88 | #[nnn(validators($sign_test))] 89 | struct NNN($ty); 90 | 91 | #[rstest] 92 | $(#[case($valid)])* 93 | fn [< valid_ $sign_test _ $ty >](#[case] input: $ty) { 94 | NNN::try_new(input).unwrap(); 95 | } 96 | 97 | #[rstest] 98 | $(#[case($invalid)])* 99 | fn [< invalid_ $sign_test _ $ty >](#[case] input: $ty) { 100 | assert!(NNN::try_new(input).is_err()); 101 | } 102 | } 103 | } 104 | )* 105 | }; 106 | } 107 | 108 | pub(crate) use sign_tests; 109 | } 110 | -------------------------------------------------------------------------------- /tests/sanitizers/custom.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(custom = |value: String| value.to_uppercase() ))] 7 | struct SanitizedWithClosureString(String); 8 | 9 | #[rstest] 10 | #[case("HeLLo WoRLd", "HELLO WORLD")] 11 | #[case("mixed_case", "MIXED_CASE")] 12 | fn sanitize_uppercase_with_closure( 13 | #[case] input: &str, 14 | #[case] expected: &str, 15 | ) { 16 | let sanitized = 17 | SanitizedWithClosureString::try_new(input.to_owned()).unwrap(); 18 | assert_eq!(sanitized.into_inner(), expected); 19 | } 20 | 21 | #[expect( 22 | clippy::needless_pass_by_value, 23 | reason = "For now we don't pass by reference" 24 | )] 25 | fn sanitizer(value: String) -> String { 26 | value.to_uppercase() 27 | } 28 | #[nnn(sanitizers(custom = sanitizer))] 29 | struct SanitizedWithFnPathString(String); 30 | 31 | #[rstest] 32 | #[case("HeLLo WoRLd", "HELLO WORLD")] 33 | #[case("mixed_case", "MIXED_CASE")] 34 | fn sanitize_uppercase_with_fn_path( 35 | #[case] input: &str, 36 | #[case] expected: &str, 37 | ) { 38 | let sanitized = 39 | SanitizedWithFnPathString::try_new(input.to_owned()).unwrap(); 40 | assert_eq!(sanitized.into_inner(), expected); 41 | } 42 | 43 | #[nnn(sanitizers(custom = { value = value.to_uppercase(); } ))] 44 | struct SanitizedWithBlockString(String); 45 | 46 | #[rstest] 47 | #[case("HeLLo WoRLd", "HELLO WORLD")] 48 | #[case("mixed_case", "MIXED_CASE")] 49 | fn sanitize_uppercase_with_block(#[case] input: &str, #[case] expected: &str) { 50 | let sanitized = 51 | SanitizedWithBlockString::try_new(input.to_owned()).unwrap(); 52 | assert_eq!(sanitized.into_inner(), expected); 53 | } 54 | -------------------------------------------------------------------------------- /tests/sanitizers/dedup.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(dedup))] 7 | struct SanitizedDedupedVec(Vec); 8 | 9 | #[rstest] 10 | #[case(vec![3_i32, 3_i32, 2_i32, 1_i32, 1_i32, 2_i32], vec![3_i32, 2_i32, 1_i32, 2_i32])] // dedeup only removes consecutives duplicates 11 | #[case(vec![1_i32, 1_i32, 1_i32], vec![1_i32])] 12 | fn sanitize_dedup(#[case] input: Vec, #[case] expected: Vec) { 13 | let sanitized = SanitizedDedupedVec::try_new(input).unwrap(); 14 | assert_eq!(sanitized.into_inner(), expected); 15 | } 16 | -------------------------------------------------------------------------------- /tests/sanitizers/each.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(each(trim, lowercase)))] 7 | struct SanitizedVec(Vec); 8 | 9 | #[rstest] 10 | #[case(vec![" HELLO ", " WoRLD "], vec!["hello", "world"])] 11 | fn sanitize_each_trim_lowercase( 12 | #[case] input: Vec<&str>, 13 | #[case] expected: Vec<&str>, 14 | ) { 15 | let input_as_strings = input.into_iter().map(String::from).collect(); 16 | let sanitized = SanitizedVec::try_new(input_as_strings).unwrap(); 17 | assert_eq!(sanitized.into_inner(), expected); 18 | } 19 | -------------------------------------------------------------------------------- /tests/sanitizers/lowercase.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(lowercase))] 7 | struct LowercasedString(String); 8 | 9 | #[rstest] 10 | #[case("HeLLo WoRLd", "hello world")] 11 | #[case("MIXED_CASE", "mixed_case")] 12 | fn sanitize_lowercase(#[case] input: &str, #[case] expected: &str) { 13 | let sanitized = LowercasedString::try_new(input.to_owned()).unwrap(); 14 | assert_eq!(sanitized.into_inner(), expected); 15 | } 16 | -------------------------------------------------------------------------------- /tests/sanitizers/sort.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(sort))] 7 | struct SanitizedSortedVec(Vec); 8 | 9 | #[rstest] 10 | #[case(vec![3_i32, 1_i32, 2_i32, 3_i32, 2_i32, 1_i32], vec![1_i32, 1_i32, 2_i32, 2_i32, 3_i32, 3_i32])] 11 | #[case(vec![5_i32, 3_i32, 4_i32], vec![3_i32, 4_i32, 5_i32])] 12 | fn sanitize_sort(#[case] input: Vec, #[case] expected: Vec) { 13 | let sanitized = SanitizedSortedVec::try_new(input).unwrap(); 14 | assert_eq!(sanitized.into_inner(), expected); 15 | } 16 | -------------------------------------------------------------------------------- /tests/sanitizers/trim.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(trim))] 7 | struct SanitizedString(String); 8 | 9 | #[rstest] 10 | #[case(" hello world ", "hello world")] 11 | #[case("\n\t trim this! ", "trim this!")] 12 | fn sanitize_trim(#[case] input: &str, #[case] expected: &str) { 13 | let sanitized = SanitizedString::try_new(input.to_owned()).unwrap(); 14 | assert_eq!(sanitized.into_inner(), expected); 15 | } 16 | -------------------------------------------------------------------------------- /tests/sanitizers/uppercase.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(sanitizers(uppercase))] 7 | struct UppercasedString(String); 8 | 9 | #[rstest] 10 | #[case("HeLLo WoRLd", "HELLO WORLD")] 11 | #[case("mixed_case", "MIXED_CASE")] 12 | fn sanitize_uppercase(#[case] input: &str, #[case] expected: &str) { 13 | let sanitized = UppercasedString::try_new(input.to_owned()).unwrap(); 14 | assert_eq!(sanitized.into_inner(), expected); 15 | } 16 | -------------------------------------------------------------------------------- /tests/ui/conditionals/invalid_compile_time_regex.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_imports, reason = "Not what we're testing.")] 2 | //! Is compile time error if feature `regex_validation` is enabled 3 | //! else it will compile but fail generated tests. 4 | use nnn::{nnn, NNNewType as _}; 5 | 6 | #[nnn(validators(regex = r#"(\d+"#))] 7 | struct InvalidRegex(String); 8 | 9 | fn main() {} 10 | -------------------------------------------------------------------------------- /tests/ui/conditionals/invalid_compile_time_regex.stderr: -------------------------------------------------------------------------------- 1 | error: Incorrect Regex regex parse error: 2 | (\d+ 3 | ^ 4 | error: unclosed group 5 | --> tests/ui/conditionals/invalid_compile_time_regex.rs:6:26 6 | | 7 | 6 | #[nnn(validators(regex = r#"(\d+"#))] 8 | | ^^^^^^^^^ 9 | -------------------------------------------------------------------------------- /tests/ui/fail/custom_derive_empty_generics_parameters.rs: -------------------------------------------------------------------------------- 1 | use nnn::nnn; 2 | 3 | #[nnn(nnn_derive(Into<>))] 4 | struct WrapperInto(i8); 5 | #[nnn(nnn_derive(From<>))] 6 | struct WrapperFrom(i8); 7 | #[nnn(nnn_derive(TryFrom<>))] 8 | struct WrapperTryFrom(i8); 9 | #[nnn(nnn_derive(Borrow<>))] 10 | struct WrapperBorrow(i8); 11 | #[nnn(nnn_derive(AsRef<>))] 12 | struct WrapperAsRef(i8); 13 | #[nnn(nnn_derive(Deref<>))] 14 | struct WrapperDeref(i8); 15 | 16 | fn main() {} 17 | -------------------------------------------------------------------------------- /tests/ui/fail/custom_derive_empty_generics_parameters.stderr: -------------------------------------------------------------------------------- 1 | error: Please provide generics arguments, or omit the '<>' for the default derive. 2 | --> tests/ui/fail/custom_derive_empty_generics_parameters.rs:3:22 3 | | 4 | 3 | #[nnn(nnn_derive(Into<>))] 5 | | ^^ 6 | 7 | error: Please provide generics arguments, or omit the '<>' for the default derive. 8 | --> tests/ui/fail/custom_derive_empty_generics_parameters.rs:5:22 9 | | 10 | 5 | #[nnn(nnn_derive(From<>))] 11 | | ^^ 12 | 13 | error: Please provide generics arguments, or omit the '<>' for the default derive. 14 | --> tests/ui/fail/custom_derive_empty_generics_parameters.rs:7:25 15 | | 16 | 7 | #[nnn(nnn_derive(TryFrom<>))] 17 | | ^^ 18 | 19 | error: Please provide generics arguments, or omit the '<>' for the default derive. 20 | --> tests/ui/fail/custom_derive_empty_generics_parameters.rs:9:24 21 | | 22 | 9 | #[nnn(nnn_derive(Borrow<>))] 23 | | ^^ 24 | 25 | error: Please provide generics arguments, or omit the '<>' for the default derive. 26 | --> tests/ui/fail/custom_derive_empty_generics_parameters.rs:11:23 27 | | 28 | 11 | #[nnn(nnn_derive(AsRef<>))] 29 | | ^^ 30 | 31 | error: Please provide generics arguments, or omit the '<>' for the default derive. 32 | --> tests/ui/fail/custom_derive_empty_generics_parameters.rs:13:23 33 | | 34 | 13 | #[nnn(nnn_derive(Deref<>))] 35 | | ^^ 36 | -------------------------------------------------------------------------------- /tests/ui/fail/derive_default.rs: -------------------------------------------------------------------------------- 1 | use nnn::nnn; 2 | 3 | #[nnn(derive(Default))] 4 | struct DefaultFloat(f64); 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /tests/ui/fail/derive_default.stderr: -------------------------------------------------------------------------------- 1 | error: To derive the `Default` trait, use the `default` or `default = ..` argument. 2 | --> tests/ui/fail/derive_default.rs:3:14 3 | | 4 | 3 | #[nnn(derive(Default))] 5 | | ^^^^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/fail/derive_from.rs: -------------------------------------------------------------------------------- 1 | use nnn::nnn; 2 | 3 | #[nnn(derive(From))] 4 | struct FromFloat(f64); 5 | 6 | fn main() {} 7 | -------------------------------------------------------------------------------- /tests/ui/fail/derive_from.stderr: -------------------------------------------------------------------------------- 1 | error: Deriving `From` results in a possible bypass of the validators and sanitizers and is therefore forbidden. 2 | --> tests/ui/fail/derive_from.rs:3:14 3 | | 4 | 3 | #[nnn(derive(From))] 5 | | ^^^^ 6 | -------------------------------------------------------------------------------- /tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_imports, reason = "Not what we're testing.")] 2 | #![expect(unexpected_cfgs, reason = "Dunno how to generate them properly.")] 3 | use nnn::{nnn, NNNewType as _}; 4 | 5 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord))] 6 | struct Float(f64); 7 | 8 | fn main() {} 9 | 10 | // Invalid because `finite` validator is gated behind a `cfg` 11 | // While `derive(Eq, Ord)` isn't. 12 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord), cfg(test, validators(finite)))] 13 | struct FiniteFloat(f64); 14 | 15 | // Invalid because `finite` validator is gated behind a `cfg` 16 | // While `derive(Eq, Ord)` is behind a different one. 17 | #[nnn( 18 | derive(PartialEq, PartialOrd), 19 | cfg(toto, validators(finite)), 20 | cfg(test, derive(Eq, Ord)) 21 | )] 22 | struct FiniteFloat2(f64); 23 | 24 | // Invalid because `not_nan` validator is gated behind a `cfg` 25 | // While `derive(Eq)` isn't. 26 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord), cfg(test, validators(not_nan)))] 27 | struct FiniteFloat3(f64); 28 | 29 | // Invalid because `not_nan` validator is gated behind a `cfg` 30 | // While `derive(Eq)` is behind a different one. 31 | #[nnn( 32 | derive(PartialEq, PartialOrd, 33 | cfg(toto, validators(not_nan)), 34 | cfg(test, derive(Eq, Ord))) 35 | )] 36 | struct FiniteFloat4(f64); 37 | -------------------------------------------------------------------------------- /tests/ui/fail/float_ord_eq_without_NAN_exclusion.stderr: -------------------------------------------------------------------------------- 1 | error: expected `,` 2 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:33:8 3 | | 4 | 33 | cfg(toto, validators(not_nan)), 5 | | ^ 6 | 7 | error[E0277]: the trait bound `f64: Ord` is not satisfied 8 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:6:14 9 | | 10 | 5 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord))] 11 | | --- in this derive macro expansion 12 | 6 | struct Float(f64); 13 | | ^^^ the trait `Ord` is not implemented for `f64` 14 | | 15 | = help: the following other types implement trait `Ord`: 16 | i128 17 | i16 18 | i32 19 | i64 20 | i8 21 | isize 22 | u128 23 | u16 24 | and $N others 25 | = note: this error originates in the derive macro `Ord` (in Nightly builds, run with -Z macro-backtrace for more info) 26 | 27 | error[E0277]: the trait bound `f64: std::cmp::Eq` is not satisfied 28 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:6:14 29 | | 30 | 5 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord))] 31 | | -- in this derive macro expansion 32 | 6 | struct Float(f64); 33 | | ^^^ the trait `std::cmp::Eq` is not implemented for `f64` 34 | | 35 | = help: the following other types implement trait `std::cmp::Eq`: 36 | i128 37 | i16 38 | i32 39 | i64 40 | i8 41 | isize 42 | u128 43 | u16 44 | and $N others 45 | note: required by a bound in `AssertParamIsEq` 46 | --> $RUST/core/src/cmp.rs 47 | | 48 | | pub struct AssertParamIsEq { 49 | | ^^ required by this bound in `AssertParamIsEq` 50 | = note: this error originates in the derive macro `Eq` (in Nightly builds, run with -Z macro-backtrace for more info) 51 | 52 | error[E0277]: the trait bound `f64: Ord` is not satisfied 53 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:13:20 54 | | 55 | 12 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord), cfg(test, validators(finite)))] 56 | | --- in this derive macro expansion 57 | 13 | struct FiniteFloat(f64); 58 | | ^^^ the trait `Ord` is not implemented for `f64` 59 | | 60 | = help: the following other types implement trait `Ord`: 61 | i128 62 | i16 63 | i32 64 | i64 65 | i8 66 | isize 67 | u128 68 | u16 69 | and $N others 70 | = note: this error originates in the derive macro `Ord` (in Nightly builds, run with -Z macro-backtrace for more info) 71 | 72 | error[E0277]: the trait bound `f64: std::cmp::Eq` is not satisfied 73 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:13:20 74 | | 75 | 12 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord), cfg(test, validators(finite)))] 76 | | -- in this derive macro expansion 77 | 13 | struct FiniteFloat(f64); 78 | | ^^^ the trait `std::cmp::Eq` is not implemented for `f64` 79 | | 80 | = help: the following other types implement trait `std::cmp::Eq`: 81 | i128 82 | i16 83 | i32 84 | i64 85 | i8 86 | isize 87 | u128 88 | u16 89 | and $N others 90 | note: required by a bound in `AssertParamIsEq` 91 | --> $RUST/core/src/cmp.rs 92 | | 93 | | pub struct AssertParamIsEq { 94 | | ^^ required by this bound in `AssertParamIsEq` 95 | = note: this error originates in the derive macro `Eq` (in Nightly builds, run with -Z macro-backtrace for more info) 96 | 97 | error[E0277]: the trait bound `f64: Ord` is not satisfied 98 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:27:21 99 | | 100 | 26 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord), cfg(test, validators(not_nan)))] 101 | | --- in this derive macro expansion 102 | 27 | struct FiniteFloat3(f64); 103 | | ^^^ the trait `Ord` is not implemented for `f64` 104 | | 105 | = help: the following other types implement trait `Ord`: 106 | i128 107 | i16 108 | i32 109 | i64 110 | i8 111 | isize 112 | u128 113 | u16 114 | and $N others 115 | = note: this error originates in the derive macro `Ord` (in Nightly builds, run with -Z macro-backtrace for more info) 116 | 117 | error[E0277]: the trait bound `f64: std::cmp::Eq` is not satisfied 118 | --> tests/ui/fail/float_ord_eq_without_NAN_exclusion.rs:27:21 119 | | 120 | 26 | #[nnn(derive(PartialEq, Eq, PartialOrd, Ord), cfg(test, validators(not_nan)))] 121 | | -- in this derive macro expansion 122 | 27 | struct FiniteFloat3(f64); 123 | | ^^^ the trait `std::cmp::Eq` is not implemented for `f64` 124 | | 125 | = help: the following other types implement trait `std::cmp::Eq`: 126 | i128 127 | i16 128 | i32 129 | i64 130 | i8 131 | isize 132 | u128 133 | u16 134 | and $N others 135 | note: required by a bound in `AssertParamIsEq` 136 | --> $RUST/core/src/cmp.rs 137 | | 138 | | pub struct AssertParamIsEq { 139 | | ^^ required by this bound in `AssertParamIsEq` 140 | = note: this error originates in the derive macro `Eq` (in Nightly builds, run with -Z macro-backtrace for more info) 141 | -------------------------------------------------------------------------------- /tests/ui/pass/float_eq_with_finite.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_imports, reason = "_")] 2 | use nnn::{nnn, NNNewType as _}; 3 | 4 | #[nnn(derive(PartialEq, Eq), validators(finite))] 5 | struct FiniteFloat(f64); 6 | 7 | fn main() {} 8 | 9 | #[nnn(cfg(test, derive(PartialEq, Eq), validators(finite)))] 10 | struct FiniteFloat2(f64); -------------------------------------------------------------------------------- /tests/ui/pass/multiple_validators.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_imports, reason = "_")] 2 | use nnn::{nnn, NNNewType as _}; 3 | 4 | #[nnn(validators(each(finite, min = 0.0_f64)))] 5 | struct FiniteFloatsVec(Vec); 6 | 7 | #[nnn(validators(not_empty, each(finite)))] 8 | struct NonEmptyFiniteFloatsVec(Vec); 9 | 10 | #[nnn(validators(each(finite), not_empty))] 11 | struct FiniteFloatsVecNonEmpty(Vec); 12 | 13 | #[nnn(validators(finite))] 14 | struct FiniteFloat(f64); 15 | 16 | #[nnn(validators(length = 3, not_empty))] 17 | struct AAA(String); 18 | 19 | fn main() {} 20 | -------------------------------------------------------------------------------- /tests/ui/pass/sign_validators_on_all_numbers.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_imports, reason = "_")] 2 | 3 | macro_rules! number_wrapper { 4 | ($($ty:ty),*) => { 5 | paste::paste! { 6 | $( 7 | mod [< signs _ $ty >] { 8 | use nnn::{nnn, NNNewType as _}; 9 | 10 | #[nnn(validators(positive, negative))] 11 | struct NNN($ty); 12 | } 13 | )* 14 | } 15 | } 16 | } 17 | 18 | number_wrapper!( 19 | u8, u16, u32, u64, u128, usize, 20 | i8, i16, i32, i64, i128, isize, 21 | f32, f64 22 | ); 23 | 24 | fn main() {} 25 | -------------------------------------------------------------------------------- /tests/ui/pass/validators_reuse.rs: -------------------------------------------------------------------------------- 1 | #![expect(unused_imports, reason = "_")] 2 | use nnn::{nnn, NNNewType as _}; 3 | 4 | #[nnn(validators(not_empty, each(not_empty)))] 5 | struct StringsVec(Vec); 6 | 7 | #[nnn(validators(not_empty, each(not_empty, each(finite))))] 8 | struct VecFiniteFloatVec(Vec>); 9 | 10 | fn main() {} 11 | -------------------------------------------------------------------------------- /tests/validators/custom.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | use core::{num, str::FromStr as _}; 3 | /* Crate imports */ 4 | use nnn::{nnn, NNNewType as _}; 5 | /* Dependencies */ 6 | use rstest::rstest; 7 | 8 | #[nnn( 9 | validators( 10 | custom(with = f64::from_str, error = num::ParseFloatError) 11 | ) 12 | )] 13 | struct ValidatedFnPathFloatString(String); 14 | 15 | #[rstest] 16 | #[case("4.0")] 17 | fn validated_fn_path_float_string_valid(#[case] input: &str) { 18 | ValidatedFnPathFloatString::try_new(input.to_owned()).unwrap(); 19 | } 20 | 21 | #[rstest] 22 | #[case("not a float")] 23 | fn validated_fn_path_float_string_valid_invalid(#[case] input: &str) { 24 | assert!(matches!( 25 | ValidatedFnPathFloatString::try_new(input.to_owned()), 26 | Err(ValidatedFnPathFloatStringError::NumParsefloaterror(_)) 27 | )); 28 | } 29 | 30 | #[nnn( 31 | validators( 32 | custom(with = |str: &String| f64::from_str(str), error = num::ParseFloatError) 33 | ) 34 | )] 35 | struct ValidatedClosureFloatString(String); 36 | 37 | #[rstest] 38 | #[case("4.0")] 39 | fn validated_closure_float_string_valid(#[case] input: &str) { 40 | ValidatedClosureFloatString::try_new(input.to_owned()).unwrap(); 41 | } 42 | 43 | #[rstest] 44 | #[case("not a float")] 45 | fn validated_closure_float_string_invalid(#[case] input: &str) { 46 | assert!(matches!( 47 | ValidatedClosureFloatString::try_new(input.to_owned()), 48 | Err(ValidatedClosureFloatStringError::NumParsefloaterror(_)) 49 | )); 50 | } 51 | 52 | #[nnn( 53 | validators( 54 | custom(with = { f64::from_str(&value) }, error = num::ParseFloatError) 55 | ) 56 | )] 57 | struct ValidatedBlockFloatString(String); 58 | 59 | #[rstest] 60 | #[case("4.0")] 61 | fn validated_block_float_string_valid(#[case] input: &str) { 62 | ValidatedBlockFloatString::try_new(input.to_owned()).unwrap(); 63 | } 64 | 65 | #[rstest] 66 | #[case("not a float")] 67 | fn validated_block_float_string_invalid(#[case] input: &str) { 68 | assert!(matches!( 69 | ValidatedBlockFloatString::try_new(input.to_owned()), 70 | Err(ValidatedBlockFloatStringError::NumParsefloaterror(_)) 71 | )); 72 | } 73 | -------------------------------------------------------------------------------- /tests/validators/each.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(each(finite, min = 0.0_f64)))] 7 | struct FiniteFloatsVec(Vec); 8 | 9 | #[rstest] 10 | #[case(vec![])] 11 | #[case(vec![0.1_f64, 1.5_f64, 2.7_f64])] 12 | fn valid_each_vec(#[case] input: Vec) { 13 | FiniteFloatsVec::try_new(input).unwrap(); 14 | } 15 | 16 | #[rstest] 17 | #[case(vec![-0.1_f64, 1.5_f64, 2.7_f64])] 18 | #[case(vec![0.1_f64, f64::INFINITY, 2.7_f64])] 19 | fn invalid_each_vec(#[case] input: Vec) { 20 | assert!(matches!( 21 | FiniteFloatsVec::try_new(input), 22 | Err(FiniteFloatsVecError::Each(_, _)) 23 | )); 24 | } 25 | 26 | 27 | #[nnn(validators(not_empty, each(not_empty, each(finite))))] 28 | struct VecFiniteFloatVec(Vec>); 29 | 30 | #[rstest] 31 | #[case(vec![vec![2.7_f64]])] 32 | #[case(vec![vec![0.1_f64, 1.5_f64], vec![2.7_f64, 3.8_f64, 3.0_f64]])] 33 | fn valid_vec_of_vecs_of_finite_floats(#[case] input: Vec>) { 34 | VecFiniteFloatVec::try_new(input).unwrap(); 35 | } 36 | 37 | #[rstest] 38 | #[case(vec![vec![]])] 39 | #[case(vec![vec![-0.1_f64, f64::INFINITY]])] 40 | #[case(vec![vec![-0.1_f64], vec![]])] 41 | fn invalid_vec_of_finite_floats(#[case] input: Vec>) { 42 | assert!(matches!( 43 | VecFiniteFloatVec::try_new(input), 44 | Err(VecFiniteFloatVecError::Each(_, _)) 45 | )); 46 | } 47 | -------------------------------------------------------------------------------- /tests/validators/exactly.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(exactly = 42_i32))] 7 | struct ExactInt(i32); 8 | 9 | #[rstest] 10 | #[case(42_i32)] 11 | fn valid_exact_signed_int(#[case] input: i32) { 12 | ExactInt::try_new(input).unwrap(); 13 | } 14 | 15 | #[rstest] 16 | #[case(41_i32)] 17 | #[case(43_i32)] 18 | fn invalid_exact_signed_int(#[case] input: i32) { 19 | assert!(matches!( 20 | ExactInt::try_new(input), 21 | Err(ExactIntError::Exactly) 22 | )); 23 | } 24 | 25 | #[nnn(validators(exactly = 3.00_f64))] 26 | struct ExactFloat(f64); 27 | 28 | #[rstest] 29 | #[case(3.00_f64)] 30 | fn valid_exact_float(#[case] input: f64) { 31 | ExactFloat::try_new(input).unwrap(); 32 | } 33 | 34 | #[rstest] 35 | #[case(3.01_f64)] 36 | #[case(2.99_f64)] 37 | fn invalid_exact_float(#[case] input: f64) { 38 | assert!(matches!( 39 | ExactFloat::try_new(input), 40 | Err(ExactFloatError::Exactly) 41 | )); 42 | } 43 | 44 | #[nnn(validators(exactly = (3.00_f64, 12_i32)))] 45 | struct ExactTuple((f64, i32)); 46 | 47 | #[rstest] 48 | #[case((3.00_f64, 12_i32))] 49 | fn valid_exact_tuple(#[case] input: (f64, i32)) { 50 | ExactTuple::try_new(input).unwrap(); 51 | } 52 | 53 | #[rstest] 54 | #[case((3.00_f64, 13_i32))] 55 | #[case((3.01_f64, 12_i32))] 56 | fn invalid_exact_tuple(#[case] input: (f64, i32)) { 57 | assert!(matches!( 58 | ExactTuple::try_new(input), 59 | Err(ExactTupleError::Exactly) 60 | )); 61 | } 62 | 63 | #[nnn(validators(exactly = [3.00_f64, 2.00_f64]))] 64 | struct ExactArray([f64; 2]); 65 | 66 | #[rstest] 67 | #[case([3.00_f64, 2.00_f64])] 68 | fn valid_exact_array(#[case] input: [f64; 2]) { 69 | ExactArray::try_new(input).unwrap(); 70 | } 71 | 72 | #[rstest] 73 | #[case([3.00_f64, 2.01_f64])] 74 | #[case([4.00_f64, 2.00_f64])] 75 | fn invalid_exact_array(#[case] input: [f64; 2]) { 76 | assert!(matches!( 77 | ExactArray::try_new(input), 78 | Err(ExactArrayError::Exactly) 79 | )); 80 | } 81 | 82 | #[nnn(validators(exactly = "HEY"))] 83 | struct ExactString<'str>(&'str str); 84 | 85 | #[rstest] 86 | #[case("HEY")] 87 | fn valid_exact_string(#[case] input: &str) { 88 | ExactString::try_new(input).unwrap(); 89 | } 90 | 91 | #[rstest] 92 | #[case("HEY2")] 93 | #[case("hey")] 94 | fn invalid_exact_string(#[case] input: &'static str) { 95 | assert!(matches!( 96 | ExactString::try_new(input), 97 | Err(ExactStringError::Exactly) 98 | )); 99 | } 100 | -------------------------------------------------------------------------------- /tests/validators/finite.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(finite))] 7 | struct FiniteFloat(f64); 8 | 9 | #[rstest] 10 | #[case(f64::MIN)] 11 | #[case(-10.0_f64)] 12 | #[case(-0.0_f64)] 13 | #[case(0.0_f64)] 14 | #[case(10.0_f64)] 15 | #[case(f64::MAX)] 16 | fn valid_finite_float(#[case] input: f64) { 17 | FiniteFloat::try_new(input).unwrap(); 18 | } 19 | 20 | #[rstest] 21 | #[case(f64::NEG_INFINITY)] 22 | #[case(f64::NAN)] 23 | #[case(f64::INFINITY)] 24 | fn invalid_finite_float(#[case] input: f64) { 25 | assert!(matches!( 26 | FiniteFloat::try_new(input), 27 | Err(FiniteFloatError::Finite) 28 | )); 29 | } 30 | -------------------------------------------------------------------------------- /tests/validators/length.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(length = 5))] 7 | struct ExactLengthStr<'str>(&'str str); 8 | 9 | #[rstest] 10 | #[case("abcde")] 11 | fn valid_exact_length_str(#[case] input: &str) { 12 | ExactLengthStr::try_new(input).unwrap(); 13 | } 14 | 15 | #[rstest] 16 | #[case("")] 17 | #[case("abc")] 18 | #[case("abcdef")] 19 | fn invalid_exact_length_str(#[case] input: &str) { 20 | assert!(matches!( 21 | ExactLengthStr::try_new(input), 22 | Err(ExactLengthStrError::Length) 23 | )); 24 | } 25 | -------------------------------------------------------------------------------- /tests/validators/max.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | // Integer max (not inclusive) 7 | #[nnn(validators(max = 42_i32))] 8 | struct MaxInt(i32); 9 | 10 | #[rstest] 11 | #[case(41_i32)] 12 | fn valid_max_signed_int(#[case] input: i32) { 13 | MaxInt::try_new(input).unwrap(); 14 | } 15 | 16 | #[rstest] 17 | #[case(42_i32)] 18 | #[case(43_i32)] 19 | fn invalid_max_signed_int(#[case] input: i32) { 20 | assert!(matches!( 21 | MaxInt::try_new(input), 22 | Err(MaxIntError::Max) 23 | )); 24 | } 25 | 26 | // Float max (not inclusive) 27 | #[nnn(validators(max = 3.00_f64))] 28 | struct MaxFloat(f64); 29 | 30 | #[rstest] 31 | #[case(2.99_f64)] 32 | fn valid_max_float(#[case] input: f64) { 33 | MaxFloat::try_new(input).unwrap(); 34 | } 35 | 36 | #[rstest] 37 | #[case(3.00_f64)] 38 | #[case(3.01_f64)] 39 | fn invalid_max_float(#[case] input: f64) { 40 | assert!(matches!( 41 | MaxFloat::try_new(input), 42 | Err(MaxFloatError::Max) 43 | )); 44 | } 45 | -------------------------------------------------------------------------------- /tests/validators/max_length.rs: -------------------------------------------------------------------------------- 1 | 2 | /* Crate imports */ 3 | use nnn::{nnn, NNNewType as _}; 4 | /* Dependencies */ 5 | use rstest::rstest; 6 | 7 | #[nnn(validators(max_length = 5))] 8 | struct MaxStr<'str>(&'str str); 9 | 10 | #[rstest] 11 | #[case("")] 12 | #[case("abcd")] 13 | fn valid_max_str(#[case] input: &str) { 14 | MaxStr::try_new(input).unwrap(); 15 | } 16 | 17 | #[rstest] 18 | #[case("abcde")] 19 | #[case("abcdefgh")] 20 | fn invalid_max_str(#[case] input: &str) { 21 | assert!(matches!( 22 | MaxStr::try_new(input), 23 | Err(MaxStrError::MaxLength) 24 | )); 25 | } 26 | -------------------------------------------------------------------------------- /tests/validators/max_length_or_eq.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(max_length_or_eq = 5))] 7 | struct MaxLengthOrEqStr<'str>(&'str str); 8 | 9 | #[rstest] 10 | #[case("")] 11 | #[case("abcde")] 12 | fn valid_max_length_or_eq_str(#[case] input: &str) { 13 | MaxLengthOrEqStr::try_new(input).unwrap(); 14 | } 15 | 16 | #[rstest] 17 | #[case("abcdef")] 18 | #[case("abcdefgh")] 19 | fn invalid_max_length_or_eq_str(#[case] input: &str) { 20 | assert!(matches!( 21 | MaxLengthOrEqStr::try_new(input), 22 | Err(MaxLengthOrEqStrError::MaxLengthOrEq) 23 | )); 24 | } 25 | -------------------------------------------------------------------------------- /tests/validators/max_or_eq.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | // Integer max_or_eq 7 | #[nnn(validators(max_or_eq = 42_i32))] 8 | struct MaxOrEqInt(i32); 9 | 10 | #[rstest] 11 | #[case(42_i32)] 12 | #[case(41_i32)] 13 | fn valid_max_or_eq_signed_int(#[case] input: i32) { 14 | MaxOrEqInt::try_new(input).unwrap(); 15 | } 16 | 17 | #[rstest] 18 | #[case(43_i32)] 19 | fn invalid_max_or_eq_signed_int(#[case] input: i32) { 20 | assert!(matches!( 21 | MaxOrEqInt::try_new(input), 22 | Err(MaxOrEqIntError::MaxOrEq) 23 | )); 24 | } 25 | 26 | // Float max_or_eq 27 | #[nnn(validators(max_or_eq = 3.00_f64))] 28 | struct MaxOrEqFloat(f64); 29 | 30 | #[rstest] 31 | #[case(3.00_f64)] 32 | #[case(2.99_f64)] 33 | fn valid_max_or_eq_float(#[case] input: f64) { 34 | MaxOrEqFloat::try_new(input).unwrap(); 35 | } 36 | 37 | #[rstest] 38 | #[case(3.01_f64)] 39 | fn invalid_max_or_eq_float(#[case] input: f64) { 40 | assert!(matches!( 41 | MaxOrEqFloat::try_new(input), 42 | Err(MaxOrEqFloatError::MaxOrEq) 43 | )); 44 | } 45 | -------------------------------------------------------------------------------- /tests/validators/min.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | // Integer min 7 | #[nnn(validators(min = 42_i32))] 8 | struct MinInt(i32); 9 | 10 | #[rstest] 11 | #[case(43_i32)] 12 | fn valid_min_signed_int(#[case] input: i32) { 13 | MinInt::try_new(input).unwrap(); 14 | } 15 | 16 | #[rstest] 17 | #[case(42_i32)] 18 | #[case(41_i32)] 19 | fn invalid_min_signed_int(#[case] input: i32) { 20 | assert!(matches!( 21 | MinInt::try_new(input), 22 | Err(MinIntError::Min) 23 | )); 24 | } 25 | 26 | // Float min 27 | #[nnn(validators(min = 3.00_f64))] 28 | struct MinFloat(f64); 29 | 30 | #[rstest] 31 | #[case(3.01_f64)] 32 | fn valid_min_float(#[case] input: f64) { 33 | MinFloat::try_new(input).unwrap(); 34 | } 35 | 36 | #[rstest] 37 | #[case(3.00_f64)] 38 | #[case(2.99_f64)] 39 | fn invalid_min_float(#[case] input: f64) { 40 | assert!(matches!( 41 | MinFloat::try_new(input), 42 | Err(MinFloatError::Min) 43 | )); 44 | } 45 | -------------------------------------------------------------------------------- /tests/validators/min_length.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(min_length = 3))] 7 | struct MinLengthStr<'str>(&'str str); 8 | 9 | #[rstest] 10 | #[case("abcde")] 11 | fn valid_min_length_str(#[case] input: &str) { 12 | MinLengthStr::try_new(input).unwrap(); 13 | } 14 | 15 | #[rstest] 16 | #[case("")] 17 | #[case("abc")] 18 | fn invalid_min_length_str(#[case] input: &str) { 19 | assert!(matches!( 20 | MinLengthStr::try_new(input), 21 | Err(MinLengthStrError::MinLength) 22 | )); 23 | } 24 | -------------------------------------------------------------------------------- /tests/validators/min_length_or_eq.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(min_length_or_eq = 3))] 7 | struct MinLengthOrEqStr<'str>(&'str str); 8 | 9 | #[rstest] 10 | #[case("abc")] 11 | #[case("abcd")] 12 | fn valid_min_length_or_eq_str(#[case] input: &str) { 13 | MinLengthOrEqStr::try_new(input).unwrap(); 14 | } 15 | 16 | #[rstest] 17 | #[case("")] 18 | #[case("ab")] 19 | fn invalid_min_length_or_eq_str(#[case] input: &str) { 20 | assert!(matches!( 21 | MinLengthOrEqStr::try_new(input), 22 | Err(MinLengthOrEqStrError::MinLengthOrEq) 23 | )); 24 | } 25 | -------------------------------------------------------------------------------- /tests/validators/min_or_eq.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | // Integer min_or_eq 7 | #[nnn(validators(min_or_eq = 42_i32))] 8 | struct MinOrEqInt(i32); 9 | 10 | #[rstest] 11 | #[case(42_i32)] 12 | #[case(43_i32)] 13 | fn valid_min_or_eq_signed_int(#[case] input: i32) { 14 | MinOrEqInt::try_new(input).unwrap(); 15 | } 16 | 17 | #[rstest] 18 | #[case(41_i32)] 19 | fn invalid_min_or_eq_signed_int(#[case] input: i32) { 20 | assert!(matches!( 21 | MinOrEqInt::try_new(input), 22 | Err(MinOrEqIntError::MinOrEq) 23 | )); 24 | } 25 | 26 | // Float min_or_eq 27 | #[nnn(validators(min_or_eq = 3.00_f64))] 28 | struct MinOrEqFloat(f64); 29 | 30 | #[rstest] 31 | #[case(3.00_f64)] 32 | #[case(3.01_f64)] 33 | fn valid_min_or_eq_float(#[case] input: f64) { 34 | MinOrEqFloat::try_new(input).unwrap(); 35 | } 36 | 37 | #[rstest] 38 | #[case(2.99_f64)] 39 | fn invalid_min_or_eq_float(#[case] input: f64) { 40 | assert!(matches!( 41 | MinOrEqFloat::try_new(input), 42 | Err(MinOrEqFloatError::MinOrEq) 43 | )); 44 | } 45 | -------------------------------------------------------------------------------- /tests/validators/negative.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | use crate::utils::sign_tests; 4 | 5 | sign_tests!(negative, 6 | // i_ suite 7 | i8, valids = [-42_i8], invalids = [0_i8, 1_i8], 8 | i16, valids = [-42_i16], invalids = [0_i16, 1_i16], 9 | i32, valids = [-42_i32], invalids = [0_i32, 1_i32], 10 | i64, valids = [-42_i64], invalids = [0_i64, 1_i64], 11 | i128, valids = [-42_i128], invalids = [0_i128, 1_i128], 12 | isize, valids = [-42_isize], invalids = [0_isize, 1_isize], 13 | // f_ suite 14 | f32, valids = [-3.0_f32], invalids = [0.0_f32, -0.0_f32, 3.0_f32, f32::NAN], 15 | f64, valids = [-3.0_f64], invalids = [0.0_f64, -0.0_f64, 3.0_f64, f64::NAN] 16 | ); -------------------------------------------------------------------------------- /tests/validators/not_empty.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(not_empty))] 7 | struct NonEmptyStr<'str>(&'str str); 8 | 9 | #[rstest] 10 | #[case("Hello")] 11 | #[case("A")] 12 | fn valid_not_empty_str(#[case] input: &str) { 13 | NonEmptyStr::try_new(input).unwrap(); 14 | } 15 | 16 | #[rstest] 17 | #[case("")] 18 | fn invalid_not_empty_str(#[case] input: &str) { 19 | assert!(matches!( 20 | NonEmptyStr::try_new(input), 21 | Err(NonEmptyStrError::NotEmpty) 22 | )); 23 | } 24 | -------------------------------------------------------------------------------- /tests/validators/not_infinite.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(not_infinite))] 7 | struct NonInfiniteFloat(f64); 8 | 9 | #[rstest] 10 | #[case(f64::MIN)] 11 | #[case(-10.0_f64)] 12 | #[case(-0.0_f64)] 13 | #[case(f64::NAN)] 14 | #[case(0.0_f64)] 15 | #[case(10.0_f64)] 16 | #[case(f64::MAX)] 17 | fn valid_not_nan_test(#[case] input: f64) { 18 | NonInfiniteFloat::try_new(input).unwrap(); 19 | } 20 | 21 | #[rstest] 22 | #[case(f64::INFINITY)] 23 | #[case(f64::NEG_INFINITY)] 24 | fn invalid_not_nan_test(#[case] input: f64) { 25 | assert!(matches!( 26 | NonInfiniteFloat::try_new(input), 27 | Err(NonInfiniteFloatError::NotInfinite) 28 | )); 29 | } 30 | -------------------------------------------------------------------------------- /tests/validators/not_nan.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | /* Dependencies */ 4 | use rstest::rstest; 5 | 6 | #[nnn(validators(not_nan))] 7 | struct NonNanFloat(f64); 8 | 9 | #[rstest] 10 | #[case(f64::NEG_INFINITY)] 11 | #[case(f64::MIN)] 12 | #[case(-10.0_f64)] 13 | #[case(-0.0_f64)] 14 | #[case(0.0_f64)] 15 | #[case(10.0_f64)] 16 | #[case(f64::MAX)] 17 | #[case(f64::INFINITY)] 18 | fn valid_not_nan_test(#[case] input: f64) { 19 | NonNanFloat::try_new(input).unwrap(); 20 | } 21 | 22 | #[rstest] 23 | #[case(f64::NAN)] 24 | fn invalid_not_nan_test(#[case] input: f64) { 25 | assert!(matches!( 26 | NonNanFloat::try_new(input), 27 | Err(NonNanFloatError::NotNAN) 28 | )); 29 | } 30 | -------------------------------------------------------------------------------- /tests/validators/positive.rs: -------------------------------------------------------------------------------- 1 | /* Crate imports */ 2 | use nnn::{nnn, NNNewType as _}; 3 | use crate::utils::sign_tests; 4 | 5 | sign_tests!(positive, 6 | // i_ suite 7 | i8, valids = [42_i8], invalids = [0_i8, -1_i8], 8 | i16, valids = [42_i16], invalids = [0_i16, -1_i16], 9 | i32, valids = [42_i32], invalids = [0_i32, -1_i32], 10 | i64, valids = [42_i64], invalids = [0_i64, -1_i64], 11 | i128, valids = [42_i128], invalids = [0_i128, -1_i128], 12 | isize, valids = [42_isize], invalids = [0_isize, -1_isize], 13 | // f_ suite 14 | f32, valids = [3.0_f32], invalids = [0.0_f32, -0.0_f32, -3.0_f32, f32::NAN], 15 | f64, valids = [3.0_f64], invalids = [0.0_f64, -0.0_f64, -3.0_f64, f64::NAN] 16 | ); 17 | -------------------------------------------------------------------------------- /tests/validators/predicate.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | use core::{num, str::FromStr as _}; 3 | /* Crate imports */ 4 | use nnn::{nnn, NNNewType as _}; 5 | /* Dependencies */ 6 | use rstest::rstest; 7 | 8 | #[nnn( 9 | validators( 10 | predicate(with = Option::is_some, error_name = NotSome) 11 | ) 12 | )] 13 | struct ValidatedFnPathOption(Option<()>); 14 | 15 | #[rstest] 16 | #[case(Some(()))] 17 | fn validated_fn_path_option_valid(#[case] input: Option<()>) { 18 | ValidatedFnPathOption::try_new(input).unwrap(); 19 | } 20 | 21 | #[rstest] 22 | #[case(None)] 23 | fn validated_fn_path_option_invalid(#[case] input: Option<()>) { 24 | assert!(matches!( 25 | ValidatedFnPathOption::try_new(input), 26 | Err(ValidatedFnPathOptionError::NotSome) 27 | )); 28 | } 29 | 30 | #[nnn( 31 | validators( 32 | predicate(with = |opt: &Option<()>| opt.is_some()) 33 | ) 34 | )] 35 | struct ValidatedClosureOption(Option<()>); 36 | 37 | #[rstest] 38 | #[case(Some(()))] 39 | fn validated_closure_option_valid(#[case] input: Option<()>) { 40 | ValidatedClosureOption::try_new(input).unwrap(); 41 | } 42 | 43 | #[rstest] 44 | #[case(None)] 45 | fn validated_closure_option_invalid(#[case] input: Option<()>) { 46 | assert!(matches!( 47 | ValidatedClosureOption::try_new(input), 48 | Err(ValidatedClosureOptionError::Predicate) 49 | )); 50 | } 51 | 52 | #[nnn( 53 | validators( 54 | predicate(with = { value.is_some() }) 55 | ) 56 | )] 57 | struct ValidatedBlockOption(Option<()>); 58 | 59 | #[rstest] 60 | #[case(Some(()))] 61 | fn validated_block_float_string_valid(#[case] input: Option<()>) { 62 | ValidatedBlockOption::try_new(input).unwrap(); 63 | } 64 | 65 | #[rstest] 66 | #[case(None)] 67 | fn validated_block_float_string_invalid(#[case] input: Option<()>) { 68 | assert!(matches!( 69 | ValidatedBlockOption::try_new(input), 70 | Err(ValidatedBlockOptionError::Predicate) 71 | )); 72 | } 73 | -------------------------------------------------------------------------------- /tests/validators/regex.rs: -------------------------------------------------------------------------------- 1 | /* Built-in imports */ 2 | use std::sync::LazyLock; 3 | /* Crate imports */ 4 | use nnn::{nnn, NNNewType as _}; 5 | /* Dependencies */ 6 | use rstest::rstest; 7 | 8 | #[nnn(validators(regex = r"^\d{3}-\d{2}-\d{4}$"))] 9 | struct RegexLiteralStr<'str>(&'str str); 10 | 11 | #[rstest] 12 | #[case("123-45-6789")] 13 | fn valid_regex_literal_str(#[case] input: &str) { 14 | RegexLiteralStr::try_new(input).unwrap(); 15 | } 16 | 17 | #[rstest] 18 | #[case("1234-56-7890")] 19 | #[case("123-45-678")] 20 | fn invalid_regex_literal_str(#[case] input: &str) { 21 | assert!(matches!( 22 | RegexLiteralStr::try_new(input), 23 | Err(RegexLiteralStrError::Regex) 24 | )); 25 | } 26 | 27 | // Regex with a variable 28 | #[expect(clippy::expect_used, reason = "Test, so this is acceptable.")] 29 | static SSN_REGEX: LazyLock = LazyLock::new(|| { 30 | regex::Regex::try_from(r"^\d{3}-\d{2}-\d{4}$").expect("Invalid test regex") 31 | }); 32 | 33 | #[nnn(validators(regex = SSN_REGEX))] 34 | struct RegexVariableStr<'str>(&'str str); 35 | 36 | #[rstest] 37 | #[case("123-45-6789")] 38 | fn valid_regex_variable_str(#[case] input: &str) { 39 | RegexVariableStr::try_new(input).unwrap(); 40 | } 41 | 42 | #[rstest] 43 | #[case("abc-def-ghij")] 44 | #[case("123-456-789")] 45 | fn invalid_regex_variable_str(#[case] input: &str) { 46 | assert!(matches!( 47 | RegexVariableStr::try_new(input), 48 | Err(RegexVariableStrError::Regex) 49 | )); 50 | } 51 | --------------------------------------------------------------------------------