From 738a990237a453fa7d025a897635adf192f326a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Tue, 23 Dec 2025 20:28:02 +0100 Subject: [PATCH] feat: add range validation --- .pre-commit-config.yaml | 2 +- Cargo.lock | 502 +++- Cargo.toml | 3 + book/src/SUMMARY.md | 1 + book/src/getting-started.md | 2 +- book/src/installation.md | 4 + book/src/introduction.md | 1 + book/src/validations/README.md | 1 + book/src/validations/range.md | 123 + packages/fortifier-macros/Cargo.toml | 9 +- .../fortifier-macros/src/validate/field.rs | 30 +- packages/fortifier-macros/src/validation.rs | 4 +- packages/fortifier-macros/src/validations.rs | 2 + .../src/validations/custom.rs | 4 +- .../src/validations/email_address.rs | 4 +- .../src/validations/length.rs | 4 +- .../src/validations/nested.rs | 4 +- .../src/validations/phone_number.rs | 4 +- .../fortifier-macros/src/validations/range.rs | 102 + .../fortifier-macros/src/validations/regex.rs | 4 +- .../fortifier-macros/src/validations/url.rs | 4 +- .../tests/integrations/serde_pass.rs | 2 +- .../tests/validations/length/options_pass.rs | 8 +- .../tests/validations/length/types_pass.rs | 26 +- .../validations/range/conflict_max_fail.rs | 9 + .../range/conflict_max_fail.stderr | 5 + .../validations/range/conflict_min_fail.rs | 9 + .../range/conflict_min_fail.stderr | 5 + .../tests/validations/range/options_pass.rs | 70 + .../tests/validations/range/unknown_fail.rs | 9 + .../validations/range/unknown_fail.stderr | 5 + packages/fortifier/Cargo.toml | 16 +- packages/fortifier/src/validations.rs | 2 + packages/fortifier/src/validations/length.rs | 146 +- packages/fortifier/src/validations/range.rs | 2078 +++++++++++++++++ packages/fortifier/tests/serde.rs | 6 +- 36 files changed, 3069 insertions(+), 141 deletions(-) create mode 100644 book/src/validations/range.md create mode 100644 packages/fortifier-macros/src/validations/range.rs create mode 100644 packages/fortifier-macros/tests/validations/range/conflict_max_fail.rs create mode 100644 packages/fortifier-macros/tests/validations/range/conflict_max_fail.stderr create mode 100644 packages/fortifier-macros/tests/validations/range/conflict_min_fail.rs create mode 100644 packages/fortifier-macros/tests/validations/range/conflict_min_fail.stderr create mode 100644 packages/fortifier-macros/tests/validations/range/options_pass.rs create mode 100644 packages/fortifier-macros/tests/validations/range/unknown_fail.rs create mode 100644 packages/fortifier-macros/tests/validations/range/unknown_fail.stderr create mode 100644 packages/fortifier/src/validations/range.rs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 143f9a5..a7d66bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: args: ['--all-targets', '--all-features', '--', '-D', 'warnings'] - repo: https://github.com/EmbarkStudios/cargo-deny - rev: 0.18.5 + rev: 0.18.9 hooks: - id: cargo-deny diff --git a/Cargo.lock b/Cargo.lock index 4a23b3e..5cd5018 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -11,12 +22,33 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "axum" version = "0.8.7" @@ -78,24 +110,111 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cc" +version = "1.2.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "convert_case" version = "0.9.0" @@ -105,6 +224,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "darling" version = "0.20.11" @@ -126,7 +251,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn", + "syn 2.0.110", ] [[package]] @@ -137,7 +262,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -154,7 +279,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -178,6 +303,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + [[package]] name = "fnv" version = "1.0.7" @@ -197,16 +328,19 @@ dependencies = [ name = "fortifier" version = "0.0.1" dependencies = [ + "chrono", "email_address", "fortifier-macros", "indexmap", "phonenumber", "pretty_assertions", "regex", + "rust_decimal", "serde", "serde_json", "url", "utoipa", + "uuid", ] [[package]] @@ -245,6 +379,7 @@ dependencies = [ name = "fortifier-macros" version = "0.0.1" dependencies = [ + "chrono", "convert_case", "email_address", "fortifier", @@ -255,13 +390,21 @@ dependencies = [ "proc-macro2", "quote", "regex", + "rust_decimal", "serde", "serde_json", - "syn", + "syn 2.0.110", "trybuild", "url", + "uuid", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -295,6 +438,17 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -313,6 +467,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashbrown" version = "0.16.0" @@ -407,6 +570,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -522,7 +709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", "serde", "serde_core", ] @@ -621,6 +808,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -686,6 +882,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -724,7 +929,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -736,6 +941,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -760,6 +985,42 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + [[package]] name = "regex" version = "1.12.2" @@ -807,6 +1068,71 @@ version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "rust_decimal_macros", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8c0cb48f413ebe24dc2d148788e0efbe09ba3e011d9277162f2eaf8e1069a3" +dependencies = [ + "quote", + "syn 2.0.110", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -819,6 +1145,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "serde" version = "1.0.228" @@ -846,7 +1178,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -894,6 +1226,18 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "smallvec" version = "1.15.1" @@ -941,7 +1285,18 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.110", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -969,9 +1324,15 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-triple" version = "1.0.0" @@ -1013,7 +1374,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1024,7 +1385,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1037,6 +1398,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -1059,7 +1435,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -1240,7 +1616,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn", + "syn 2.0.110", "uuid", ] @@ -1262,7 +1638,7 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ - "getrandom", + "getrandom 0.3.4", "js-sys", "serde_core", "wasm-bindgen", @@ -1295,9 +1671,15 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1345,7 +1727,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.110", "wasm-bindgen-shared", ] @@ -1367,12 +1749,65 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -1477,6 +1912,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yansi" version = "1.0.1" @@ -1502,10 +1946,30 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "zerofrom" version = "0.1.6" @@ -1523,7 +1987,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", "synstructure", ] @@ -1557,5 +2021,5 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] diff --git a/Cargo.toml b/Cargo.toml index ce47bb0..a5231e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ repository = "https://github.com/RustForWeb/fortifier" version = "0.0.1" [workspace.dependencies] +chrono = "0.4.42" email_address = { version = "0.2.9", default-features = false } fortifier = { path = "./packages/fortifier", version = "0.0.1" } fortifier-macros = { path = "./packages/fortifier-macros", version = "0.0.1" } @@ -17,11 +18,13 @@ indexmap = "2.12.0" phonenumber = "0.3.7" pretty_assertions = "1.4.1" regex = "1.12.2" +rust_decimal = "1.39.0" serde = "1.0.228" serde_json = "1.0.145" tokio = "1.48.0" url = "2.5.7" utoipa = "5.4.0" +uuid = "1.19.0" [workspace.lints.rust] unsafe_code = "deny" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index dd1f01e..157c723 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -10,6 +10,7 @@ - [Email Address](./validations/email-address.md) - [Length](./validations/length.md) - [Phone Number](./validations/phone-number.md) + - [Range](./validations/range.md) - [Regular Expression](./validations/regular-expression.md) - [URL](./validations/url.md) - [Integrations]() diff --git a/book/src/getting-started.md b/book/src/getting-started.md index 115124f..0eff1f2 100644 --- a/book/src/getting-started.md +++ b/book/src/getting-started.md @@ -106,7 +106,7 @@ fn main() { LengthError::Min { code: LengthErrorCode, min: 1, - length: 0, + value: 0, } ), ])), diff --git a/book/src/installation.md b/book/src/installation.md index 4bfa7d8..eeef82c 100644 --- a/book/src/installation.md +++ b/book/src/installation.md @@ -17,7 +17,11 @@ cargo add fortifier ### Types +- `all-types` - Enable all features below. +- `chrono` - Support for the `DateTime`, `NaiveDate`, `NaiveDateTime`, `NaiveTime` and `TimeDelta` types from the [`chrono`](https://docs.rs/chrono/latest/chrono/) crate. +- `decimal` - Support for the `Decimal` type from the [`rust_decimal`](https://docs.rs/rust_decimal/latest/rust_decimal/) crate. - `indexmap` - Support for the `IndexMap` and `IndexSet` types from the [`indexmap`](https://docs.rs/indexmap/latest/indexmap/) crate. +- `uuid` - Support for the `Uuid` type from the [`uuid`](https://docs.rs/uuid/latest/uuid/) crate. ### Validations diff --git a/book/src/introduction.md b/book/src/introduction.md index 1b93cbe..c54b26e 100644 --- a/book/src/introduction.md +++ b/book/src/introduction.md @@ -9,6 +9,7 @@ Schema validation. - Email address - Length - Phone number + - Range - Regular expression - URL - Support for Serde & Utoipa diff --git a/book/src/validations/README.md b/book/src/validations/README.md index 50591da..aea8714 100644 --- a/book/src/validations/README.md +++ b/book/src/validations/README.md @@ -3,5 +3,6 @@ - [Email Address](./email-address.md) - [Length](./length.md) - [Phone Number](./phone-number.md) +- [Range](./range.md) - [Regular Expression](./regular-expression.md) - [URL](./url.md) diff --git a/book/src/validations/range.md b/book/src/validations/range.md new file mode 100644 index 0000000..ca0343c --- /dev/null +++ b/book/src/validations/range.md @@ -0,0 +1,123 @@ +# Range + +Validate a value is within a range. + +```rust +# extern crate fortifier; +# +use fortifier::Validate; + +#[derive(Validate)] +struct Object { + #[validate(range(exclusive_min = 0.0, max = 100.0))] + height: f64, +} +``` + +## Types + +### Boolean + +- [`bool`](https://doc.rust-lang.org/std/primitive.bool.html) + +### Number + +- [`u8`](https://doc.rust-lang.org/std/primitive.u8.html) +- [`u16`](https://doc.rust-lang.org/std/primitive.u16.html) +- [`u32`](https://doc.rust-lang.org/std/primitive.u32.html) +- [`u64`](https://doc.rust-lang.org/std/primitive.u64.html) +- [`u128`](https://doc.rust-lang.org/std/primitive.u128.html) +- [`usize`](https://doc.rust-lang.org/std/primitive.usize.html) +- [`i8`](https://doc.rust-lang.org/std/primitive.i8.html) +- [`i16`](https://doc.rust-lang.org/std/primitive.i16.html) +- [`i32`](https://doc.rust-lang.org/std/primitive.i32.html) +- [`i64`](https://doc.rust-lang.org/std/primitive.i64.html) +- [`i128`](https://doc.rust-lang.org/std/primitive.i128.html) +- [`isize`](https://doc.rust-lang.org/std/primitive.isize.html) +- [`f32`](https://doc.rust-lang.org/std/primitive.f32.html) +- [`f64`](https://doc.rust-lang.org/std/primitive.f64.html) + +### Character + +- [`char`](https://doc.rust-lang.org/std/primitive.char.html) + +### String + +- [`str`](https://doc.rust-lang.org/std/primitive.str.html) +- [`String`](https://doc.rust-lang.org/std/string/struct.String.html) + +### Other + +- [`DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) (requires feature `chrono`) +- [`NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html) (requires feature `chrono`) +- [`NaiveDateTime`](https://docs.rs/chrono/latest/chrono/struct.NaiveDateTime.html) (requires feature `chrono`) +- [`NaiveTime`](https://docs.rs/chrono/latest/chrono/struct.NaiveTime.html) (requires feature `chrono`) +- [`TimeDelta`](https://docs.rs/chrono/latest/chrono/struct.TimeDelta.html) (requires feature `chrono`) +- [`Decimal`](https://docs.rs/rust_decimal/latest/rust_decimal/struct.Decimal.html) (requires feature `decimal`) +- [`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) (requires feature `uuid`) + +## Options + +### `min` + +The value should be equal to or greater than the specified expression. + +```rust +# extern crate fortifier; +# +use fortifier::Validate; + +#[derive(Validate)] +struct Object { + #[validate(range(min = 0.0))] + height: f64 +} +``` + +### `max` + +The value should be equal to or less than the specified expression. + +```rust +# extern crate fortifier; +# +use fortifier::Validate; + +#[derive(Validate)] +struct Object { + #[validate(range(max = 100.0))] + height: f64 +} +``` + +### `exclusive_min` + +The value should be greater than the specified expression. + +```rust +# extern crate fortifier; +# +use fortifier::Validate; + +#[derive(Validate)] +struct Object { + #[validate(range(exclusive_min = 0.0))] + height: f64 +} +``` + +### `exclusive_max` + +The value should be less than the specified expression. + +```rust +# extern crate fortifier; +# +use fortifier::Validate; + +#[derive(Validate)] +struct Object { + #[validate(range(exclusive_max = 100.0))] + height: f64 +} +``` diff --git a/packages/fortifier-macros/Cargo.toml b/packages/fortifier-macros/Cargo.toml index a5da113..38f4e54 100644 --- a/packages/fortifier-macros/Cargo.toml +++ b/packages/fortifier-macros/Cargo.toml @@ -11,6 +11,10 @@ version.workspace = true [package.metadata.docs.rs] all-features = true +# TODO: Remove after adding more tests. +[package.metadata.cargo-machete] +ignored = ["chrono", "rust_decimal", "uuid"] + [lib] proc-macro = true @@ -27,20 +31,23 @@ quote = "1.0.42" syn = "2.0.110" [dev-dependencies] +chrono.workspace = true email_address.workspace = true fortifier = { workspace = true, features = [ + "all-types", "all-validations", - "indexmap", "serde", ] } indexmap.workspace = true phonenumber.workspace = true pretty_assertions.workspace = true regex.workspace = true +rust_decimal.workspace = true serde.workspace = true serde_json.workspace = true trybuild = "1.0.114" url.workspace = true +uuid.workspace = true [lints] workspace = true diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index c9fa8cd..fe3afa5 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -7,7 +7,7 @@ use crate::{ attributes::enum_attributes, validate::r#type::{KnownOrUnknown, should_validate_type}, validation::{Execution, Validation}, - validations::{Custom, EmailAddress, Length, Nested, PhoneNumber, Regex, Url}, + validations::{Custom, EmailAddress, Length, Nested, PhoneNumber, Range, Regex, Url}, }; pub enum LiteralOrIdent { @@ -65,36 +65,50 @@ impl<'a> ValidateField<'a> { if attr.path().is_ident("validate") { attr.parse_nested_meta(|meta| { if meta.path.is_ident("custom") { - result.validations.push(Box::new(Custom::parse(&meta)?)); + result + .validations + .push(Box::new(Custom::parse(field, &meta)?)); Ok(()) } else if meta.path.is_ident("email_address") { result .validations - .push(Box::new(EmailAddress::parse(&meta)?)); + .push(Box::new(EmailAddress::parse(field, &meta)?)); Ok(()) } else if meta.path.is_ident("length") { - result.validations.push(Box::new(Length::parse(&meta)?)); + result + .validations + .push(Box::new(Length::parse(field, &meta)?)); Ok(()) } else if meta.path.is_ident("nested") { - result.validations.push(Box::new(Nested::parse(&meta)?)); + result + .validations + .push(Box::new(Nested::parse(field, &meta)?)); skip_nested = true; Ok(()) } else if meta.path.is_ident("phone_number") { result .validations - .push(Box::new(PhoneNumber::parse(&meta)?)); + .push(Box::new(PhoneNumber::parse(field, &meta)?)); + + Ok(()) + } else if meta.path.is_ident("range") { + result + .validations + .push(Box::new(Range::parse(field, &meta)?)); Ok(()) } else if meta.path.is_ident("regex") { - result.validations.push(Box::new(Regex::parse(&meta)?)); + result + .validations + .push(Box::new(Regex::parse(field, &meta)?)); Ok(()) } else if meta.path.is_ident("url") { - result.validations.push(Box::new(Url::parse(&meta)?)); + result.validations.push(Box::new(Url::parse(field, &meta)?)); Ok(()) } else if meta.path.is_ident("skip") { diff --git a/packages/fortifier-macros/src/validation.rs b/packages/fortifier-macros/src/validation.rs index 0236fa1..733bb2c 100644 --- a/packages/fortifier-macros/src/validation.rs +++ b/packages/fortifier-macros/src/validation.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use syn::{Ident, Result, meta::ParseNestedMeta}; +use syn::{Field, Ident, Result, meta::ParseNestedMeta}; #[derive(Clone, Copy)] pub enum Execution { @@ -8,7 +8,7 @@ pub enum Execution { } pub trait Validation { - fn parse(_meta: &ParseNestedMeta<'_>) -> Result + fn parse(_field: &Field, _meta: &ParseNestedMeta<'_>) -> Result where Self: Sized; diff --git a/packages/fortifier-macros/src/validations.rs b/packages/fortifier-macros/src/validations.rs index 3dfb1bb..1136ee2 100644 --- a/packages/fortifier-macros/src/validations.rs +++ b/packages/fortifier-macros/src/validations.rs @@ -3,6 +3,7 @@ mod email_address; mod length; mod nested; mod phone_number; +mod range; mod regex; mod url; @@ -11,5 +12,6 @@ pub use email_address::*; pub use length::*; pub use nested::*; pub use phone_number::*; +pub use range::*; pub use regex::*; pub use url::*; diff --git a/packages/fortifier-macros/src/validations/custom.rs b/packages/fortifier-macros/src/validations/custom.rs index e459567..caa0d12 100644 --- a/packages/fortifier-macros/src/validations/custom.rs +++ b/packages/fortifier-macros/src/validations/custom.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta}; +use syn::{Field, Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -11,7 +11,7 @@ pub struct Custom { } impl Validation for Custom { - fn parse(meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { let mut execution = Execution::Sync; let mut error_type: Option = None; let mut function_path: Option = None; diff --git a/packages/fortifier-macros/src/validations/email_address.rs b/packages/fortifier-macros/src/validations/email_address.rs index 67d9698..8cb4d71 100644 --- a/packages/fortifier-macros/src/validations/email_address.rs +++ b/packages/fortifier-macros/src/validations/email_address.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, LitBool, LitInt, Result, meta::ParseNestedMeta}; +use syn::{Field, Ident, LitBool, LitInt, Result, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -21,7 +21,7 @@ impl Default for EmailAddress { } impl Validation for EmailAddress { - fn parse(meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { let mut result = EmailAddress::default(); if !meta.input.is_empty() { diff --git a/packages/fortifier-macros/src/validations/length.rs b/packages/fortifier-macros/src/validations/length.rs index 0fecd83..b557c16 100644 --- a/packages/fortifier-macros/src/validations/length.rs +++ b/packages/fortifier-macros/src/validations/length.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Field, Ident, Result, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -12,7 +12,7 @@ pub struct Length { } impl Validation for Length { - fn parse(meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { let mut result = Length::default(); meta.parse_nested_meta(|meta| { diff --git a/packages/fortifier-macros/src/validations/nested.rs b/packages/fortifier-macros/src/validations/nested.rs index 301233c..10020bd 100644 --- a/packages/fortifier-macros/src/validations/nested.rs +++ b/packages/fortifier-macros/src/validations/nested.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{Ident, Path, Result, meta::ParseNestedMeta}; +use syn::{Field, Ident, Path, Result, meta::ParseNestedMeta}; use crate::{ attributes::enum_field_attributes, @@ -19,7 +19,7 @@ impl Nested { } impl Validation for Nested { - fn parse(meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { let mut error_type: Option = None; meta.parse_nested_meta(|meta| { diff --git a/packages/fortifier-macros/src/validations/phone_number.rs b/packages/fortifier-macros/src/validations/phone_number.rs index 5aa7351..723ea51 100644 --- a/packages/fortifier-macros/src/validations/phone_number.rs +++ b/packages/fortifier-macros/src/validations/phone_number.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Field, Ident, Result, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -11,7 +11,7 @@ pub struct PhoneNumber { } impl Validation for PhoneNumber { - fn parse(meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { let mut result = PhoneNumber::default(); if !meta.input.is_empty() { diff --git a/packages/fortifier-macros/src/validations/range.rs b/packages/fortifier-macros/src/validations/range.rs new file mode 100644 index 0000000..901ee7b --- /dev/null +++ b/packages/fortifier-macros/src/validations/range.rs @@ -0,0 +1,102 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{Expr, Field, Ident, Result, Type, meta::ParseNestedMeta}; + +use crate::validation::{Execution, Validation}; + +pub struct Range { + r#type: Type, + min: Option, + max: Option, + exclusive_min: Option, + exclusive_max: Option, +} + +impl Validation for Range { + fn parse(field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + let mut result = Range { + r#type: field.ty.clone(), + min: None, + max: None, + exclusive_min: None, + exclusive_max: None, + }; + + meta.parse_nested_meta(|meta| { + if meta.path.is_ident("min") { + let expr: Expr = meta.value()?.parse()?; + result.min = Some(expr); + + Ok(()) + } else if meta.path.is_ident("max") { + let expr: Expr = meta.value()?.parse()?; + result.max = Some(expr); + + Ok(()) + } else if meta.path.is_ident("exclusive_min") { + let expr: Expr = meta.value()?.parse()?; + result.exclusive_min = Some(expr); + + Ok(()) + } else if meta.path.is_ident("exclusive_max") { + let expr: Expr = meta.value()?.parse()?; + result.exclusive_max = Some(expr); + + Ok(()) + } else { + Err(meta.error("unknown parameter")) + } + })?; + + if result.min.is_some() && result.exclusive_min.is_some() { + return Err(meta.error("`exclusive_min` and `min` are conflicting parameters")); + } + if result.max.is_some() && result.exclusive_max.is_some() { + return Err(meta.error("`exclusive_max` and `max` are conflicting parameters")); + } + + Ok(result) + } + + fn ident(&self) -> Ident { + format_ident!("Range") + } + + fn error_type(&self) -> TokenStream { + let r#type = &self.r#type; + + quote!(::fortifier::RangeError<#r#type>) + } + + fn expr(&self, exeuction: Execution, expr: &TokenStream) -> Option { + match exeuction { + Execution::Sync => { + let min = if let Some(min) = &self.min { + quote!(Some(#min)) + } else { + quote!(None) + }; + let max = if let Some(max) = &self.max { + quote!(Some(#max)) + } else { + quote!(None) + }; + let exclusive_min = if let Some(exclusive_min) = &self.exclusive_min { + quote!(Some(#exclusive_min)) + } else { + quote!(None) + }; + let exclusive_max = if let Some(exclusive_max) = &self.exclusive_max { + quote!(Some(#exclusive_max)) + } else { + quote!(None) + }; + + Some(quote! { + ::fortifier::ValidateRange::validate_range(&#expr, #min, #max, #exclusive_min, #exclusive_max) + }) + } + Execution::Async => None, + } + } +} diff --git a/packages/fortifier-macros/src/validations/regex.rs b/packages/fortifier-macros/src/validations/regex.rs index ad4eec4..5827d02 100644 --- a/packages/fortifier-macros/src/validations/regex.rs +++ b/packages/fortifier-macros/src/validations/regex.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Field, Ident, Result, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -9,7 +9,7 @@ pub struct Regex { } impl Validation for Regex { - fn parse(meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { let mut expression: Option = None; if let Ok(value) = meta.value() { diff --git a/packages/fortifier-macros/src/validations/url.rs b/packages/fortifier-macros/src/validations/url.rs index 547bb6e..f6f9587 100644 --- a/packages/fortifier-macros/src/validations/url.rs +++ b/packages/fortifier-macros/src/validations/url.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, Result, meta::ParseNestedMeta}; +use syn::{Field, Ident, Result, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -8,7 +8,7 @@ use crate::validation::{Execution, Validation}; pub struct Url {} impl Validation for Url { - fn parse(_meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_field: &Field, _meta: &ParseNestedMeta<'_>) -> Result { Ok(Url::default()) } diff --git a/packages/fortifier-macros/tests/integrations/serde_pass.rs b/packages/fortifier-macros/tests/integrations/serde_pass.rs index 8b3a99a..e101165 100644 --- a/packages/fortifier-macros/tests/integrations/serde_pass.rs +++ b/packages/fortifier-macros/tests/integrations/serde_pass.rs @@ -34,7 +34,7 @@ fn main() { "code": "length", "subcode": "min", "min": 1, - "length": 0 + "value": 0 }, { "path": "emailAddresses", diff --git a/packages/fortifier-macros/tests/validations/length/options_pass.rs b/packages/fortifier-macros/tests/validations/length/options_pass.rs index 036cc3f..b6dac95 100644 --- a/packages/fortifier-macros/tests/validations/length/options_pass.rs +++ b/packages/fortifier-macros/tests/validations/length/options_pass.rs @@ -26,22 +26,22 @@ fn main() { LengthDataValidationError::Equal(LengthError::Equal { code: LengthErrorCode, equal: 2, - length: 1 + value: 1 }), LengthDataValidationError::Min(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::Max(LengthError::Max { code: LengthErrorCode, max: 4, - length: 5 + value: 5 }), LengthDataValidationError::MinMax(LengthError::Max { code: LengthErrorCode, max: 4, - length: 6 + value: 6 }) ])) ); diff --git a/packages/fortifier-macros/tests/validations/length/types_pass.rs b/packages/fortifier-macros/tests/validations/length/types_pass.rs index 52811a3..6e49163 100644 --- a/packages/fortifier-macros/tests/validations/length/types_pass.rs +++ b/packages/fortifier-macros/tests/validations/length/types_pass.rs @@ -56,67 +56,67 @@ fn main() { LengthDataValidationError::Str(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::String(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::Array(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::Slice(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::BTreeMap(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::BTreeSet(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::HashMap(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::HashSet(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::IndexMap(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::IndexSet(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::LinkedList(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::Vec(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), LengthDataValidationError::VecDeque(LengthError::Min { code: LengthErrorCode, min: 1, - length: 0 + value: 0 }), ])) ); diff --git a/packages/fortifier-macros/tests/validations/range/conflict_max_fail.rs b/packages/fortifier-macros/tests/validations/range/conflict_max_fail.rs new file mode 100644 index 0000000..4cf4316 --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/conflict_max_fail.rs @@ -0,0 +1,9 @@ +use fortifier::Validate; + +#[derive(Validate)] +struct RangeData<'a> { + #[validate(range(exclusive_max = 1, max = 2))] + value: &'a str, +} + +fn main() {} diff --git a/packages/fortifier-macros/tests/validations/range/conflict_max_fail.stderr b/packages/fortifier-macros/tests/validations/range/conflict_max_fail.stderr new file mode 100644 index 0000000..3a5c1c0 --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/conflict_max_fail.stderr @@ -0,0 +1,5 @@ +error: `exclusive_max` and `max` are conflicting parameters + --> tests/validations/range/conflict_max_fail.rs:5:16 + | +5 | #[validate(range(exclusive_max = 1, max = 2))] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/fortifier-macros/tests/validations/range/conflict_min_fail.rs b/packages/fortifier-macros/tests/validations/range/conflict_min_fail.rs new file mode 100644 index 0000000..7b6882f --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/conflict_min_fail.rs @@ -0,0 +1,9 @@ +use fortifier::Validate; + +#[derive(Validate)] +struct RangeData<'a> { + #[validate(range(exclusive_min = 1, min = 2))] + value: &'a str, +} + +fn main() {} diff --git a/packages/fortifier-macros/tests/validations/range/conflict_min_fail.stderr b/packages/fortifier-macros/tests/validations/range/conflict_min_fail.stderr new file mode 100644 index 0000000..e54a5eb --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/conflict_min_fail.stderr @@ -0,0 +1,5 @@ +error: `exclusive_min` and `min` are conflicting parameters + --> tests/validations/range/conflict_min_fail.rs:5:16 + | +5 | #[validate(range(exclusive_min = 1, min = 2))] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/packages/fortifier-macros/tests/validations/range/options_pass.rs b/packages/fortifier-macros/tests/validations/range/options_pass.rs new file mode 100644 index 0000000..8515df8 --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/options_pass.rs @@ -0,0 +1,70 @@ +use fortifier::{RangeError, RangeErrorCode, Validate, ValidationErrors}; + +#[derive(Validate)] +struct RangeData { + #[validate(range(min = 1))] + min: usize, + #[validate(range(max = 4))] + max: usize, + #[validate(range(min = 1, max = 4))] + min_max: usize, + #[validate(range(exclusive_min = 1))] + exclusive_min: usize, + #[validate(range(exclusive_max = 4))] + exclusive_max: usize, + #[validate(range(exclusive_min = 1, exclusive_max = 4))] + exclusive_min_exclusive_max: usize, + #[validate(range(exclusive_min = 1, max = 7))] + exclusive_min_max: usize, + #[validate(range(min = 1, exclusive_max = 7))] + min_exclusive_max: usize, +} + +fn main() { + let data = RangeData { + min: 0, + max: 5, + min_max: 6, + exclusive_min: 1, + exclusive_max: 4, + exclusive_min_exclusive_max: 1, + exclusive_min_max: 2, + min_exclusive_max: 2, + }; + + assert_eq!( + data.validate_sync(), + Err(ValidationErrors::from_iter([ + RangeDataValidationError::Min(RangeError::Min { + code: RangeErrorCode, + min: 1, + value: 0 + }), + RangeDataValidationError::Max(RangeError::Max { + code: RangeErrorCode, + max: 4, + value: 5 + }), + RangeDataValidationError::MinMax(RangeError::Max { + code: RangeErrorCode, + max: 4, + value: 6 + }), + RangeDataValidationError::ExclusiveMin(RangeError::ExclusiveMin { + code: RangeErrorCode, + exclusive_min: 1, + value: 1 + }), + RangeDataValidationError::ExclusiveMax(RangeError::ExclusiveMax { + code: RangeErrorCode, + exclusive_max: 4, + value: 4 + }), + RangeDataValidationError::ExclusiveMinExclusiveMax(RangeError::ExclusiveMin { + code: RangeErrorCode, + exclusive_min: 1, + value: 1 + }) + ])) + ); +} diff --git a/packages/fortifier-macros/tests/validations/range/unknown_fail.rs b/packages/fortifier-macros/tests/validations/range/unknown_fail.rs new file mode 100644 index 0000000..936f91f --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/unknown_fail.rs @@ -0,0 +1,9 @@ +use fortifier::Validate; + +#[derive(Validate)] +struct RangeData<'a> { + #[validate(range(unknown = 2))] + value: &'a str, +} + +fn main() {} diff --git a/packages/fortifier-macros/tests/validations/range/unknown_fail.stderr b/packages/fortifier-macros/tests/validations/range/unknown_fail.stderr new file mode 100644 index 0000000..d6ccb1f --- /dev/null +++ b/packages/fortifier-macros/tests/validations/range/unknown_fail.stderr @@ -0,0 +1,5 @@ +error: unknown parameter + --> tests/validations/range/unknown_fail.rs:5:22 + | +5 | #[validate(range(unknown = 2))] + | ^^^^^^^ diff --git a/packages/fortifier/Cargo.toml b/packages/fortifier/Cargo.toml index 69ed21d..054ada9 100644 --- a/packages/fortifier/Cargo.toml +++ b/packages/fortifier/Cargo.toml @@ -13,29 +13,43 @@ all-features = true [features] default = ["macros"] +all-types = ["chrono", "decimal", "indexmap", "uuid"] all-validations = ["email-address", "phone-number", "regex", "url"] +chrono = ["dep:chrono"] +decimal = ["dep:rust_decimal"] email-address = ["dep:email_address"] indexmap = ["dep:indexmap"] macros = ["dep:fortifier-macros"] message = [] phone-number = ["dep:phonenumber"] regex = ["dep:regex"] -serde = ["dep:serde", "email_address?/serde_support", "fortifier-macros?/serde"] +serde = [ + "dep:serde", + "chrono?/serde", + "email_address?/serde_support", + "fortifier-macros?/serde", + "uuid?/serde", +] url = ["dep:url"] utoipa = ["dep:utoipa", "fortifier-macros?/utoipa"] +uuid = ["dep:uuid"] [dependencies] +chrono = { workspace = true, optional = true } email_address = { workspace = true, default-features = false, optional = true } fortifier-macros = { workspace = true, optional = true } indexmap = { workspace = true, optional = true } phonenumber = { workspace = true, optional = true } regex = { workspace = true, optional = true } +rust_decimal = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"], optional = true } url = { workspace = true, optional = true } utoipa = { workspace = true, optional = true } +uuid = { workspace = true, optional = true } [dev-dependencies] pretty_assertions.workspace = true +rust_decimal = { workspace = true, features = ["macros"] } serde_json.workspace = true [lints] diff --git a/packages/fortifier/src/validations.rs b/packages/fortifier/src/validations.rs index 9dcbc97..2357f0d 100644 --- a/packages/fortifier/src/validations.rs +++ b/packages/fortifier/src/validations.rs @@ -3,6 +3,7 @@ mod email_address; mod length; #[cfg(feature = "phone-number")] mod phone_number; +mod range; #[cfg(feature = "regex")] mod regex; #[cfg(feature = "url")] @@ -13,6 +14,7 @@ pub use email_address::*; pub use length::*; #[cfg(feature = "phone-number")] pub use phone_number::*; +pub use range::*; #[cfg(feature = "regex")] pub use regex::*; #[cfg(feature = "url")] diff --git a/packages/fortifier/src/validations/length.rs b/packages/fortifier/src/validations/length.rs index 85b5f31..ed9b9dc 100644 --- a/packages/fortifier/src/validations/length.rs +++ b/packages/fortifier/src/validations/length.rs @@ -33,7 +33,7 @@ pub enum LengthError { equal: T, /// The actual length. - length: T, + value: T, /// The error code. #[cfg_attr(feature = "serde", serde(default))] @@ -49,7 +49,7 @@ pub enum LengthError { min: T, /// The actual length. - length: T, + value: T, /// The error code. #[cfg_attr(feature = "serde", serde(default))] @@ -59,13 +59,13 @@ pub enum LengthError { #[cfg(feature = "message")] message: String, }, - /// Length is more than the maximum length. + /// Length is greater than the maximum length. Max { /// The maximum length. max: T, /// The length. - length: T, + value: T, /// The error code. #[cfg_attr(feature = "serde", serde(default))] @@ -103,7 +103,7 @@ where return Err(LengthError::Equal { equal, - length, + value: length, code: LengthErrorCode, #[cfg(feature = "message")] message, @@ -118,7 +118,7 @@ where return Err(LengthError::Min { min, - length, + value: length, code: LengthErrorCode, #[cfg(feature = "message")] message, @@ -133,7 +133,7 @@ where return Err(LengthError::Max { max, - length, + value: length, code: LengthErrorCode, #[cfg(feature = "message")] message, @@ -330,7 +330,7 @@ mod tests { (*"a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -340,7 +340,7 @@ mod tests { "a".validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -350,7 +350,7 @@ mod tests { "a".to_owned().validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -360,7 +360,7 @@ mod tests { Cow::::Borrowed("a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -370,7 +370,7 @@ mod tests { Cow::::Owned("a".to_owned()).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -381,7 +381,7 @@ mod tests { Some("a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -392,7 +392,7 @@ mod tests { [""; 1].validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -402,7 +402,7 @@ mod tests { [""].validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -412,7 +412,7 @@ mod tests { BTreeSet::from([""]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -422,7 +422,7 @@ mod tests { BTreeMap::from([("", "")]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -432,7 +432,7 @@ mod tests { HashSet::from([""]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -442,7 +442,7 @@ mod tests { HashMap::from([("", "")]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -452,7 +452,7 @@ mod tests { vec![""].validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -462,7 +462,7 @@ mod tests { VecDeque::from([""]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -475,7 +475,7 @@ mod tests { IndexSet::from([""]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -485,7 +485,7 @@ mod tests { IndexMap::from([("", "")]).validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -497,7 +497,7 @@ mod tests { (&"a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -509,7 +509,7 @@ mod tests { Box::new("a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -520,7 +520,7 @@ mod tests { Arc::new("a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -530,7 +530,7 @@ mod tests { Rc::new("a").validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -542,7 +542,7 @@ mod tests { cell.borrow().validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -552,7 +552,7 @@ mod tests { cell.borrow_mut().validate_length(Some(2), None, None), Err(LengthError::Equal { equal: 2, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), @@ -566,7 +566,7 @@ mod tests { (*"a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -576,7 +576,7 @@ mod tests { "a".validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -586,7 +586,7 @@ mod tests { "a".to_owned().validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -596,7 +596,7 @@ mod tests { Cow::::Borrowed("a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -606,7 +606,7 @@ mod tests { Cow::::Owned("a".to_owned()).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -617,7 +617,7 @@ mod tests { Some("a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -628,7 +628,7 @@ mod tests { [""; 1].validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -638,7 +638,7 @@ mod tests { [""].validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -648,7 +648,7 @@ mod tests { BTreeSet::from([""]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -658,7 +658,7 @@ mod tests { BTreeMap::from([("", "")]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -668,7 +668,7 @@ mod tests { HashSet::from([""]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -678,7 +678,7 @@ mod tests { HashMap::from([("", "")]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -688,7 +688,7 @@ mod tests { vec![""].validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -698,7 +698,7 @@ mod tests { VecDeque::from([""]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -711,7 +711,7 @@ mod tests { IndexSet::from([""]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -721,7 +721,7 @@ mod tests { IndexMap::from([("", "")]).validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -733,7 +733,7 @@ mod tests { (&"a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -745,7 +745,7 @@ mod tests { Box::new("a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -756,7 +756,7 @@ mod tests { Arc::new("a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -766,7 +766,7 @@ mod tests { Rc::new("a").validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -778,7 +778,7 @@ mod tests { cell.borrow().validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -788,7 +788,7 @@ mod tests { cell.borrow_mut().validate_length(None, Some(3), None), Err(LengthError::Min { min: 3, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), @@ -802,7 +802,7 @@ mod tests { (*"a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -812,7 +812,7 @@ mod tests { "a".validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -822,7 +822,7 @@ mod tests { "a".to_owned().validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -832,7 +832,7 @@ mod tests { Cow::::Borrowed("a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -842,7 +842,7 @@ mod tests { Cow::::Owned("a".to_owned()).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -853,7 +853,7 @@ mod tests { Some("a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -864,7 +864,7 @@ mod tests { [""; 1].validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -874,7 +874,7 @@ mod tests { [""].validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -884,7 +884,7 @@ mod tests { BTreeSet::from([""]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -894,7 +894,7 @@ mod tests { BTreeMap::from([("", "")]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -904,7 +904,7 @@ mod tests { HashSet::from([""]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -914,7 +914,7 @@ mod tests { HashMap::from([("", "")]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -924,7 +924,7 @@ mod tests { vec![""].validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -934,7 +934,7 @@ mod tests { VecDeque::from([""]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -947,7 +947,7 @@ mod tests { IndexSet::from([""]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -957,7 +957,7 @@ mod tests { IndexMap::from([("", "")]).validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -969,7 +969,7 @@ mod tests { (&"a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -981,7 +981,7 @@ mod tests { Box::new("a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -992,7 +992,7 @@ mod tests { Arc::new("a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -1002,7 +1002,7 @@ mod tests { Rc::new("a").validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -1014,7 +1014,7 @@ mod tests { cell.borrow().validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), @@ -1024,7 +1024,7 @@ mod tests { cell.borrow_mut().validate_length(None, None, Some(0)), Err(LengthError::Max { max: 0, - length: 1, + value: 1, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), diff --git a/packages/fortifier/src/validations/range.rs b/packages/fortifier/src/validations/range.rs new file mode 100644 index 0000000..c185df4 --- /dev/null +++ b/packages/fortifier/src/validations/range.rs @@ -0,0 +1,2078 @@ +use std::{ + cell::{Ref, RefMut}, + fmt::Display, + rc::Rc, + sync::Arc, +}; + +use crate::error_code; + +error_code!(RangeErrorCode, "range"); + +/// Range validation error. +#[derive(Debug, Eq, PartialEq)] +#[cfg_attr( + feature = "serde", + derive(serde::Deserialize, serde::Serialize), + serde( + tag = "subcode", + rename_all = "camelCase", + rename_all_fields = "camelCase" + ) +)] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] +pub enum RangeError { + /// Value is less than the minimum value. + Min { + /// The minimum value. + min: T, + + /// The actual value. + value: T, + + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: RangeErrorCode, + + /// A human-readable error message. + #[cfg(feature = "message")] + message: String, + }, + /// Value is more than the maximum value. + Max { + /// The maximum value. + max: T, + + /// The value. + value: T, + + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: RangeErrorCode, + + /// A human-readable error message. + #[cfg(feature = "message")] + message: String, + }, + /// Value is less than or equal to the exclusive minimum value. + ExclusiveMin { + /// The minimum value. + exclusive_min: T, + + /// The actual value. + value: T, + + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: RangeErrorCode, + + /// A human-readable error message. + #[cfg(feature = "message")] + message: String, + }, + /// Value is greater than or equal to the exclusive maximum value. + ExclusiveMax { + /// The maximum value. + exclusive_max: T, + + /// The value. + value: T, + + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: RangeErrorCode, + + /// A human-readable error message. + #[cfg(feature = "message")] + message: String, + }, +} + +/// Validate a range. +pub trait ValidateRange +where + T: Display + PartialEq + PartialOrd, +{ + /// The value. + fn range_value(&self) -> Option; + + /// Validate range. + fn validate_range( + &self, + min: Option, + max: Option, + exclusive_min: Option, + exclusive_max: Option, + ) -> Result<(), RangeError> { + let Some(value) = self.range_value() else { + return Ok(()); + }; + + if let Some(min) = min + && value < min + { + #[cfg(feature = "message")] + let message = format!("value {value} is less than minimum value {min}"); + + return Err(RangeError::Min { + min, + value, + code: RangeErrorCode, + #[cfg(feature = "message")] + message, + }); + } + + if let Some(max) = max + && value > max + { + #[cfg(feature = "message")] + let message = format!("value {value} is greater than maximum value {max}"); + + return Err(RangeError::Max { + max, + value, + code: RangeErrorCode, + #[cfg(feature = "message")] + message, + }); + } + + if let Some(exclusive_min) = exclusive_min + && value <= exclusive_min + { + #[cfg(feature = "message")] + let message = format!( + "value {value} is less than or equal to exclusive minimum value {exclusive_min}" + ); + + return Err(RangeError::ExclusiveMin { + exclusive_min, + value, + code: RangeErrorCode, + #[cfg(feature = "message")] + message, + }); + } + + if let Some(exclusive_max) = exclusive_max + && value >= exclusive_max + { + #[cfg(feature = "message")] + let message = format!( + "value {value} is greater than or equal to exclusive maximum value {exclusive_max}" + ); + + return Err(RangeError::ExclusiveMax { + exclusive_max, + value, + code: RangeErrorCode, + #[cfg(feature = "message")] + message, + }); + } + + Ok(()) + } +} + +macro_rules! validate_with_copy { + ($type:ty) => { + impl ValidateRange<$type> for $type { + fn range_value(&self) -> Option { + Some(*self) + } + } + }; +} + +validate_with_copy!(bool); +validate_with_copy!(u8); +validate_with_copy!(u16); +validate_with_copy!(u32); +validate_with_copy!(u64); +validate_with_copy!(u128); +validate_with_copy!(usize); +validate_with_copy!(i8); +validate_with_copy!(i16); +validate_with_copy!(i32); +validate_with_copy!(i64); +validate_with_copy!(i128); +validate_with_copy!(isize); +validate_with_copy!(f32); +validate_with_copy!(f64); +validate_with_copy!(char); +#[cfg(feature = "chrono")] +validate_with_copy!(chrono::NaiveDate); +#[cfg(feature = "chrono")] +validate_with_copy!(chrono::NaiveDateTime); +#[cfg(feature = "chrono")] +validate_with_copy!(chrono::NaiveTime); +#[cfg(feature = "chrono")] +validate_with_copy!(chrono::TimeDelta); +#[cfg(feature = "decimal")] +validate_with_copy!(rust_decimal::Decimal); +#[cfg(feature = "uuid")] +validate_with_copy!(uuid::Uuid); + +impl<'a> ValidateRange<&'a str> for &'a str { + fn range_value(&self) -> Option { + Some(self) + } +} + +macro_rules! validate_with_clone { + ($type:ty) => { + impl ValidateRange<$type> for $type { + fn range_value(&self) -> Option { + Some(self.clone()) + } + } + }; +} + +validate_with_clone!(String); + +#[cfg(feature = "chrono")] +impl ValidateRange> for chrono::DateTime +where + Tz: chrono::TimeZone, + Tz::Offset: Display, +{ + fn range_value(&self) -> Option { + Some(self.clone()) + } +} + +impl ValidateRange for Option +where + L: Display + PartialEq + PartialOrd, + T: ValidateRange, +{ + fn range_value(&self) -> Option { + if let Some(s) = self { + T::range_value(s) + } else { + None + } + } +} + +macro_rules! validate_with_deref { + ($type:ty) => { + impl ValidateRange for $type + where + V: Display + PartialEq + PartialOrd, + T: ValidateRange, + { + fn range_value(&self) -> Option { + T::range_value(self) + } + } + }; +} + +validate_with_deref!(&T); +validate_with_deref!(Arc); +validate_with_deref!(Box); +validate_with_deref!(Rc); +validate_with_deref!(Ref<'_, T>); +validate_with_deref!(RefMut<'_, T>); + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc, sync::Arc}; + + #[cfg(feature = "chrono")] + use chrono::{NaiveDate, NaiveDateTime, NaiveTime, TimeDelta}; + #[cfg(feature = "decimal")] + use rust_decimal::dec; + use uuid::Uuid; + + use super::{RangeError, RangeErrorCode, ValidateRange}; + + #[test] + fn ok() { + assert_eq!(false.validate_range(Some(false), None, None, None), Ok(())); + assert_eq!(3u8.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3u16.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3u32.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3u32.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3u64.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3u128.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3usize.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3i8.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3i16.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3i32.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3i32.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3i64.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3i128.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3isize.validate_range(Some(1), None, None, None), Ok(())); + assert_eq!(3.0f32.validate_range(Some(1.0), None, None, None), Ok(())); + assert_eq!(3.0f64.validate_range(Some(1.0), None, None, None), Ok(())); + assert_eq!('c'.validate_range(Some('a'), None, None, None), Ok(())); + assert_eq!("c".validate_range(Some("a"), None, None, None), Ok(())); + + #[cfg(feature = "chrono")] + { + assert_eq!( + NaiveDate::from_ymd_opt(2025, 4, 3) + .expect("valid date") + .validate_range( + Some(NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date")), + None, + None, + None + ), + Ok(()) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ) + .validate_range( + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + )), + None, + None, + None + ), + Ok(()) + ); + assert_eq!( + NaiveTime::from_hms_opt(12, 34, 56) + .expect("valid time") + .validate_range( + Some(NaiveTime::from_hms_opt(0, 0, 0).expect("valid time")), + None, + None, + None + ), + Ok(()) + ); + assert_eq!( + TimeDelta::minutes(3).validate_range(Some(TimeDelta::minutes(1)), None, None, None), + Ok(()) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ) + .and_utc() + .validate_range( + Some( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ) + .and_utc() + ), + None, + None, + None + ), + Ok(()) + ); + } + + #[cfg(feature = "decimal")] + assert_eq!( + dec!(3.0).validate_range(Some(dec!(1.0)), None, None, None), + Ok(()) + ); + + #[cfg(feature = "uuid")] + assert_eq!( + Uuid::max().validate_range(Some(Uuid::nil()), None, None, None), + Ok(()) + ); + + assert_eq!( + None::.validate_range(Some(1), None, None, None), + Ok(()) + ); + assert_eq!(Some(3).validate_range(Some(1), None, None, None), Ok(())); + + #[expect(clippy::needless_borrow)] + { + assert_eq!((&3).validate_range(Some(1), None, None, None), Ok(())); + } + #[expect(unused_allocation)] + { + assert_eq!( + (Box::new(3)).validate_range(Some(1), None, None, None), + Ok(()) + ); + } + assert_eq!( + (Arc::new(3)).validate_range(Some(1), None, None, None), + Ok(()) + ); + assert_eq!( + (Rc::new(3)).validate_range(Some(1), None, None, None), + Ok(()) + ); + + let cell = RefCell::new(3); + assert_eq!( + cell.borrow().validate_range(Some(1), None, None, None), + Ok(()) + ); + assert_eq!( + cell.borrow_mut().validate_range(Some(1), None, None, None), + Ok(()) + ); + } + + #[test] + fn min_error() { + assert_eq!( + false.validate_range(Some(true), None, None, None), + Err(RangeError::Min { + min: true, + value: false, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value false is less than minimum value true".to_owned(), + }) + ); + assert_eq!( + 3u8.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3u16.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3u64.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3u128.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3usize.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3i8.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3i16.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3i64.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3i128.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3isize.validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3.0f32.validate_range(Some(4.0), None, None, None), + Err(RangeError::Min { + min: 4.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 3.0f64.validate_range(Some(4.0), None, None, None), + Err(RangeError::Min { + min: 4.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + 'c'.validate_range(Some('d'), None, None, None), + Err(RangeError::Min { + min: 'd', + value: 'c', + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is less than minimum value d".to_owned(), + }) + ); + assert_eq!( + "c".validate_range(Some("d"), None, None, None), + Err(RangeError::Min { + min: "d", + value: "c", + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is less than minimum value d".to_owned(), + }) + ); + + #[cfg(feature = "chrono")] + { + assert_eq!( + NaiveDate::from_ymd_opt(2024, 3, 2) + .expect("valid date") + .validate_range( + Some(NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date")), + None, + None, + None + ), + Err(RangeError::Min { + min: NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + value: NaiveDate::from_ymd_opt(2024, 3, 2).expect("valid date"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 2024-03-02 is less than minimum value 2025-01-01".to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2024, 3, 2).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ) + .validate_range( + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + )), + None, + None, + None + ), + Err(RangeError::Min { + min: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2024, 3, 2).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2024-03-02 12:34:56 is less than minimum value 2025-01-01 00:00:00" + .to_owned(), + }) + ); + assert_eq!( + NaiveTime::from_hms_opt(12, 34, 56) + .expect("valid time") + .validate_range( + Some(NaiveTime::from_hms_opt(13, 0, 0).expect("valid time")), + None, + None, + None + ), + Err(RangeError::Min { + min: NaiveTime::from_hms_opt(13, 0, 0).expect("valid time"), + value: NaiveTime::from_hms_opt(12, 34, 56).expect("valid time"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 12:34:56 is less than minimum value 13:00:00".to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2024, 3, 2).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ) + .and_utc() + .validate_range( + Some( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ) + .and_utc() + ), + None, + None, + None + ), + Err(RangeError::Min { + min: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ) + .and_utc(), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2024, 3, 2).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ) + .and_utc(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2024-03-02 12:34:56 UTC is less than minimum value 2025-01-01 00:00:00 UTC" + .to_owned(), + }) + ); + assert_eq!( + TimeDelta::minutes(3).validate_range(Some(TimeDelta::minutes(4)), None, None, None), + Err(RangeError::Min { + min: TimeDelta::minutes(4), + value: TimeDelta::minutes(3), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value PT180S is less than minimum value PT240S".to_owned(), + }) + ); + } + + #[cfg(feature = "decimal")] + assert_eq!( + dec!(3.0).validate_range(Some(dec!(4.0)), None, None, None), + Err(RangeError::Min { + min: dec!(4.0), + value: dec!(3.0), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3.0 is less than minimum value 4.0".to_owned(), + }) + ); + + #[cfg(feature = "uuid")] + assert_eq!( + Uuid::nil().validate_range(Some(Uuid::max()), None, None, None), + Err(RangeError::Min { + min: Uuid::max(), + value: Uuid::nil(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 00000000-0000-0000-0000-000000000000 is less than minimum value ffffffff-ffff-ffff-ffff-ffffffffffff".to_owned(), + }) + ); + + assert_eq!( + Some(3).validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + + #[expect(clippy::needless_borrow)] + { + assert_eq!( + (&3).validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + } + #[expect(unused_allocation)] + { + assert_eq!( + (Box::new(3)).validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + } + assert_eq!( + (Arc::new(3)).validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + (Rc::new(3)).validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + + let cell = RefCell::new(3); + assert_eq!( + cell.borrow().validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + assert_eq!( + cell.borrow_mut().validate_range(Some(4), None, None, None), + Err(RangeError::Min { + min: 4, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than minimum value 4".to_owned(), + }) + ); + } + + #[test] + fn max_error() { + assert_eq!( + true.validate_range(None, Some(false), None, None), + Err(RangeError::Max { + max: false, + value: true, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value true is greater than maximum value false".to_owned(), + }) + ); + assert_eq!( + 3u8.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3u16.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3u64.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3u128.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3usize.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3i8.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3i16.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3i64.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3i128.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3isize.validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3.0f32.validate_range(None, Some(2.0), None, None), + Err(RangeError::Max { + max: 2.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 3.0f64.validate_range(None, Some(2.0), None, None), + Err(RangeError::Max { + max: 2.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + 'c'.validate_range(None, Some('b'), None, None), + Err(RangeError::Max { + max: 'b', + value: 'c', + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is greater than maximum value b".to_owned(), + }) + ); + assert_eq!( + "c".validate_range(None, Some("b"), None, None), + Err(RangeError::Max { + max: "b", + value: "c", + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is greater than maximum value b".to_owned(), + }) + ); + + #[cfg(feature = "chrono")] + { + assert_eq!( + NaiveDate::from_ymd_opt(2025, 4, 3) + .expect("valid date") + .validate_range( + None, + Some(NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date")), + None, + None + ), + Err(RangeError::Max { + max: NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + value: NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 2025-04-03 is greater than maximum value 2025-01-01".to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ) + .validate_range( + None, + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + )), + None, + None + ), + Err(RangeError::Max { + max: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2025-04-03 12:34:56 is greater than maximum value 2025-01-01 00:00:00" + .to_owned(), + }) + ); + assert_eq!( + NaiveTime::from_hms_opt(12, 34, 56) + .expect("valid time") + .validate_range( + None, + Some(NaiveTime::from_hms_opt(0, 0, 0).expect("valid time")), + None, + None + ), + Err(RangeError::Max { + max: NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"), + value: NaiveTime::from_hms_opt(12, 34, 56).expect("valid time"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 12:34:56 is greater than maximum value 00:00:00".to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ).and_utc() + .validate_range( + None, + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc()), + None, + None + ), + Err(RangeError::Max { + max: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc(), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 4, 3).expect("valid date"), + NaiveTime::from_hms_opt(12, 34, 56).expect("valid time") + ).and_utc(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2025-04-03 12:34:56 UTC is greater than maximum value 2025-01-01 00:00:00 UTC" + .to_owned(), + }) + ); + assert_eq!( + TimeDelta::minutes(3).validate_range(None, Some(TimeDelta::minutes(2)), None, None), + Err(RangeError::Max { + max: TimeDelta::minutes(2), + value: TimeDelta::minutes(3), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value PT180S is greater than maximum value PT120S".to_owned(), + }) + ); + } + + #[cfg(feature = "decimal")] + assert_eq!( + dec!(3.0).validate_range(None, Some(dec!(2.0)), None, None), + Err(RangeError::Max { + max: dec!(2.0), + value: dec!(3.0), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3.0 is greater than maximum value 2.0".to_owned(), + }) + ); + + #[cfg(feature = "uuid")] + assert_eq!( + Uuid::max().validate_range(None, Some(Uuid::nil()), None, None), + Err(RangeError::Max { + max: Uuid::nil(), + value: Uuid::max(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value ffffffff-ffff-ffff-ffff-ffffffffffff is greater than maximum value 00000000-0000-0000-0000-000000000000".to_owned(), + }) + ); + + assert_eq!( + Some(3).validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + + #[expect(clippy::needless_borrow)] + { + assert_eq!( + (&3).validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + } + #[expect(unused_allocation)] + { + assert_eq!( + (Box::new(3)).validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + } + assert_eq!( + (Arc::new(3)).validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + (Rc::new(3)).validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + + let cell = RefCell::new(3); + assert_eq!( + cell.borrow().validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + assert_eq!( + cell.borrow_mut().validate_range(None, Some(2), None, None), + Err(RangeError::Max { + max: 2, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than maximum value 2".to_owned(), + }) + ); + } + + #[test] + fn exclusive_min_error() { + assert_eq!( + true.validate_range(None, None, Some(true), None), + Err(RangeError::ExclusiveMin { + exclusive_min: true, + value: true, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value true is less than or equal to exclusive minimum value true" + .to_owned(), + }) + ); + assert_eq!( + 3u8.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3u16.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3u64.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3u128.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3usize.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3i8.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3i16.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3i64.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3i128.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3isize.validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3.0f32.validate_range(None, None, Some(3.0), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 3.0f64.validate_range(None, None, Some(3.0), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + 'c'.validate_range(None, None, Some('c'), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 'c', + value: 'c', + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is less than or equal to exclusive minimum value c".to_owned(), + }) + ); + assert_eq!( + "c".validate_range(None, None, Some("c"), None), + Err(RangeError::ExclusiveMin { + exclusive_min: "c", + value: "c", + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is less than or equal to exclusive minimum value c".to_owned(), + }) + ); + + #[cfg(feature = "chrono")] + { + assert_eq!( + NaiveDate::from_ymd_opt(2025, 1, 1) + .expect("valid date") + .validate_range( + None, + None, + Some(NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date")), + None + ), + Err(RangeError::ExclusiveMin { + exclusive_min: NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + value: NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 2025-01-01 is less than or equal to exclusive minimum value 2025-01-01" + .to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ) + .validate_range( + None, + None, + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + )), + None + ), + Err(RangeError::ExclusiveMin { + exclusive_min: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0,0,0).expect("valid time") + ), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2025-01-01 00:00:00 is less than or equal to exclusive minimum value 2025-01-01 00:00:00" + .to_owned(), + }) + ); + assert_eq!( + NaiveTime::from_hms_opt(0, 0, 0) + .expect("valid time") + .validate_range( + None, + None, + Some(NaiveTime::from_hms_opt(0, 0, 0).expect("valid time")), + None + ), + Err(RangeError::ExclusiveMin { + exclusive_min: NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"), + value: NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 00:00:00 is less than or equal to exclusive minimum value 00:00:00" + .to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc() + .validate_range( + None, + None, + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc()), + None + ), + Err(RangeError::ExclusiveMin { + exclusive_min: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc(), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0,0,0).expect("valid time") + ).and_utc(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2025-01-01 00:00:00 UTC is less than or equal to exclusive minimum value 2025-01-01 00:00:00 UTC" + .to_owned(), + }) + ); + assert_eq!( + TimeDelta::minutes(3).validate_range(None, None, Some(TimeDelta::minutes(3)), None), + Err(RangeError::ExclusiveMin { + exclusive_min: TimeDelta::minutes(3), + value: TimeDelta::minutes(3), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value PT180S is less than or equal to exclusive minimum value PT180S" + .to_owned(), + }) + ); + } + + #[cfg(feature = "decimal")] + assert_eq!( + dec!(3.0).validate_range(None, None, Some(dec!(3.0)), None), + Err(RangeError::ExclusiveMin { + exclusive_min: dec!(3.0), + value: dec!(3.0), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3.0 is less than or equal to exclusive minimum value 3.0" + .to_owned(), + }) + ); + + #[cfg(feature = "uuid")] + assert_eq!( + Uuid::nil().validate_range(None, None, Some(Uuid::nil()), None), + Err(RangeError::ExclusiveMin { + exclusive_min: Uuid::nil(), + value: Uuid::nil(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 00000000-0000-0000-0000-000000000000 is less than or equal to exclusive minimum value 00000000-0000-0000-0000-000000000000".to_owned(), + }) + ); + + assert_eq!( + Some(3).validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + + #[expect(clippy::needless_borrow)] + { + assert_eq!( + (&3).validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3" + .to_owned(), + }) + ); + } + #[expect(unused_allocation)] + { + assert_eq!( + (Box::new(3)).validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3" + .to_owned(), + }) + ); + } + assert_eq!( + (Arc::new(3)).validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + (Rc::new(3)).validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + + let cell = RefCell::new(3); + assert_eq!( + cell.borrow().validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + assert_eq!( + cell.borrow_mut().validate_range(None, None, Some(3), None), + Err(RangeError::ExclusiveMin { + exclusive_min: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is less than or equal to exclusive minimum value 3".to_owned(), + }) + ); + } + + #[test] + fn exclusive_max_error() { + assert_eq!( + true.validate_range(None, None, None, Some(true)), + Err(RangeError::ExclusiveMax { + exclusive_max: true, + value: true, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value true is greater than or equal to exclusive maximum value true" + .to_owned(), + }) + ); + assert_eq!( + 3u8.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3u16.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3u32.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3u64.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3u128.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3usize.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3i8.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3i16.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3i32.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3i64.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3i128.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3isize.validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3.0f32.validate_range(None, None, None, Some(3.0)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 3.0f64.validate_range(None, None, None, Some(3.0)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3.0, + value: 3.0, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + 'c'.validate_range(None, None, None, Some('c')), + Err(RangeError::ExclusiveMax { + exclusive_max: 'c', + value: 'c', + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is greater than or equal to exclusive maximum value c".to_owned(), + }) + ); + assert_eq!( + "c".validate_range(None, None, None, Some("c")), + Err(RangeError::ExclusiveMax { + exclusive_max: "c", + value: "c", + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value c is greater than or equal to exclusive maximum value c".to_owned(), + }) + ); + + #[cfg(feature = "chrono")] + { + assert_eq!( + NaiveDate::from_ymd_opt(2025, 1, 1) + .expect("valid date") + .validate_range( + None, + None, + None, + Some(NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date")), + ), + Err(RangeError::ExclusiveMax { + exclusive_max: NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + value: NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 2025-01-01 is greater than or equal to exclusive maximum value 2025-01-01" + .to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ) + .validate_range( + None, + None, + None, + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + )), + ), + Err(RangeError::ExclusiveMax { + exclusive_max: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0,0,0).expect("valid time") + ), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2025-01-01 00:00:00 is greater than or equal to exclusive maximum value 2025-01-01 00:00:00" + .to_owned(), + }) + ); + assert_eq!( + NaiveTime::from_hms_opt(0, 0, 0) + .expect("valid time") + .validate_range( + None, + None, + None, + Some(NaiveTime::from_hms_opt(0, 0, 0).expect("valid time")), + ), + Err(RangeError::ExclusiveMax { + exclusive_max: NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"), + value: NaiveTime::from_hms_opt(0, 0, 0).expect("valid time"), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 00:00:00 is greater than or equal to exclusive maximum value 00:00:00" + .to_owned(), + }) + ); + assert_eq!( + NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc() + .validate_range( + None, + None, + None, + Some(NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc()), + ), + Err(RangeError::ExclusiveMax { + exclusive_max: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0, 0, 0).expect("valid time") + ).and_utc(), + value: NaiveDateTime::new( + NaiveDate::from_ymd_opt(2025, 1, 1).expect("valid date"), + NaiveTime::from_hms_opt(0,0,0).expect("valid time") + ).and_utc(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value 2025-01-01 00:00:00 UTC is greater than or equal to exclusive maximum value 2025-01-01 00:00:00 UTC" + .to_owned(), + }) + ); + assert_eq!( + TimeDelta::minutes(3) + .validate_range(None, None, None, Some(TimeDelta::minutes(3)),), + Err(RangeError::ExclusiveMax { + exclusive_max: TimeDelta::minutes(3), + value: TimeDelta::minutes(3), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: + "value PT180S is greater than or equal to exclusive maximum value PT180S" + .to_owned(), + }) + ); + } + + #[cfg(feature = "decimal")] + assert_eq!( + dec!(3.0).validate_range(None, None, None, Some(dec!(3.0))), + Err(RangeError::ExclusiveMax { + exclusive_max: dec!(3.0), + value: dec!(3.0), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3.0 is greater than or equal to exclusive maximum value 3.0" + .to_owned(), + }) + ); + + #[cfg(feature = "uuid")] + assert_eq!( + Uuid::max().validate_range(None, None,None, Some(Uuid::max())), + Err(RangeError::ExclusiveMax { + exclusive_max: Uuid::max(), + value: Uuid::max(), + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value ffffffff-ffff-ffff-ffff-ffffffffffff is greater than or equal to exclusive maximum value ffffffff-ffff-ffff-ffff-ffffffffffff".to_owned(), + }) + ); + + assert_eq!( + Some(3).validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + + #[expect(clippy::needless_borrow)] + { + assert_eq!( + (&3).validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3" + .to_owned(), + }) + ); + } + #[expect(unused_allocation)] + { + assert_eq!( + (Box::new(3)).validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3" + .to_owned(), + }) + ); + } + assert_eq!( + (Arc::new(3)).validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + (Rc::new(3)).validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + + let cell = RefCell::new(3); + assert_eq!( + cell.borrow().validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + assert_eq!( + cell.borrow_mut().validate_range(None, None, None, Some(3)), + Err(RangeError::ExclusiveMax { + exclusive_max: 3, + value: 3, + code: RangeErrorCode, + #[cfg(feature = "message")] + message: "value 3 is greater than or equal to exclusive maximum value 3".to_owned(), + }) + ); + } +} diff --git a/packages/fortifier/tests/serde.rs b/packages/fortifier/tests/serde.rs index aa57b34..9efaadc 100644 --- a/packages/fortifier/tests/serde.rs +++ b/packages/fortifier/tests/serde.rs @@ -25,7 +25,7 @@ fn setup() -> (ValidationErrors, Value) { )), TestError::Length(LengthError::Equal { equal: 1, - length: 2, + value: 2, code: LengthErrorCode, #[cfg(feature = "message")] message: "length 2 is not equal to required length 1".to_owned(), @@ -43,7 +43,7 @@ fn setup() -> (ValidationErrors, Value) { "code": "length", "subcode": "equal", "equal": 1, - "length": 2, + "value": 2, }, { "code": "regex", @@ -64,7 +64,7 @@ fn setup() -> (ValidationErrors, Value) { "code": "length", "subcode": "equal", "equal": 1, - "length": 2, + "value": 2, "message": "length 2 is not equal to required length 1", }, {