Add a form management and validation library called Formbeam
Signed-off-by: Olivier <olivier@librepush.net>
This commit is contained in:
parent
b69662cfdc
commit
f21500d5e8
278
Cargo.lock
generated
278
Cargo.lock
generated
@ -28,14 +28,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -47,6 +48,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.16"
|
||||
@ -84,13 +94,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.64"
|
||||
version = "0.1.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2"
|
||||
checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -166,96 +176,77 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bevy_macro_utils"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd3868e555723249fde3786891f35893b3001b2be4efb51f431467cb7fc378cd"
|
||||
checksum = "c3ad860d35d74b35d4d6ae7f656d163b6f475aa2e64fc293ee86ac901977ddb7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc-hash",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.72",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_ptr"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c74fcf37593a0053f539c3b088f34f268cbefed031d8eb8ff0fb10d175160242"
|
||||
checksum = "c115c97a5c8a263bd0aa7001b999772c744ac5ba797d07c86f25734ce381ea69"
|
||||
|
||||
[[package]]
|
||||
name = "bevy_reflect"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "362492a6b66f676176705cc06017b012320fa260a9cf4baf3513387e9c05693e"
|
||||
checksum = "406ea0fce267169c2320c7302d97d09f605105686346762562c5f65960b5ca2f"
|
||||
dependencies = [
|
||||
"bevy_ptr",
|
||||
"bevy_reflect_derive",
|
||||
"bevy_utils",
|
||||
"downcast-rs",
|
||||
"erased-serde",
|
||||
"once_cell",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_reflect_derive"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e974d78eaf1b45e1b4146711b5c16e37c24234e12f3a52f5f2e28332c969d3c"
|
||||
checksum = "0427fdb4425fc72cc96d45e550df83ace6347f0503840de116c76a40843ba751"
|
||||
dependencies = [
|
||||
"bevy_macro_utils",
|
||||
"bit-set",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.72",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_utils"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10bfde141f0cdd15e07bca72f4439a9db80877c283738f581d061972ef483b1b"
|
||||
checksum = "7fab364910e8f5839578aba9cfda00a8388e9ebe352ceb8491a742ce6af9ec6e"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"ahash 0.8.11",
|
||||
"bevy_utils_proc_macros",
|
||||
"getrandom",
|
||||
"hashbrown 0.14.0",
|
||||
"instant",
|
||||
"petgraph",
|
||||
"thiserror",
|
||||
"hashbrown",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"uuid",
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bevy_utils_proc_macros"
|
||||
version = "0.11.0"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e37f2e885b0e8af59dc19871c313d3cf2a2495db35bb4d4ae0a61b3f87d5401"
|
||||
checksum = "ad9db261ab33a046e1f54b35f885a44f21fcc80aa2bc9050319466b88fe58fe3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
@ -424,10 +415,15 @@ dependencies = [
|
||||
name = "demo_hornbeam_project"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
"bevy_reflect",
|
||||
"color-eyre",
|
||||
"eyre",
|
||||
"formbeam",
|
||||
"formbeam_derive",
|
||||
"hornbeam",
|
||||
"serde",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
@ -486,11 +482,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.3.24"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4ca605381c017ec7a5fef5e548f1cfaa419ed0f6df6367339300db74c92aa7d"
|
||||
checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"typeid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -515,12 +512,6 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fixedbitset"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "fluent"
|
||||
version = "0.16.0"
|
||||
@ -628,6 +619,25 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "formbeam"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bevy_reflect",
|
||||
"regex",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "formbeam_derive"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
@ -705,26 +715,20 @@ version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 0.7.20",
|
||||
"bstr",
|
||||
"fnv",
|
||||
"log",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
|
||||
dependencies = [
|
||||
"ahash 0.8.3",
|
||||
"ahash 0.8.11",
|
||||
"allocator-api2",
|
||||
"serde",
|
||||
]
|
||||
@ -777,6 +781,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bevy_reflect",
|
||||
"fluent-templates",
|
||||
"formbeam",
|
||||
"hornbeam_grammar",
|
||||
"hornbeam_ir",
|
||||
"html-escape",
|
||||
@ -900,16 +905,6 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
@ -917,7 +912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.0",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -953,18 +948,6 @@ dependencies = [
|
||||
"similar",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "intl-memoizer"
|
||||
version = "0.5.1"
|
||||
@ -1071,7 +1054,7 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
"regex-automata 0.1.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1082,9 +1065,9 @@ checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
@ -1162,9 +1145,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.1"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
@ -1273,16 +1256,6 @@ dependencies = [
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "petgraph"
|
||||
version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap 1.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.12"
|
||||
@ -1329,18 +1302,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.66"
|
||||
version = "1.0.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.32"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -1356,13 +1329,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.1"
|
||||
version = "1.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
|
||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"aho-corasick 1.1.3",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-automata 0.4.7",
|
||||
"regex-syntax 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1371,7 +1345,18 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.6.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
|
||||
dependencies = [
|
||||
"aho-corasick 1.1.3",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1380,6 +1365,12 @@ version = "0.6.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.21"
|
||||
@ -1427,22 +1418,22 @@ checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.152"
|
||||
version = "1.0.204"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb"
|
||||
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.152"
|
||||
version = "1.0.204"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e"
|
||||
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1514,9 +1505,9 @@ checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.10.0"
|
||||
version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "snafu"
|
||||
@ -1559,6 +1550,12 @@ dependencies = [
|
||||
"lock_api",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
@ -1572,9 +1569,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.28"
|
||||
version = "2.0.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
|
||||
checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1668,17 +1665,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.3"
|
||||
version = "0.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||
checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db"
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.14"
|
||||
version = "0.22.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||
checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
@ -1818,6 +1815,12 @@ dependencies = [
|
||||
"rustc-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.16.0"
|
||||
@ -1892,7 +1895,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1989,10 +1991,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.61"
|
||||
name = "web-time"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
|
||||
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@ -2112,9 +2114,29 @@ checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.3"
|
||||
version = "0.6.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807"
|
||||
checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.72",
|
||||
]
|
||||
|
@ -5,10 +5,12 @@ members = [
|
||||
"hornbeam_interpreter",
|
||||
"hornbeam_macros",
|
||||
"hornbeam",
|
||||
"demo_hornbeam_project",
|
||||
"demo_hornbeam_project", "formbeam", "formbeam_derive",
|
||||
]
|
||||
|
||||
|
||||
[workspace.dependencies]
|
||||
bevy_reflect = "0.14.0"
|
||||
|
||||
# Enable optimisation for testing helpers
|
||||
[profile.dev.package.insta]
|
||||
|
@ -8,9 +8,15 @@ private = true
|
||||
|
||||
[dependencies]
|
||||
axum = "0.6.9"
|
||||
bevy_reflect.workspace = true
|
||||
eyre = "0.6.8"
|
||||
color-eyre = "0.6.2"
|
||||
tokio = { version = "1.25.0", features = ["full"] }
|
||||
tracing = "0.1.37"
|
||||
hornbeam = { version = "0.0.4", path = "../hornbeam" }
|
||||
hornbeam = { version = "0.0.4", path = "../hornbeam", features = ["formbeam"] }
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
|
||||
async-trait = "0.1.81"
|
||||
formbeam = { version = "0.0.4", path = "../formbeam" }
|
||||
formbeam_derive = { version = "0.0.4", path = "../formbeam_derive" }
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
|
@ -1,14 +1,20 @@
|
||||
use axum::extract::Path;
|
||||
use axum::extract::{self, Path};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{Html, IntoResponse, Response};
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use hornbeam::{initialise_template_manager, make_template_manager, render_template_string};
|
||||
use formbeam::traits::FormValidation;
|
||||
use formbeam::FormPartial;
|
||||
use hornbeam::{
|
||||
initialise_template_manager, make_template_manager, render_template_string, ReflectedForm,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use tracing::debug;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
|
||||
use formbeam_derive::Form;
|
||||
|
||||
make_template_manager! {
|
||||
static ref TEMPLATING = {
|
||||
default_locale: "en",
|
||||
@ -28,7 +34,9 @@ async fn main() -> eyre::Result<()> {
|
||||
|
||||
initialise_template_manager!(TEMPLATING);
|
||||
|
||||
let app = Router::new().route("/:lang/hello/:name", get(say_hello));
|
||||
let app = Router::new()
|
||||
.route("/:lang/hello/:name", get(say_hello))
|
||||
.route("/:lang/formdemo", get(form_demo));
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||
debug!("Listening on http://{}", addr);
|
||||
@ -60,3 +68,34 @@ impl IntoResponse for Rendered {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Form)]
|
||||
pub struct MyForm {
|
||||
#[form(min_chars(2), max_chars(10), regex(r"^[^_:]+$"))]
|
||||
name: String,
|
||||
#[form(email)]
|
||||
email: Option<String>,
|
||||
}
|
||||
|
||||
async fn form_demo(
|
||||
Path(lang): Path<String>,
|
||||
form_in: Option<extract::Form<MyFormRaw>>,
|
||||
) -> impl IntoResponse {
|
||||
let (form, valid_submission) = match form_in {
|
||||
Some(extract::Form(form_raw)) => {
|
||||
let validation = form_raw.validate().await.unwrap();
|
||||
|
||||
if validation.is_valid() {
|
||||
let submitted = form_raw.form().unwrap();
|
||||
(Default::default(), Some(format!("{submitted:?}")))
|
||||
} else {
|
||||
(ReflectedForm::new(form_raw, validation), None)
|
||||
}
|
||||
}
|
||||
None => (Default::default(), None),
|
||||
};
|
||||
|
||||
Rendered(render_template_string!(TEMPLATING, form_demo, lang, {
|
||||
form, valid_submission
|
||||
}))
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
if $errors.len() != 0
|
||||
"errors on this field (or 'form-wide' section):"
|
||||
ul
|
||||
for $e in $errors
|
||||
li
|
||||
"${$e.error_code()}"
|
@ -0,0 +1,19 @@
|
||||
"validators on this field:"
|
||||
ul
|
||||
for $fv in $form.info.field_validators($name)
|
||||
li
|
||||
match $fv
|
||||
MinLength($l) =>
|
||||
"minimum length: $l"
|
||||
MaxLength($l) =>
|
||||
"maximum length: $l"
|
||||
Required =>
|
||||
"this field is required"
|
||||
Email =>
|
||||
"e-mail address"
|
||||
Regex($pat) =>
|
||||
"matches regex: $pat"
|
||||
Custom($cust) =>
|
||||
"(custom) $cust"
|
||||
_ =>
|
||||
"(other)"
|
48
demo_hornbeam_project/templates/pages/form_demo.hnb
Normal file
48
demo_hornbeam_project/templates/pages/form_demo.hnb
Normal file
@ -0,0 +1,48 @@
|
||||
html
|
||||
head
|
||||
title
|
||||
"Hello!"
|
||||
|
||||
body
|
||||
h1
|
||||
"Form demo!"
|
||||
|
||||
|
||||
match $valid_submission
|
||||
Some($vs) =>
|
||||
"great, you managed to submit a form! $vs"
|
||||
br
|
||||
"You can send another one if you want :)."
|
||||
|
||||
None =>
|
||||
"Why don't you try submitting a form?"
|
||||
|
||||
hr
|
||||
|
||||
form {method="GET", action=""}
|
||||
ShowErrors {errors=$form.errors.form_wide}
|
||||
|
||||
b
|
||||
"Name"
|
||||
input {type="text", name="name", value=$form.raw.name.unwrap_or("")}
|
||||
|
||||
br
|
||||
ShowValidators {$form, name="name"}
|
||||
br
|
||||
ShowErrors {errors=$form.errors.name}
|
||||
|
||||
hr
|
||||
|
||||
b
|
||||
"E-mail address"
|
||||
input {type="email", name="email", value=$form.raw.email.unwrap_or("")}
|
||||
|
||||
br
|
||||
ShowValidators {$form, name="email"}
|
||||
br
|
||||
ShowErrors {errors=$form.errors.email}
|
||||
|
||||
hr
|
||||
|
||||
button {type="submit"}
|
||||
"Submit!"
|
12
flake.lock
generated
12
flake.lock
generated
@ -30,11 +30,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1717827974,
|
||||
"narHash": "sha256-ixopuTeTouxqTxfMuzs6IaRttbT8JqRW5C9Q/57WxQw=",
|
||||
"lastModified": 1722148092,
|
||||
"narHash": "sha256-5QS64rfIFDzU1jmZrOK6wyZOCi6Vn/90apWRI6Hy+xk=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "ab655c627777ab5f9964652fe23bbb1dfbd687a8",
|
||||
"rev": "b39d8959f286dc7b9da91ae92f6af56de0169e87",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -238,11 +238,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1717583671,
|
||||
"narHash": "sha256-+lRAmz92CNUxorqWusgJbL9VE1eKCnQQojglRemzwkw=",
|
||||
"lastModified": 1722099723,
|
||||
"narHash": "sha256-61f+rvQAObm/TuBEqYFNUTngm/wXcuNhGtQbAmfZVvY=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "48bbdd6a74f3176987d5c809894ac33957000d19",
|
||||
"rev": "a46788318cce3b62e14606f70a14896b223ee5ec",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -44,6 +44,9 @@
|
||||
# Releasing a full workspace of packages
|
||||
pkgs.cargo-workspaces
|
||||
|
||||
# Macro debugging
|
||||
pkgs.cargo-expand
|
||||
|
||||
pkgs.grass-sass
|
||||
pkgs.entr
|
||||
|
||||
|
10
formbeam/Cargo.toml
Normal file
10
formbeam/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "formbeam"
|
||||
version = "0.0.4"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.81"
|
||||
regex = "1.10.5"
|
||||
bevy_reflect.workspace = true
|
||||
static_assertions = "1.1.0"
|
81
formbeam/src/errors.rs
Normal file
81
formbeam/src/errors.rs
Normal file
@ -0,0 +1,81 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
use bevy_reflect::{FromReflect, Reflect, TypePath};
|
||||
use static_assertions::assert_impl_all;
|
||||
|
||||
#[derive(Clone, Reflect)]
|
||||
// This makes the struct opaque to the reflection engine, meaning it will
|
||||
// be cloned absolutely instead of being converted to a DynamicEnum.
|
||||
// However it won't implement Enum. I still think that's preferable, so `error_code()` will work etc.`
|
||||
#[reflect_value]
|
||||
pub enum FieldError {
|
||||
Missing,
|
||||
|
||||
TooShort {
|
||||
current: u32,
|
||||
min_length: u32,
|
||||
max_length: u32,
|
||||
unit: FieldUnit,
|
||||
},
|
||||
TooLong {
|
||||
current: u32,
|
||||
min_length: u32,
|
||||
max_length: u32,
|
||||
unit: FieldUnit,
|
||||
},
|
||||
|
||||
Custom {
|
||||
code: String,
|
||||
description: String,
|
||||
values: BTreeMap<String, String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl FieldError {
|
||||
pub fn error_code(&self) -> &str {
|
||||
match self {
|
||||
FieldError::Missing => "missing",
|
||||
FieldError::TooShort {
|
||||
unit: FieldUnit::Characters,
|
||||
..
|
||||
} => "too_short_chars",
|
||||
FieldError::TooLong {
|
||||
unit: FieldUnit::Characters,
|
||||
..
|
||||
} => "too_long_chars",
|
||||
FieldError::Custom { code, .. } => code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Reflect)]
|
||||
pub enum FieldUnit {
|
||||
Characters,
|
||||
}
|
||||
|
||||
impl Display for FieldUnit {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FieldUnit::Characters => write!(f, "characters"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for FieldError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
FieldError::Missing => write!(f, "The field is missing"),
|
||||
FieldError::TooShort { current, min_length, max_length, unit } => write!(f, "The field is too short ({current}); it should be between {min_length} and {max_length} {unit}."),
|
||||
FieldError::TooLong { current, min_length, max_length, unit } => write!(f, "The field is too long ({current}); it should be between {min_length} and {max_length} {unit}."),
|
||||
FieldError::Custom {
|
||||
code,
|
||||
description,
|
||||
values,
|
||||
} => write!(f, "[{code}] {description} {values:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type FieldErrors = Vec<FieldError>;
|
||||
assert_impl_all!(FieldErrors: Reflect, FromReflect, TypePath);
|
9
formbeam/src/lib.rs
Normal file
9
formbeam/src/lib.rs
Normal file
@ -0,0 +1,9 @@
|
||||
pub mod errors;
|
||||
pub mod traits;
|
||||
pub mod validators;
|
||||
|
||||
pub use errors::{FieldError, FieldErrors};
|
||||
pub use traits::{
|
||||
FieldInfo, FieldValidator, FieldValidatorInfo, Form, FormPartial, FormPartialInfo,
|
||||
FormValidator,
|
||||
};
|
98
formbeam/src/traits.rs
Normal file
98
formbeam/src/traits.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::errors::FieldErrors;
|
||||
|
||||
#[async_trait]
|
||||
pub trait FormValidator<F: Form, C: Send + 'static> {
|
||||
type Error: Send + 'static;
|
||||
|
||||
async fn validate(&self, form: &mut F, context: &mut C) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait FieldValidator<C: Send + 'static> {
|
||||
type Error: Send + 'static;
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
value: &str,
|
||||
errors: &mut FieldErrors,
|
||||
context: &mut C,
|
||||
) -> Result<(), Self::Error>;
|
||||
}
|
||||
|
||||
/// The type of a realised, fully validated and populated, form.
|
||||
pub trait Form: Sized + 'static {
|
||||
type Partial: FormPartial<Form = Self>;
|
||||
}
|
||||
|
||||
/// The type of a partially populated and as-yet-unvalidated form.
|
||||
/// Structs for and implementations of this trait can be generated via derive macro.
|
||||
#[async_trait]
|
||||
pub trait FormPartial {
|
||||
type Form: Form<Partial = Self>;
|
||||
type Validation: FormValidation<Partial = Self>;
|
||||
type Error: Send + 'static;
|
||||
|
||||
/// Converts the partial into a form.
|
||||
///
|
||||
/// # Preconditions
|
||||
///
|
||||
/// - The form should already have been validated.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Only structural/type errors will be returned here, with only the name of the field
|
||||
/// to be returned.
|
||||
///
|
||||
/// No other validation is performed here.
|
||||
fn form(&self) -> Result<Self::Form, &'static str>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Runs all the validators on the form and calculates errors.
|
||||
///
|
||||
/// Should only be called once.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns direct errors from validators if one was thrown.
|
||||
async fn validate(&self) -> Result<Self::Validation, Self::Error>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
const INFO: &'static FormPartialInfo;
|
||||
|
||||
fn validator_info(&self) -> &'static FormPartialInfo {
|
||||
Self::INFO
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of validation.
|
||||
pub trait FormValidation {
|
||||
type Partial: FormPartial<Validation = Self>;
|
||||
|
||||
/// Returns true if the form is valid.
|
||||
fn is_valid(&self) -> bool;
|
||||
}
|
||||
|
||||
pub struct FormPartialInfo {
|
||||
pub form_validators: &'static [&'static str],
|
||||
pub fields: &'static [FieldInfo],
|
||||
}
|
||||
|
||||
pub struct FieldInfo {
|
||||
pub name: &'static str,
|
||||
pub validators: &'static [FieldValidatorInfo],
|
||||
}
|
||||
|
||||
pub enum FieldValidatorInfo {
|
||||
MinLength(u32),
|
||||
MaxLength(u32),
|
||||
MinValue(i64),
|
||||
MaxValue(i64),
|
||||
Required,
|
||||
Email,
|
||||
Regex(&'static str),
|
||||
Custom(&'static str),
|
||||
}
|
102
formbeam/src/validators.rs
Normal file
102
formbeam/src/validators.rs
Normal file
@ -0,0 +1,102 @@
|
||||
use std::{collections::BTreeMap, convert::Infallible, sync::LazyLock};
|
||||
|
||||
use async_trait::async_trait;
|
||||
pub use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
errors::{FieldError, FieldErrors, FieldUnit},
|
||||
traits::FieldValidator,
|
||||
};
|
||||
|
||||
/// Constrains the minimum and maximum length, counted in `char`s, of a text input.
|
||||
pub struct LengthInChars {
|
||||
pub min_length: u32,
|
||||
pub max_length: u32,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FieldValidator<()> for LengthInChars {
|
||||
type Error = Infallible;
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
value: &str,
|
||||
errors: &mut FieldErrors,
|
||||
_context: &mut (),
|
||||
) -> Result<(), Self::Error> {
|
||||
// Clamp to u32::MAX
|
||||
let length = value.chars().count().min(u32::MAX as usize) as u32;
|
||||
|
||||
if length < self.min_length {
|
||||
errors.push(FieldError::TooShort {
|
||||
current: length,
|
||||
min_length: self.min_length,
|
||||
max_length: self.max_length,
|
||||
unit: FieldUnit::Characters,
|
||||
});
|
||||
}
|
||||
|
||||
if length > self.max_length {
|
||||
errors.push(FieldError::TooLong {
|
||||
current: length,
|
||||
min_length: self.min_length,
|
||||
max_length: self.max_length,
|
||||
unit: FieldUnit::Characters,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Regex for an e-mail address according to
|
||||
/// <https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address>
|
||||
///
|
||||
/// This is likely representative of what browsers accept, but note that
|
||||
/// it intentionally deviates from RFC 5322 which is not considered practical.
|
||||
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$").expect("email regex")
|
||||
});
|
||||
|
||||
pub struct Email;
|
||||
|
||||
#[async_trait]
|
||||
impl FieldValidator<()> for Email {
|
||||
type Error = Infallible;
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
value: &str,
|
||||
errors: &mut FieldErrors,
|
||||
_context: &mut (),
|
||||
) -> Result<(), Self::Error> {
|
||||
if !EMAIL_REGEX.is_match_at(value, 0) {
|
||||
errors.push(FieldError::Custom {
|
||||
code: "invalid_email".to_owned(),
|
||||
description: "Not a valid e-mail address".to_owned(),
|
||||
values: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FieldValidator<()> for Regex {
|
||||
type Error = Infallible;
|
||||
|
||||
async fn validate(
|
||||
&self,
|
||||
value: &str,
|
||||
errors: &mut FieldErrors,
|
||||
_context: &mut (),
|
||||
) -> Result<(), Self::Error> {
|
||||
if !self.is_match_at(value, 0) {
|
||||
errors.push(FieldError::Custom {
|
||||
code: "regex_unmatched".to_owned(),
|
||||
description: "Field did not match the intended pattern".to_owned(),
|
||||
values: BTreeMap::new(),
|
||||
})
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
12
formbeam_derive/Cargo.toml
Normal file
12
formbeam_derive/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "formbeam_derive"
|
||||
version = "0.0.4"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0.86"
|
||||
quote = "1.0.36"
|
||||
syn = "2.0.72"
|
505
formbeam_derive/src/derive_form.rs
Normal file
505
formbeam_derive/src/derive_form.rs
Normal file
@ -0,0 +1,505 @@
|
||||
use proc_macro2::Ident;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::format_ident;
|
||||
use quote::quote;
|
||||
use syn::parenthesized;
|
||||
use syn::DeriveInput;
|
||||
use syn::Field;
|
||||
use syn::LitInt;
|
||||
use syn::LitStr;
|
||||
use syn::Type;
|
||||
use syn::Visibility;
|
||||
|
||||
pub fn derive_form(input: DeriveInput) -> TokenStream {
|
||||
let fields = match input.data {
|
||||
syn::Data::Struct(strukt) => match strukt.fields {
|
||||
syn::Fields::Named(named) => named,
|
||||
syn::Fields::Unnamed(_) => panic!("cannot derive Form for a tuple struct"),
|
||||
syn::Fields::Unit => panic!("cannot derive Form for a unit struct"),
|
||||
},
|
||||
syn::Data::Enum(_) => panic!("cannot derive Form for an enum"),
|
||||
syn::Data::Union(_) => panic!("cannot derive Form for a union"),
|
||||
};
|
||||
|
||||
let mut fields_vec = Vec::new();
|
||||
for pair in fields.named.into_pairs() {
|
||||
let field = pair.into_value();
|
||||
let f_info = parse_field_attrs(&field);
|
||||
fields_vec.push((field, f_info));
|
||||
}
|
||||
|
||||
let partial_ident = format_ident!("{}Raw", input.ident);
|
||||
let validation_ident = format_ident!("{}Validation", input.ident);
|
||||
|
||||
let partial_struct = write_form_partial_struct(&partial_ident, &fields_vec, &input.vis);
|
||||
let validation_struct =
|
||||
write_form_validation_struct(&partial_ident, &validation_ident, &fields_vec, &input.vis);
|
||||
let partial_impl =
|
||||
write_form_partial_impl(&input.ident, &partial_ident, &validation_ident, &fields_vec);
|
||||
|
||||
quote!(
|
||||
#partial_struct
|
||||
#validation_struct
|
||||
|
||||
#partial_impl
|
||||
)
|
||||
}
|
||||
|
||||
fn identify_base_type(unprefixed: &Ident) -> FieldType {
|
||||
match unprefixed.to_string().as_str() {
|
||||
"u8" | "i8" | "u16" | "i16" | "u32" | "i32" | "u64" | "i64" | "u128" | "i128" | "f32"
|
||||
| "f64" => FieldType::Numeric,
|
||||
"String" => FieldType::String,
|
||||
other => {
|
||||
panic!("unsupported field type: `{other}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn identify_type(ty: &Type) -> (FieldType, bool) {
|
||||
let (ftype, needed) = match ty {
|
||||
syn::Type::Array(_) => panic!("unsupported field type: array"),
|
||||
syn::Type::BareFn(_) => panic!("unsupported field type: bare fn"),
|
||||
syn::Type::Group(_) => panic!("unsupported field type: Group"),
|
||||
syn::Type::ImplTrait(_) => panic!("unsupported field type: impl Trait"),
|
||||
syn::Type::Infer(_) => panic!("unsupported field type: Infer"),
|
||||
syn::Type::Macro(_) => panic!("unsupported field type: macro"),
|
||||
syn::Type::Never(_) => panic!("unsupported field type: Never"),
|
||||
syn::Type::Paren(_) => panic!("unsupported field type: parenthesised"),
|
||||
syn::Type::Path(type_path) => {
|
||||
let path = &type_path.path;
|
||||
if path.segments.len() != 1 {
|
||||
panic!("unsupported field type syntax (can't deal with multiple path segments)");
|
||||
}
|
||||
let segment = path.segments.get(0).unwrap();
|
||||
|
||||
if segment.ident == "Option" {
|
||||
match &segment.arguments {
|
||||
syn::PathArguments::None => {
|
||||
panic!("unsupported field type: Option without args")
|
||||
}
|
||||
syn::PathArguments::AngleBracketed(bracks) => {
|
||||
if bracks.args.len() != 1 {
|
||||
panic!("unsupported field type: Option with other than 1 arg");
|
||||
}
|
||||
let arg = bracks.args.get(0).unwrap();
|
||||
match arg {
|
||||
syn::GenericArgument::Lifetime(_) => {
|
||||
panic!("unsupported field type with lifetime")
|
||||
}
|
||||
syn::GenericArgument::Type(ty) => {
|
||||
let (base_type, check_needed) = identify_type(ty);
|
||||
if !check_needed {
|
||||
panic!("unsupported field type with nested Options or something like that");
|
||||
}
|
||||
(base_type, false)
|
||||
}
|
||||
syn::GenericArgument::Const(_) => {
|
||||
panic!("unsupported field type with const")
|
||||
}
|
||||
syn::GenericArgument::AssocType(_) => {
|
||||
panic!("unsupported field type with associated type")
|
||||
}
|
||||
syn::GenericArgument::AssocConst(_) => {
|
||||
panic!("unsupported field type with associated const")
|
||||
}
|
||||
syn::GenericArgument::Constraint(_) => {
|
||||
panic!("unsupported field type with constraint")
|
||||
}
|
||||
_ => panic!("other generic argument unsupported?"),
|
||||
}
|
||||
}
|
||||
syn::PathArguments::Parenthesized(_) => {
|
||||
panic!("unsupported field type with ( )s")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match &segment.arguments {
|
||||
syn::PathArguments::None => (identify_base_type(&segment.ident), true),
|
||||
syn::PathArguments::AngleBracketed(_) => {
|
||||
panic!("unsupported field type: non-Option with args")
|
||||
}
|
||||
syn::PathArguments::Parenthesized(_) => {
|
||||
panic!("unsupported field type with ( )s")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
syn::Type::Ptr(_) => panic!("unsupported field type: pointer"),
|
||||
syn::Type::Reference(_) => panic!("unsupported field type: ref"),
|
||||
syn::Type::Slice(_) => panic!("unsupported field type: slice"),
|
||||
syn::Type::TraitObject(_) => panic!("unsupported field type: trait object"),
|
||||
syn::Type::Tuple(_) => panic!("unsupported field type: tuple"),
|
||||
syn::Type::Verbatim(_) => panic!("verbatim?"),
|
||||
_ => panic!("unsupported (unknown) field type"),
|
||||
};
|
||||
(ftype, needed)
|
||||
}
|
||||
|
||||
fn parse_field_attrs(f: &Field) -> FieldInfo {
|
||||
let (ftype, needed) = identify_type(&f.ty);
|
||||
let mut out = FieldInfo {
|
||||
ftype,
|
||||
needed,
|
||||
need_nonempty: true,
|
||||
min_chars: None,
|
||||
max_chars: None,
|
||||
email: false,
|
||||
regex: None,
|
||||
};
|
||||
|
||||
for attr in &f.attrs {
|
||||
if !attr.meta.path().is_ident("form") {
|
||||
continue;
|
||||
}
|
||||
|
||||
attr.parse_nested_meta(|meta| {
|
||||
// #[form(allow_empty)]
|
||||
if meta.path.is_ident("allow_empty") {
|
||||
out.need_nonempty = false;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// #[form(email)]
|
||||
if meta.path.is_ident("email") {
|
||||
if out.email {
|
||||
panic!("duplicate email annotation");
|
||||
}
|
||||
out.email = true;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// #[form(min_chars = 2)]
|
||||
if meta.path.is_ident("min_chars") {
|
||||
let content;
|
||||
parenthesized!(content in meta.input);
|
||||
let lit: LitInt = content.parse()?;
|
||||
let n: u32 = lit.base10_parse()?;
|
||||
if out.min_chars.is_some() {
|
||||
panic!("duplicate min_chars annotation");
|
||||
}
|
||||
out.min_chars = Some(n);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// #[form(max_chars = 2)]
|
||||
if meta.path.is_ident("max_chars") {
|
||||
let content;
|
||||
parenthesized!(content in meta.input);
|
||||
let lit: LitInt = content.parse()?;
|
||||
let n: u32 = lit.base10_parse()?;
|
||||
if out.max_chars.is_some() {
|
||||
panic!("duplicate max_chars annotation");
|
||||
}
|
||||
out.max_chars = Some(n);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// #[form(regex = "\A[0-9]+\Z")]
|
||||
if meta.path.is_ident("regex") {
|
||||
let content;
|
||||
parenthesized!(content in meta.input);
|
||||
let lit: LitStr = content.parse()?;
|
||||
if out.regex.is_some() {
|
||||
panic!("duplicate regex annotation");
|
||||
}
|
||||
out.regex = Some(lit.value());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
panic!(
|
||||
"unrecognised field attribute: {}",
|
||||
meta.path
|
||||
.get_ident()
|
||||
.map(|i| format!("`{i}`"))
|
||||
.unwrap_or_else(|| "<???>".to_owned())
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn write_form_partial_struct(
|
||||
partial_ident: &Ident,
|
||||
fields: &[(Field, FieldInfo)],
|
||||
vis: &Visibility,
|
||||
) -> TokenStream {
|
||||
let field_idents: Vec<&Ident> = fields
|
||||
.iter()
|
||||
.map(|(f, _)| f.ident.as_ref().unwrap())
|
||||
.collect();
|
||||
|
||||
quote!(
|
||||
#[derive(Default, Debug, ::bevy_reflect::Reflect, ::serde::Deserialize)]
|
||||
#vis struct #partial_ident {
|
||||
#(#field_idents: Option<String>),*
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn write_form_validation_struct(
|
||||
partial_ident: &Ident,
|
||||
validation_ident: &Ident,
|
||||
fields: &[(Field, FieldInfo)],
|
||||
vis: &Visibility,
|
||||
) -> TokenStream {
|
||||
let field_idents: Vec<&Ident> = fields
|
||||
.iter()
|
||||
.map(|(f, _)| f.ident.as_ref().unwrap())
|
||||
.collect();
|
||||
|
||||
quote!(
|
||||
#[derive(Default, Debug, ::bevy_reflect::Reflect)]
|
||||
#vis struct #validation_ident {
|
||||
form_wide: ::formbeam::FieldErrors,
|
||||
#(#field_idents: ::formbeam::FieldErrors),*
|
||||
}
|
||||
|
||||
impl ::formbeam::traits::FormValidation for #validation_ident {
|
||||
type Partial = #partial_ident;
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
self.form_wide.is_empty()
|
||||
#(&& self.#field_idents.is_empty())*
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn write_form_partial_impl(
|
||||
form_ident: &Ident,
|
||||
partial_ident: &Ident,
|
||||
validation_ident: &Ident,
|
||||
fields: &[(Field, FieldInfo)],
|
||||
) -> TokenStream {
|
||||
let validate_body = write_partial_impl_validate(validation_ident, fields);
|
||||
let form_method_body = write_partial_impl_form_method(form_ident, fields);
|
||||
let form_info = write_form_info(fields);
|
||||
quote!(
|
||||
impl ::formbeam::Form for #form_ident {
|
||||
type Partial = #partial_ident;
|
||||
}
|
||||
|
||||
#[::async_trait::async_trait]
|
||||
impl ::formbeam::FormPartial for #partial_ident {
|
||||
type Form = #form_ident;
|
||||
type Validation = #validation_ident;
|
||||
type Error = ::core::convert::Infallible; // TODO
|
||||
|
||||
const INFO: &'static ::formbeam::FormPartialInfo = &#form_info;
|
||||
|
||||
async fn validate(&self) -> Result<Self::Validation, Self::Error> {
|
||||
use ::formbeam::FieldValidator;
|
||||
#validate_body
|
||||
}
|
||||
|
||||
fn form(&self) -> Result<Self::Form, &'static str> {
|
||||
#form_method_body
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn write_partial_impl_validate(
|
||||
validation_ident: &Ident,
|
||||
fields: &[(Field, FieldInfo)],
|
||||
) -> TokenStream {
|
||||
let field_statements = fields.iter().map(write_validate_statement);
|
||||
quote!(
|
||||
let mut errors = #validation_ident::default();
|
||||
|
||||
#(#field_statements)*
|
||||
|
||||
Ok(errors)
|
||||
)
|
||||
}
|
||||
|
||||
struct FieldInfo {
|
||||
ftype: FieldType,
|
||||
needed: bool,
|
||||
need_nonempty: bool,
|
||||
min_chars: Option<u32>,
|
||||
max_chars: Option<u32>,
|
||||
email: bool,
|
||||
regex: Option<String>,
|
||||
}
|
||||
|
||||
enum FieldType {
|
||||
Numeric,
|
||||
String,
|
||||
}
|
||||
|
||||
fn write_form_info(fields: &[(Field, FieldInfo)]) -> TokenStream {
|
||||
let form_validators: Vec<TokenStream> = Vec::new();
|
||||
let fields: Vec<TokenStream> = fields
|
||||
.iter()
|
||||
.map(|(f, fi)| write_form_info_for_field(f, fi))
|
||||
.collect();
|
||||
quote!(
|
||||
::formbeam::FormPartialInfo {
|
||||
form_validators: &[
|
||||
#(#form_validators),*
|
||||
],
|
||||
fields: &[
|
||||
#(#fields),*
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn write_form_info_for_field(field: &Field, f_info: &FieldInfo) -> TokenStream {
|
||||
let name = field.ident.as_ref().unwrap().to_string();
|
||||
let mut validators = Vec::new();
|
||||
|
||||
if f_info.need_nonempty {
|
||||
validators.push(quote!(::formbeam::FieldValidatorInfo::Required));
|
||||
}
|
||||
|
||||
if let Some(l) = f_info.min_chars {
|
||||
validators.push(quote!(::formbeam::FieldValidatorInfo::MinLength(#l)));
|
||||
}
|
||||
if let Some(l) = f_info.max_chars {
|
||||
validators.push(quote!(::formbeam::FieldValidatorInfo::MaxLength(#l)));
|
||||
}
|
||||
if f_info.email {
|
||||
validators.push(quote!(::formbeam::FieldValidatorInfo::Email));
|
||||
}
|
||||
if let Some(r) = &f_info.regex {
|
||||
validators.push(quote!(::formbeam::FieldValidatorInfo::Regex(#r)));
|
||||
}
|
||||
|
||||
quote!(
|
||||
::formbeam::FieldInfo {
|
||||
name: #name,
|
||||
validators: &[#(#validators),*]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn write_validate_statement((field, f_info): &(Field, FieldInfo)) -> TokenStream {
|
||||
let f = field.ident.as_ref().unwrap();
|
||||
|
||||
let configured_validators = write_validators(field, f_info);
|
||||
|
||||
let check_nonempty = if f_info.need_nonempty {
|
||||
quote!(
|
||||
if field.is_empty() {
|
||||
errors.#f.push(::formbeam::FieldError::Missing);
|
||||
} else {
|
||||
#(#configured_validators)*
|
||||
}
|
||||
)
|
||||
} else {
|
||||
quote!(#(#configured_validators)*)
|
||||
};
|
||||
|
||||
let else_not_present = if f_info.needed {
|
||||
quote!(
|
||||
errors.#f.push(::formbeam::FieldError::Missing);
|
||||
)
|
||||
} else {
|
||||
TokenStream::new()
|
||||
};
|
||||
|
||||
quote!(
|
||||
if let Some(field) = &self.#f {
|
||||
#check_nonempty
|
||||
} else {
|
||||
#else_not_present
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fn write_validators(field: &Field, f_info: &FieldInfo) -> Vec<TokenStream> {
|
||||
let f = field.ident.as_ref().unwrap();
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
if f_info.min_chars.is_some() || f_info.max_chars.is_some() {
|
||||
let effective_min = f_info.min_chars.unwrap_or(0);
|
||||
let effective_max = f_info.max_chars.unwrap_or(u32::MAX);
|
||||
|
||||
out.push(quote!(
|
||||
::formbeam::validators::LengthInChars { min_length: #effective_min, max_length: #effective_max }.validate(&field, &mut errors.#f, &mut ()).await?;
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(regex) = &f_info.regex {
|
||||
let regex_ident = format_ident!("REGEX_{}", f.to_string().to_uppercase());
|
||||
out.push(quote!(
|
||||
static #regex_ident: ::std::sync::LazyLock<::formbeam::validators::Regex> = ::std::sync::LazyLock::new(|| {
|
||||
::formbeam::validators::Regex::new(#regex).expect("invalid regex")
|
||||
});
|
||||
#regex_ident.validate(&field, &mut errors.#f, &mut ()).await?;
|
||||
));
|
||||
}
|
||||
|
||||
if f_info.email {
|
||||
out.push(quote!(
|
||||
::formbeam::validators::Email.validate(&field, &mut errors.#f, &mut ()).await?;
|
||||
));
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn write_partial_impl_form_method(
|
||||
form_ident: &Ident,
|
||||
fields: &[(Field, FieldInfo)],
|
||||
) -> TokenStream {
|
||||
let field_idents: Vec<&Ident> = fields
|
||||
.iter()
|
||||
.map(|(f, _)| f.ident.as_ref().unwrap())
|
||||
.collect();
|
||||
|
||||
let mut statements = Vec::new();
|
||||
|
||||
for (field, field_info) in fields.iter() {
|
||||
let f = field.ident.as_ref().unwrap();
|
||||
|
||||
let none_case = if field_info.needed {
|
||||
let f_name = f.to_string();
|
||||
quote!(return Err(#f_name);)
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
|
||||
let some_case = {
|
||||
let converter = converter_from_raw_to_field(f, field_info);
|
||||
|
||||
if field_info.needed {
|
||||
converter
|
||||
} else {
|
||||
quote!(Some({#converter}))
|
||||
}
|
||||
};
|
||||
|
||||
statements.push(quote!(
|
||||
let #f = match &self.#f {
|
||||
Some(raw) => {
|
||||
#some_case
|
||||
},
|
||||
None => {
|
||||
#none_case
|
||||
}
|
||||
};
|
||||
))
|
||||
}
|
||||
|
||||
quote!(
|
||||
#(#statements)*
|
||||
Ok(#form_ident { #(#field_idents),* })
|
||||
)
|
||||
}
|
||||
|
||||
fn converter_from_raw_to_field(f: &Ident, field_info: &FieldInfo) -> TokenStream {
|
||||
let f_name = f.to_string();
|
||||
match field_info.ftype {
|
||||
FieldType::Numeric => quote!(
|
||||
raw.parse().map_err(|_| #f_name)?
|
||||
),
|
||||
FieldType::String => quote!(raw.to_owned()),
|
||||
}
|
||||
}
|
10
formbeam_derive/src/lib.rs
Normal file
10
formbeam_derive/src/lib.rs
Normal file
@ -0,0 +1,10 @@
|
||||
use proc_macro::TokenStream;
|
||||
use syn::parse_macro_input;
|
||||
|
||||
mod derive_form;
|
||||
|
||||
#[proc_macro_derive(Form, attributes(form))]
|
||||
pub fn my_proc_macro(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as syn::DeriveInput);
|
||||
derive_form::derive_form(input).into()
|
||||
}
|
@ -21,3 +21,4 @@ axum = { version = "0.6.10", optional = true }
|
||||
default = ["interpreted", "hot_reload"]
|
||||
interpreted = []
|
||||
hot_reload = ["tokio", "axum"]
|
||||
formbeam = ["hornbeam_interpreter/formbeam"]
|
||||
|
@ -115,6 +115,11 @@ fn load_new_template_system(
|
||||
|
||||
#[cfg(feature = "hot_reload")]
|
||||
pub fn is_hot_reload_enabled() -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
const DEFAULT_HOT_RELOAD: bool = true;
|
||||
#[cfg(not(debug_assertions))]
|
||||
const DEFAULT_HOT_RELOAD: bool = false;
|
||||
|
||||
std::env::var("HORNBEAM_HOT")
|
||||
.map(|env_var| {
|
||||
if let Ok(i) = env_var.parse::<u32>() {
|
||||
@ -130,9 +135,9 @@ pub fn is_hot_reload_enabled() -> bool {
|
||||
return false;
|
||||
}
|
||||
eprintln!("Not sure how to interpret HORNBEAM_HOT={env_var:?}, assuming yes.");
|
||||
return true;
|
||||
true
|
||||
})
|
||||
.unwrap_or(true)
|
||||
.unwrap_or(DEFAULT_HOT_RELOAD)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "hot_reload"))]
|
||||
|
@ -17,3 +17,7 @@ pub use interpreted::{
|
||||
is_hot_reload_enabled, lazy_static, new_template_manager, Params, TemplateError,
|
||||
TemplateManager,
|
||||
};
|
||||
|
||||
#[cfg(feature = "formbeam")]
|
||||
#[cfg(feature = "interpreted")]
|
||||
pub use hornbeam_interpreter::formbeam_integration::ReflectedForm;
|
||||
|
@ -12,8 +12,9 @@ hornbeam_grammar = { version = "0.0.4", path = "../hornbeam_grammar" }
|
||||
hornbeam_ir = { version = "0.0.4", path = "../hornbeam_ir" }
|
||||
|
||||
fluent-templates = { version = "0.8.0", optional = true }
|
||||
bevy_reflect = { version = "0.11.0" }
|
||||
bevy_reflect.workspace = true
|
||||
html-escape = "0.2.13"
|
||||
formbeam = { version = "0.0.4", path = "../formbeam", optional = true }
|
||||
|
||||
walkdir = "2.3.2"
|
||||
|
||||
@ -29,7 +30,8 @@ percent-encoding = "2.2.0"
|
||||
|
||||
[features]
|
||||
default = ["fluent"]
|
||||
fluent = ["fluent-templates"]
|
||||
fluent = ["dep:fluent-templates"]
|
||||
formbeam = ["dep:formbeam"]
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1.38.0"
|
||||
|
@ -52,7 +52,13 @@ impl Value {
|
||||
Value::Int(_) => "Int",
|
||||
Value::Bool(_) => "Bool",
|
||||
Value::List(_) => "List",
|
||||
Value::Reflective(reflective) => reflective.type_name(),
|
||||
// TODO get rid of unwraps
|
||||
Value::Reflective(reflective) => reflective
|
||||
.get_represented_type_info()
|
||||
.unwrap()
|
||||
.type_path_table()
|
||||
.ident()
|
||||
.unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -761,7 +767,11 @@ impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpret
|
||||
Expression::Variable { name, loc } => {
|
||||
let locals = &self.scopes[scope_idx].variables;
|
||||
match locals.get(name as &str) {
|
||||
Some(variable_value) => Ok(variable_value.clone()),
|
||||
Some(variable_value) => {
|
||||
let new = variable_value.clone();
|
||||
eprintln!("{variable_value:?} -> {new:?}");
|
||||
Ok(variable_value.clone())
|
||||
}
|
||||
None => {
|
||||
let locals_list = locals.keys().join(", ");
|
||||
Err(InterpreterError::TypeError {
|
||||
|
62
hornbeam_interpreter/src/formbeam_integration.rs
Normal file
62
hornbeam_interpreter/src/formbeam_integration.rs
Normal file
@ -0,0 +1,62 @@
|
||||
use bevy_reflect::Reflect;
|
||||
use formbeam::{FieldValidatorInfo, FormPartial, FormPartialInfo};
|
||||
|
||||
#[derive(Reflect)]
|
||||
pub struct ReflectedForm<P: FormPartial> {
|
||||
pub raw: P,
|
||||
pub errors: P::Validation,
|
||||
pub info: InfoWrapper,
|
||||
}
|
||||
|
||||
impl<P: FormPartial> ReflectedForm<P> {
|
||||
pub fn new(raw: P, errors: P::Validation) -> Self {
|
||||
Self {
|
||||
raw,
|
||||
errors,
|
||||
info: InfoWrapper(Some(P::INFO)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: FormPartial + Default> Default for ReflectedForm<P>
|
||||
where
|
||||
P::Validation: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new(Default::default(), Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Reflect)]
|
||||
// This makes the struct opaque to the reflection engine, meaning it will
|
||||
// be cloned absolutely instead of being converted to a DynamicTupleStruct.
|
||||
// However it won't implement TupleStruct. But that's fine, that's actually what we want!
|
||||
#[reflect_value]
|
||||
pub struct InfoWrapper(#[reflect(ignore)] pub(crate) Option<&'static FormPartialInfo>);
|
||||
|
||||
#[derive(Clone, Debug, Reflect)]
|
||||
pub enum ReflectFieldValidatorInfo {
|
||||
MinLength(u32),
|
||||
MaxLength(u32),
|
||||
MinValue(i64),
|
||||
MaxValue(i64),
|
||||
Required,
|
||||
Email,
|
||||
Regex(String),
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&FieldValidatorInfo> for ReflectFieldValidatorInfo {
|
||||
fn from(value: &FieldValidatorInfo) -> Self {
|
||||
match value {
|
||||
FieldValidatorInfo::MinLength(m) => ReflectFieldValidatorInfo::MinLength(*m),
|
||||
FieldValidatorInfo::MaxLength(m) => ReflectFieldValidatorInfo::MaxLength(*m),
|
||||
FieldValidatorInfo::MinValue(m) => ReflectFieldValidatorInfo::MinValue(*m),
|
||||
FieldValidatorInfo::MaxValue(m) => ReflectFieldValidatorInfo::MaxValue(*m),
|
||||
FieldValidatorInfo::Required => ReflectFieldValidatorInfo::Required,
|
||||
FieldValidatorInfo::Email => ReflectFieldValidatorInfo::Email,
|
||||
FieldValidatorInfo::Regex(s) => ReflectFieldValidatorInfo::Regex((*s).to_owned()),
|
||||
FieldValidatorInfo::Custom(s) => ReflectFieldValidatorInfo::Custom((*s).to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
use crate::interface::Value;
|
||||
|
||||
pub(crate) mod defaults;
|
||||
#[cfg(feature = "formbeam")]
|
||||
pub(crate) mod formbeam_integration;
|
||||
|
||||
/// A method that can be accessed (called) by templates.
|
||||
/// There is no dynamic dispatch for methods: the name of the method is the only thing that determines which one to call,
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use bevy_reflect::ReflectRef;
|
||||
use bevy_reflect::{ReflectRef, VariantType};
|
||||
use percent_encoding::NON_ALPHANUMERIC;
|
||||
|
||||
use crate::interface::Value;
|
||||
@ -15,6 +15,7 @@ const DEFAULT_TEMPLATE_ACCESSIBLE_METHODS: &'static [(
|
||||
("urlencode", urlencode),
|
||||
("len", len),
|
||||
("split", split),
|
||||
("unwrap_or", unwrap_or),
|
||||
];
|
||||
|
||||
/// Return a map of the default suggested template-accessible methods.
|
||||
@ -128,7 +129,7 @@ pub fn split(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||
};
|
||||
|
||||
let Value::Str(delimiter) = &args[0] else {
|
||||
return Err(format!("first arg is not a string: can't split!"));
|
||||
return Err("first arg is not a string: can't split!".to_owned());
|
||||
};
|
||||
|
||||
let result = string_to_split
|
||||
@ -138,3 +139,36 @@ pub fn split(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||
|
||||
Ok(Value::List(result))
|
||||
}
|
||||
|
||||
/// Unwraps an Option or returns the given default..
|
||||
///
|
||||
/// `<Option<T>>.unwrap_or(<T>) -> <T>`
|
||||
pub fn unwrap_or(obj: Value, mut args: Vec<Value>) -> Result<Value, String> {
|
||||
if args.len() != 1 {
|
||||
return Err(format!("unwrap_or takes 1 arg, not {}", args.len()));
|
||||
}
|
||||
|
||||
match obj {
|
||||
Value::Reflective(reflect) => match reflect.reflect_ref() {
|
||||
ReflectRef::Enum(reflenum) => match reflenum.variant_name() {
|
||||
"Some" => {
|
||||
if reflenum.field_len() != 1 {
|
||||
return Err("wrong number of fields in Some".to_owned());
|
||||
}
|
||||
|
||||
if reflenum.variant_type() != VariantType::Tuple {
|
||||
return Err("Some is not a tuple variant".to_owned());
|
||||
}
|
||||
|
||||
Ok(Value::from_reflect(
|
||||
reflenum.field_at(0).unwrap().clone_value(),
|
||||
))
|
||||
}
|
||||
"None" => Ok(args.pop().unwrap()),
|
||||
other => Err(format!("`{other}` is not Some or None")),
|
||||
},
|
||||
_ => Err(format!("reflective {reflect:?} is not an Option")),
|
||||
},
|
||||
other => Err(format!("{other:?} is not an Option")),
|
||||
}
|
||||
}
|
||||
|
110
hornbeam_interpreter/src/functions/formbeam_integration.rs
Normal file
110
hornbeam_interpreter/src/functions/formbeam_integration.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use formbeam::FieldError;
|
||||
|
||||
use crate::{
|
||||
formbeam_integration::{InfoWrapper, ReflectFieldValidatorInfo},
|
||||
interface::Value,
|
||||
};
|
||||
|
||||
use super::TemplateAccessibleMethod;
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
const FORMBEAM_TEMPLATE_ACCESSIBLE_METHODS: &[(
|
||||
&str,
|
||||
fn(Value, Vec<Value>) -> Result<Value, String>,
|
||||
)] = &[
|
||||
("field_validators", field_validators),
|
||||
("error_code", error_code),
|
||||
];
|
||||
|
||||
/// Return a map of the default suggested template-accessible methods.
|
||||
pub fn formbeam_template_accessible_methods() -> BTreeMap<String, TemplateAccessibleMethod> {
|
||||
FORMBEAM_TEMPLATE_ACCESSIBLE_METHODS
|
||||
.iter()
|
||||
.map(|(name, func)| {
|
||||
(
|
||||
(*name).to_owned(),
|
||||
TemplateAccessibleMethod { function: *func },
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns the validators for the given-named field.
|
||||
///
|
||||
/// - `<InfoWrapper>.field_validators(<Str>) -> ...`
|
||||
pub fn field_validators(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||
if args.len() != 1 {
|
||||
return Err(format!("field_validators takes 1 arg, not {}", args.len()));
|
||||
}
|
||||
let info_wrapper = match obj {
|
||||
Value::Reflective(reflect) => match reflect.downcast::<InfoWrapper>() {
|
||||
Ok(info_wrapper) => info_wrapper,
|
||||
Err(not_an_info_wrapper) => {
|
||||
return Err(format!(
|
||||
"{not_an_info_wrapper:?} is reflective but not an InfoWrapper!"
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(format!("{obj:?} is not an InfoWrapper!"));
|
||||
}
|
||||
};
|
||||
|
||||
let field_name = match args.first().unwrap() {
|
||||
Value::Str(s) => s,
|
||||
other => {
|
||||
return Err(format!(
|
||||
"{other:?} is not a string so cannot be a field name!"
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let info = (*info_wrapper).0.as_ref().unwrap();
|
||||
|
||||
for field in info.fields {
|
||||
if field.name == field_name.as_str() {
|
||||
return Ok(Value::List(
|
||||
field
|
||||
.validators
|
||||
.iter()
|
||||
.map(|validator| {
|
||||
Value::from_reflect(Box::new(ReflectFieldValidatorInfo::from(validator)))
|
||||
})
|
||||
.collect(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!(
|
||||
"No such field by the name of {field_name} on this form!"
|
||||
))
|
||||
}
|
||||
|
||||
/// Return the 'code' of an error, suitable for passing to the localisation engine as a key.
|
||||
///
|
||||
/// - `<FieldError>.error_code() -> Str`
|
||||
pub fn error_code(obj: Value, args: Vec<Value>) -> Result<Value, String> {
|
||||
if !args.is_empty() {
|
||||
return Err(format!("error_code takes 0 args, not {}", args.len()));
|
||||
}
|
||||
|
||||
let field_error = match obj {
|
||||
Value::Reflective(reflect) => match reflect.downcast::<FieldError>() {
|
||||
Ok(ferr) => ferr,
|
||||
Err(not_an_info_wrapper) => {
|
||||
return Err(format!(
|
||||
"{not_an_info_wrapper:?} is reflective but not a FieldError!"
|
||||
));
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
return Err(format!("{obj:?} is not a FieldError!"));
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Value::Str(Arc::new(field_error.error_code().to_owned())))
|
||||
}
|
||||
|
||||
// TODO error_args
|
@ -39,6 +39,8 @@ pub trait OutputSystem {
|
||||
|
||||
// Value is currently used in the localisation system. We might pull it away later on...
|
||||
pub use crate::engine::Value;
|
||||
#[cfg(feature = "formbeam")]
|
||||
use crate::formbeam_template_accessible_methods;
|
||||
use crate::{default_template_accessible_methods, InterpreterError};
|
||||
|
||||
pub struct LoadedTemplates<LS> {
|
||||
@ -66,13 +68,26 @@ impl Params {
|
||||
|
||||
impl<'a, LS> LoadedTemplates<LS> {
|
||||
pub fn new(localisation_system: LS) -> Self {
|
||||
#[allow(unused_mut)]
|
||||
let mut methods = default_template_accessible_methods();
|
||||
#[cfg(feature = "formbeam")]
|
||||
methods.extend(formbeam_template_accessible_methods());
|
||||
LoadedTemplates {
|
||||
template_functions: Default::default(),
|
||||
methods: Arc::new(default_template_accessible_methods()),
|
||||
methods: Arc::new(methods),
|
||||
localisation: Arc::new(localisation_system),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_methods(
|
||||
&mut self,
|
||||
methods: impl IntoIterator<Item = (String, TemplateAccessibleMethod)>,
|
||||
) {
|
||||
let registered_methods = Arc::make_mut(&mut self.methods);
|
||||
|
||||
registered_methods.extend(methods);
|
||||
}
|
||||
|
||||
pub fn unload_template(&mut self, template_name: &str) -> bool {
|
||||
let was_removed = self.template_functions.remove(template_name).is_some();
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
mod engine;
|
||||
#[cfg(feature = "formbeam")]
|
||||
pub mod formbeam_integration;
|
||||
mod functions;
|
||||
pub(crate) mod interface;
|
||||
|
||||
@ -38,5 +40,7 @@ pub enum InterpreterError<LE: Debug + Clone, OE: Debug> {
|
||||
}
|
||||
|
||||
pub use functions::defaults::default_template_accessible_methods;
|
||||
#[cfg(feature = "formbeam")]
|
||||
pub use functions::formbeam_integration::formbeam_template_accessible_methods;
|
||||
pub use functions::TemplateAccessibleMethod;
|
||||
pub use interface::{LoadedTemplates, LocalisationSystem, OutputSystem, Params, PreparedTemplate};
|
||||
|
@ -8,7 +8,6 @@ use std::collections::BTreeMap;
|
||||
use std::ops::Deref;
|
||||
use thiserror::Error;
|
||||
|
||||
// TODO use the void tags
|
||||
/// List of all void (self-closing) HTML tags.
|
||||
const VOID_TAGS: &'static [&'static str] = &[
|
||||
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "menuitem", "meta",
|
||||
|
Loading…
x
Reference in New Issue
Block a user