diff --git a/Cargo.lock b/Cargo.lock index c7d817b..4c53b06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,111 +27,64 @@ dependencies = [ ] [[package]] -name = "alloc-no-stdlib" -version = "2.0.4" +name = "anyhow" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] -name = "alloc-stdlib" -version = "0.2.2" +name = "async-stream" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ - "alloc-no-stdlib", + "async-stream-impl", + "futures-core", + "pin-project-lite", ] [[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - -[[package]] -name = "async-compression" -version = "0.4.5" +name = "async-stream-impl" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ - "brotli", - "flate2", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "zstd", - "zstd-safe", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] -name = "autocfg" -version = "1.1.0" +name = "atomic" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" [[package]] -name = "axum" -version = "0.7.2" +name = "atomic" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202651474fe73c62d9e0a56c6133f7a0ff1dc1c8cf7a5b03381af2a26553ac9d" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" dependencies = [ - "async-trait", - "axum-core", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-util", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tower", - "tower-layer", - "tower-service", + "bytemuck", ] [[package]] -name = "axum-core" -version = "0.4.1" +name = "autocfg" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77cb22c689c44d4c07b0ab44ebc25d69d8ae601a2f28fb8d672d344178fa17aa" -dependencies = [ - "async-trait", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "rustversion", - "sync_wrapper", - "tower-layer", - "tower-service", -] +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" @@ -149,10 +102,10 @@ dependencies = [ ] [[package]] -name = "base64" -version = "0.21.5" +name = "binascii" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bit-set" @@ -177,9 +130,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block-buffer" @@ -191,25 +144,16 @@ dependencies = [ ] [[package]] -name = "brotli" -version = "3.4.0" +name = "bumpalo" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] -name = "brotli-decompressor" -version = "2.5.1" +name = "bytemuck" +version = "1.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] +checksum = "ea31d69bda4949c1c1562c1e6f042a1caefac98cdc8a298260a2ff41c1e2d42b" [[package]] name = "bytes" @@ -223,7 +167,6 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ - "jobserver", "libc", ] @@ -234,21 +177,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] -name = "cpufeatures" -version = "0.2.11" +name = "cookie" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8" dependencies = [ - "libc", + "percent-encoding", + "time", + "version_check", ] [[package]] -name = "crc32fast" -version = "1.3.2" +name = "cpufeatures" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ - "cfg-if", + "libc", ] [[package]] @@ -273,14 +218,47 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", "serde", ] +[[package]] +name = "devise" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6eacefd3f541c66fc61433d65e54e0e46e0a029a819a7dbbc7a7b489e8a85f8" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8cf4b8dd484ede80fd5c547592c46c3745a617c8af278e2b72bea86b2dfed6" +dependencies = [ + "devise_core", + "quote", +] + +[[package]] +name = "devise_core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a" +dependencies = [ + "bitflags 2.4.2", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + [[package]] name = "diesel" version = "2.1.4" @@ -302,7 +280,7 @@ dependencies = [ "diesel_table_macro_syntax", "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -322,7 +300,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" dependencies = [ - "syn 2.0.39", + "syn", ] [[package]] @@ -337,42 +315,56 @@ dependencies = [ [[package]] name = "dirs" -version = "4.0.0" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.3.7" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.48.0", ] -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "either" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "fancy-regex" version = "0.11.0" @@ -384,13 +376,23 @@ dependencies = [ ] [[package]] -name = "flate2" -version = "1.0.28" +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "figment" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +checksum = "2b6e5bc7bd59d60d0d45a6ccab6cf0f4ce28698fb4e81e750ddf229c9b824026" dependencies = [ - "crc32fast", - "miniz_oxide", + "atomic 0.6.0", + "pear", + "serde", + "toml 0.8.10", + "uncased", + "version_check", ] [[package]] @@ -400,54 +402,83 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "form_urlencoded" -version = "1.2.1" +name = "futures" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ - "percent-encoding", + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", ] [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", + "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-io" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", "slab", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -460,13 +491,15 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -475,11 +508,17 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" -version = "0.4.0" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d308f63daf4181410c242d34c11f928dcb3aa105852019e043c9d1f4e4368a" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -502,42 +541,15 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - -[[package]] -name = "hotwire-turbo" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972770143869a967800e948386ccce0e9b5f20782cf27c08e47f9a542ea11113" -dependencies = [ - "html-escape", -] - -[[package]] -name = "hotwire-turbo-axum" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a1b2d42218d1e6ca790ea278ae32bea7508f9ff3d3dda83543ab9ac8de4e1" -dependencies = [ - "axum", -] - -[[package]] -name = "html-escape" -version = "0.2.13" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" -dependencies = [ - "utf8-width", -] +checksum = "d0c62115964e08cb8039170eb33c1d0e2388a256930279edca206fff675f82c3" [[package]] name = "http" -version = "1.0.0" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -546,33 +558,15 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" -dependencies = [ - "bytes", - "http", -] - -[[package]] -name = "http-body-util" -version = "0.1.0" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "futures-util", "http", - "http-body", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ce4ef31cda248bbdb6e6820603b82dfcd9e833db65a43e997a0ccec777d11fe" - [[package]] name = "httparse" version = "1.8.0" @@ -587,12 +581,13 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.0.1" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f9214f3e703236b221f1a9cd88ec8b4adfa5296de01ab96216361f4692f56" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", "h2", "http", @@ -601,97 +596,63 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "tokio", -] - -[[package]] -name = "hyper-util" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca339002caeb0d159cc6e023dff48e199f081e42fa039895c7c6f38b37f2e9d" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http", - "http-body", - "hyper", - "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", + "want", ] -[[package]] -name = "idna" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "if_chain" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" - [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "824b2ae422412366ba479e8111fd301f7b5faece8149317bb81925979a53f520" dependencies = [ "equivalent", "hashbrown", + "serde", ] [[package]] -name = "iri-string" -version = "0.7.0" +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + +[[package]] +name = "is-terminal" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21859b667d66a4c1dacd9df0863b3efb65785474255face87f5bca39dd8407c0" +checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ - "memchr", - "serde", + "hermit-abi", + "rustix", + "windows-sys 0.52.0", ] [[package]] name = "itertools" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25db6b064527c5d482d0423354fcd07a89a2dfe07b67892e62411946db7f07b0" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] -name = "jobserver" -version = "0.1.27" +name = "js-sys" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ - "libc", + "wasm-bindgen", ] [[package]] @@ -702,9 +663,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libredox" @@ -712,7 +673,7 @@ version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "libc", "redox_syscall", ] @@ -728,6 +689,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "lock_api" version = "0.4.11" @@ -744,6 +711,21 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + [[package]] name = "markup" version = "0.15.0" @@ -761,7 +743,7 @@ checksum = "9ab6ee21fd1855134cacf2f41afdf45f1bc456c7d7f6165d763b4647062dd2be" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -773,17 +755,11 @@ dependencies = [ "regex-automata 0.1.10", ] -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "migrations_internals" @@ -792,7 +768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" dependencies = [ "serde", - "toml", + "toml 0.7.8", ] [[package]] @@ -824,9 +800,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] @@ -839,22 +815,48 @@ checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] -name = "nu-ansi-term" -version = "0.46.0" +name = "multer" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num_cpus" -version = "1.16.0" + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "tokio", + "tokio-util", + "version_check", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_cpus" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ @@ -864,9 +866,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -877,6 +879,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "overload" version = "0.1.1" @@ -903,35 +911,38 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.5", ] [[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "pin-project" -version = "1.1.3" +name = "pear" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "4ccca0f6c17acc81df8e242ed473ec144cbf5c98037e69aa6d144780aad103c8" dependencies = [ - "pin-project-internal", + "inlinable_string", + "pear_codegen", + "yansi", ] [[package]] -name = "pin-project-internal" -version = "1.1.3" +name = "pear_codegen" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2e22670e8eb757cff11d6c199ca7b987f352f0346e0be4dd23869ec72cb53c77" dependencies = [ "proc-macro2", + "proc-macro2-diagnostics", "quote", - "syn 2.0.39", + "syn", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -946,9 +957,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" [[package]] name = "powerfmt" @@ -963,43 +974,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro2" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", + "unicode-ident", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro2-diagnostics" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", + "syn", "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.70" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" -dependencies = [ - "unicode-ident", + "yansi", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1065,15 +1065,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ref-cast" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.3", + "regex-automata 0.4.5", "regex-syntax 0.8.2", ] @@ -1088,9 +1108,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -1109,11 +1129,117 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "rocket" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7bb57ccb26670d73b6a47396c83139447b9e7878cab627fdfe9ea8da489150" +dependencies = [ + "async-stream", + "async-trait", + "atomic 0.5.3", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot", + "pin-project-lite", + "rand", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "state", + "tempfile", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "ubyte", + "version_check", + "yansi", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2238066abf75f21be6cd7dc1a09d5414a671f4246e384e49fe3f8a4936bd04c" +dependencies = [ + "devise", + "glob", + "indexmap", + "proc-macro2", + "quote", + "rocket_http", + "syn", + "unicode-xid", + "version_check", +] + +[[package]] +name = "rocket_http" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a1663694d059fe5f943ea5481363e48050acedd241d46deb2e27f71110389e" +dependencies = [ + "cookie", + "either", + "futures", + "http", + "hyper", + "indexmap", + "log", + "memchr", + "pear", + "percent-encoding", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec", + "stable-pattern", + "state", + "time", + "tokio", + "uncased", +] + +[[package]] +name = "rocket_sync_db_pools" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83f32721ed79509adac4328e97f817a8f55a47c4b64799f6fd6cc3adb6e42ff" +dependencies = [ + "diesel", + "r2d2", + "rocket", + "rocket_sync_db_pools_codegen", + "serde", + "tokio", + "version_check", +] + +[[package]] +name = "rocket_sync_db_pools_codegen" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc890925dc79370c28eb15c9957677093fdb7e8c44966d189f38cedb995ee68" +dependencies = [ + "devise", + "quote", +] + [[package]] name = "rust-embed" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" +checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -1122,23 +1248,23 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" +checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", "shellexpand", - "syn 2.0.39", + "syn", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.0.0" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" +checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" dependencies = [ "sha2", "walkdir", @@ -1149,29 +1275,20 @@ name = "rust-quote-editor" version = "0.1.0" dependencies = [ "anyhow", - "axum", "currency_rs", "diesel", "diesel_migrations", - "dotenvy", - "hotwire-turbo", - "hotwire-turbo-axum", "itertools", "libsqlite3-sys", "markup", "mime_guess", "once_cell", "regex", + "rocket", + "rocket_sync_db_pools", "rust-embed", - "serde", - "serde_json", "time", - "tokio", - "tower-http", - "tracing", - "tracing-subscriber", "ulid", - "validator", ] [[package]] @@ -1180,6 +1297,19 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -1188,9 +1318,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" [[package]] name = "same-file" @@ -1210,6 +1340,12 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -1218,63 +1354,41 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" -version = "1.0.193" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.193" +version = "1.0.196" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79" dependencies = [ "itoa", "ryu", "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" -dependencies = [ - "itoa", - "serde", -] - [[package]] name = "serde_spanned" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_urlencoded" -version = "0.7.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", "serde", ] @@ -1300,9 +1414,9 @@ dependencies = [ [[package]] name = "shellexpand" -version = "2.1.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" dependencies = [ "dirs", ] @@ -1327,9 +1441,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" @@ -1338,25 +1452,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] -name = "syn" -version = "1.0.109" +name = "spin" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", + "memchr", +] + +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", ] [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -1364,29 +1491,35 @@ dependencies = [ ] [[package]] -name = "sync_wrapper" -version = "0.1.2" +name = "tempfile" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -1401,12 +1534,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -1421,45 +1555,30 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -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.34.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1470,7 +1589,18 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", ] [[package]] @@ -1496,7 +1626,19 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.4", ] [[package]] @@ -1522,57 +1664,18 @@ dependencies = [ ] [[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.5.0" +name = "toml_edit" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e12e6351354851911bdf8c2b8f2ab15050c567d70a8b9a37ae7b8301a4080d" +checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" dependencies = [ - "async-compression", - "base64", - "bitflags 2.4.1", - "bytes", - "futures-util", - "http", - "http-body", - "http-body-util", - "http-range-header", - "httpdate", - "iri-string", - "mime", - "mime_guess", - "percent-encoding", - "pin-project-lite", - "tokio", - "tokio-util", - "tower", - "tower-layer", - "tower-service", - "tracing", - "uuid", + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] -[[package]] -name = "tower-layer" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" - [[package]] name = "tower-service" version = "0.3.2" @@ -1585,7 +1688,6 @@ version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1599,7 +1701,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn", ] [[package]] @@ -1641,35 +1743,56 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" +dependencies = [ + "serde", +] + [[package]] name = "ulid" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e37c4b6cbcc59a8dcd09a6429fbc7890286bcbb79215cea7b38a3c4c0921d93" +checksum = "34778c17965aa2a08913b57e1f34db9b4a63f5de31768b55bf20d2795f921259" dependencies = [ + "getrandom", "rand", + "web-time", ] [[package]] -name = "unicase" -version = "2.7.0" +name = "uncased" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" dependencies = [ + "serde", "version_check", ] [[package]] -name = "unicode-bidi" -version = "0.3.14" +name = "unicase" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] [[package]] name = "unicode-ident" @@ -1678,116 +1801,118 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] -name = "unicode-normalization" -version = "0.1.22" +name = "unicode-xid" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] -name = "url" -version = "2.5.0" +name = "valuable" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" -dependencies = [ - "form_urlencoded", - "idna 0.5.0", - "percent-encoding", -] +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] -name = "utf8-width" -version = "0.1.7" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] -name = "uuid" -version = "1.6.1" +name = "version_check" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ - "getrandom", + "same-file", + "winapi-util", ] [[package]] -name = "validator" -version = "0.16.1" +name = "want" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "idna 0.4.0", - "lazy_static", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", + "try-lock", ] [[package]] -name = "validator_derive" -version = "0.16.0" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ - "if_chain", - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.109", - "validator_types", + "cfg-if", + "wasm-bindgen-macro", ] [[package]] -name = "validator_types" -version = "0.16.0" +name = "wasm-bindgen-backend" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ + "bumpalo", + "log", + "once_cell", "proc-macro2", - "syn 1.0.109", + "quote", + "syn", + "wasm-bindgen-shared", ] [[package]] -name = "valuable" -version = "0.1.0" +name = "wasm-bindgen-macro" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "wasm-bindgen-macro-support" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] [[package]] -name = "version_check" -version = "0.9.4" +name = "wasm-bindgen-shared" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] -name = "walkdir" -version = "2.4.0" +name = "web-time" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "2ee269d72cc29bf77a2c4bc689cc750fb39f5cbd493d2205bbb3f5c7779cf7b0" dependencies = [ - "same-file", - "winapi-util", + "js-sys", + "wasm-bindgen", ] -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - [[package]] name = "winapi" version = "0.3.9" @@ -1819,13 +1944,31 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -1834,13 +1977,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -1849,30 +2007,60 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -1880,44 +2068,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "windows_x86_64_gnullvm" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" [[package]] -name = "winnow" -version = "0.5.26" +name = "windows_x86_64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67b5f0a4e7a27a64c651977932b9dc5667ca7fc31ac44b03ed37a0cf42fdfff" -dependencies = [ - "memchr", -] +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "zstd" -version = "0.13.0" +name = "windows_x86_64_msvc" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffb3309596d527cfcba7dfc6ed6052f1d39dfbd7c867aa2e865e4a449c10110" -dependencies = [ - "zstd-safe", -] +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" [[package]] -name = "zstd-safe" -version = "7.0.0" +name = "winnow" +version = "0.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43747c7422e2924c11144d5229878b98180ef8b06cca4ab5af37afc8a8d8ea3e" +checksum = "5389a154b01683d28c77f8f68f49dea75f0a4da32557a58f68ee51ebba472d29" dependencies = [ - "zstd-sys", + "memchr", ] [[package]] -name = "zstd-sys" -version = "2.0.9+zstd.1.5.5" +name = "yansi" +version = "1.0.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" dependencies = [ - "cc", - "pkg-config", + "is-terminal", ] diff --git a/Cargo.toml b/Cargo.toml index 1d8972b..e5a3955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,26 +14,17 @@ lto = true [dependencies] anyhow = "1.0" -axum = "0.7" currency_rs = { git = "https://github.com/johnbcodes/currency_rs", branch = "feature/db-diesel2-sqlite", version = "1.1", features = [ "db-diesel2-sqlite" ] } -diesel = { version = "2.1", features = ["r2d2", "sqlite", "time"] } +diesel = { version = "2.1", features = ["sqlite", "time"] } diesel_migrations = "2.1" -dotenvy = "0.15" -hotwire-turbo = "0.1" -hotwire-turbo-axum = "0.1" itertools = "0.12" libsqlite3-sys = { version = "0.27", features = ["bundled"] } markup = "0.15" mime_guess = "2" once_cell = "1" regex = "1" +rocket = "0.5" +rocket_sync_db_pools = { version = "0.1", features = ["diesel_sqlite_pool"]} rust-embed = { version = "8", features = ["interpolate-folder-path"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" time = { version = "0.3", features = ["formatting", "macros", "parsing", "serde"] } -tokio = { version = "1", features = ["full"] } -tower-http = { version = "0.5", features = ["full"] } -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } ulid = "1.1" -validator = { version = "0.16", features = ["derive"] } diff --git a/Dockerfile b/Dockerfile index 1193757..84d9500 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,10 +15,11 @@ RUN USER=root cargo new --bin app WORKDIR /app # copy over infrequently changing files -COPY package.json package-lock.json Cargo.lock Cargo.toml ./ -# copy your source tree, ordered again by infrequent to frequently changed files COPY tailwind.config.js ./ COPY build.rs ./ +COPY Rocket.toml ./ +COPY package.json package-lock.json Cargo.lock Cargo.toml ./ +# copy your source tree, ordered again by infrequent to frequently changed files COPY ./migrations ./migrations COPY ./ui ./ui COPY ./src ./src @@ -38,7 +39,7 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \ ## Deploy locally FROM debug as dev -ENV DATABASE_URL=sqlite://data/demo.db +ENV ROCKET_PROFILE=docker EXPOSE 8080 @@ -65,9 +66,10 @@ WORKDIR / RUN mkdir data +COPY --from=release /app/Rocket.toml . COPY --from=release /usr/local/cargo/bin/demo . -ENV DATABASE_URL=sqlite://data/demo.db +ENV ROCKET_PROFILE=docker EXPOSE 8080 diff --git a/README.md b/README.md index 0c12e21..a53c922 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ > Rust implementation of the quote editor from [Turbo Rails Tutorial](https://www.hotrails.dev/turbo-rails). +### TODO +* Group queries in run block where possible + ### Motivation and caveats The main motivation is learning to develop web applications with Rust and JavaScript combined. It now includes @@ -9,7 +12,7 @@ the following stack: * [htmx](https://htmx.org/) * [hyperscript](https://hyperscript.org/) -* [Axum](https://github.com/tokio-rs/axum) +* [Rocket](https://rocket.rs/) * [Diesel](https://diesel.rs/) * [markup.rs](https://github.com/utkarshkukreti/markup.rs) * Custom Rust / NPM build integration @@ -18,6 +21,7 @@ the following stack: In the past it included these technologies: * [Hotwire Turbo](https://turbo.hotwired.dev/) +* [Axum](https://github.com/tokio-rs/axum) * [Rusqlite](https://github.com/rusqlite/rusqlite) Some features of the tutorial were intentionally left out and possibly will be worked on in the future: @@ -29,11 +33,9 @@ Additionally, there were some other features and integral parts of Rails that ha * The look and feel deviates from [demo](https://www.hotrails.dev/quotes) because the author has made some UI enhancements that are not in the tutorial * Viewports less than tablet sizing -* Proper validation error messages ("to_sentence" on ValidationErrors struct for flash message) -* Only add border color to fields with errors * Labels for input fields * Delete confirmation -* Probably a few others +* ...probably a few others ## Getting Started @@ -62,7 +64,7 @@ Additionally, there were some other features and integral parts of Rails that ha * Create volume with `docker volume create db-data` * Build with `docker build -t rust-quote-editor .` -* Run with `docker run -itd -e "DATABASE_URL=sqlite:///data/demo.db" -p 8080:8080 -v db-data:/data rust-quote-editor` +* Run with `docker run -itd -p 8080:8080 -v db-data:/data rust-quote-editor` #### Docker Compose @@ -78,12 +80,11 @@ Additionally, there were some other features and integral parts of Rails that ha * Update `primary_region` property in `fly.toml` * `fly volumes create -s 1 -r ` * Update `mounts.source` property in `fly.toml` with -* `fly secrets set DATABASE_URL=/data/demo.db` * `docker build -t registry.fly.io/: --target deploy .` * `fly deploy --image registry.fly.io/:` ## Automated deployment of new versions with GitHub [action](.github/workflows/deploy.yml) -* [Set up](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) your `FLY_API_TOKEN` secret in your repository +* [Set up](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) your `FLY_API_TOKEN` [secret](https://fly.io/docs/reference/deploy-tokens/) in your repository * Tag release with a tag name starting with 'v' * Example: `git tag -a v2 -m "My new release!" && git push --tags` diff --git a/Rocket.toml b/Rocket.toml new file mode 100644 index 0000000..6a1380a --- /dev/null +++ b/Rocket.toml @@ -0,0 +1,13 @@ +[default] +log_level = "debug" + +[default.databases.demo] +url = "data/demo.db" +timeout = 10 + +[docker] +address = "0.0.0.0" + +[docker.databases.demo] +url = "/data/demo.db" +timeout = 10 diff --git a/docker-compose.yml b/docker-compose.yml index 30a9347..95e3338 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,8 @@ services: web.app: build: target: dev - environment: - - DATABASE_URL=/data/demo.db ports: - - "8080:8080" + - "8000:8000" volumes: - db-data:/data diff --git a/fly.toml b/fly.toml index 20d1c85..8c6d656 100644 --- a/fly.toml +++ b/fly.toml @@ -8,11 +8,11 @@ kill_timeout = 5 PORT = "8080" [mounts] -source = "jbc_ah_data" +source = "jbc_qe_data" destination = "/data" [[services]] -internal_port = 8080 +internal_port = 8000 protocol = "tcp" [services.concurrency] @@ -31,6 +31,6 @@ port = 443 [[services.tcp_checks]] grace_period = "1s" interval = "15s" -port = "8080" +port = "8000" restart_limit = 6 timeout = "2s" diff --git a/src/assets.rs b/src/assets.rs index 209c9c8..a2c6086 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -1,54 +1,48 @@ -use axum::{ - body::Body, - http::{header, StatusCode, Uri}, - response::{IntoResponse, Response}, +use rocket::{ + fairing::AdHoc, + http::{ContentType, Header}, + response::Responder, }; use rust_embed::RustEmbed; -use tracing::info; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::path::PathBuf; #[derive(RustEmbed)] #[folder = "$CARGO_MANIFEST_DIR/ui/target/public/"] -pub(crate) struct Assets; - -pub(crate) struct StaticFile(pub(crate) T); +pub(crate) struct Asset; + +#[derive(Responder)] +#[response(status = 200)] +struct AssetResponse { + content: Cow<'static, [u8]>, + content_type: ContentType, + max_age: Header<'static>, +} #[cfg(debug_assertions)] const MAX_AGE: &str = "max-age=120"; #[cfg(not(debug_assertions))] const MAX_AGE: &str = "max-age=31536000"; -impl IntoResponse for StaticFile -where - T: Into, -{ - fn into_response(self) -> Response { - let path = self.0.into(); - - match Assets::get(path.as_str()) { - Some(content) => { - info!("Retrieving asset with path: {path}"); - let body = Body::from(content.data); - let mime = mime_guess::from_path(path).first_or_octet_stream(); - Response::builder() - .header(header::CONTENT_TYPE, mime.as_ref()) - .header(header::CACHE_CONTROL, MAX_AGE) - .body(body) - .unwrap() - } - None => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(Body::from("Not Found")) - .unwrap(), - } - } +pub(crate) fn stage() -> AdHoc { + AdHoc::on_ignite("Assets Stage", |rocket| async { + rocket.mount("/dist", routes![asset_handler]) + }) } -pub(crate) async fn asset_handler(uri: Uri) -> impl IntoResponse { - let mut path = uri.path().trim_start_matches('/').to_string(); - - if path.starts_with("dist/") { - path = path.replace("dist/", ""); - } - - StaticFile(path) +#[get("/")] +fn asset_handler(file: PathBuf) -> Option { + let filename = file.display().to_string(); + let asset = Asset::get(&filename)?; + let content_type = file + .extension() + .and_then(OsStr::to_str) + .and_then(ContentType::from_extension) + .unwrap_or(ContentType::Bytes); + Some(AssetResponse { + content: asset.data, + content_type, + max_age: Header::new("Cache-Control", MAX_AGE), + }) } diff --git a/src/currency.rs b/src/currency.rs index a2fcc61..d4667e7 100644 --- a/src/currency.rs +++ b/src/currency.rs @@ -2,4 +2,4 @@ use once_cell::sync::Lazy; use regex::Regex; pub(crate) static FORM_CURRENCY_REGEX: Lazy = - Lazy::new(|| Regex::new(r"^\d*(\.\d{2})?$").unwrap()); + Lazy::new(|| Regex::new(r"^\d+(\.\d{2})?$").unwrap()); diff --git a/src/error.rs b/src/error.rs index 092af7b..67a5977 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,19 +1,14 @@ -use axum::{ - http::StatusCode, - response::{IntoResponse, Response}, +use rocket::{ + response::{Debug, Responder, Result}, + Request, }; // Make our own error that wraps `anyhow::Error`. pub(crate) struct AppError(anyhow::Error); -// Tell axum how to convert `AppError` into a response. -impl IntoResponse for AppError { - fn into_response(self) -> Response { - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", self.0), - ) - .into_response() +impl<'r> Responder<'r, 'r> for AppError { + fn respond_to(self, request: &Request<'_>) -> Result<'r> { + Debug(self.0).respond_to(request) } } diff --git a/src/forms.rs b/src/forms.rs new file mode 100644 index 0000000..bfd9c3f --- /dev/null +++ b/src/forms.rs @@ -0,0 +1,56 @@ +use crate::{currency::FORM_CURRENCY_REGEX, time::DATE_REGEX}; +use once_cell::sync::Lazy; +use regex::Regex; +use rocket::form::{Contextual, Form}; + +pub(crate) static QUANTITY_REGEX: Lazy = Lazy::new(|| Regex::new(r"^\d+$").unwrap()); + +pub(crate) fn css_for_field<'b, T>( + form: &Form>, + field: &'b str, + default_class: &'b str, + error_class: &'b str, +) -> String { + if form.context.exact_field_errors(field).count() == 0 { + default_class.to_string() + } else { + format!("{} {}", default_class, error_class) + } +} + +pub(crate) fn validate_date<'v>(date: &str) -> rocket::form::Result<'v, ()> { + if date.is_empty() { + Err(rocket::form::Error::validation("Please enter a date"))?; + } + if !DATE_REGEX.is_match(date) { + Err(rocket::form::Error::validation("Please enter a valid date"))?; + } + + Ok(()) +} + +pub(crate) fn validate_amount<'v>(amount: &str) -> rocket::form::Result<'v, ()> { + if amount.is_empty() { + Err(rocket::form::Error::validation("Please enter an amount"))?; + } + if !FORM_CURRENCY_REGEX.is_match(amount) { + Err(rocket::form::Error::validation( + "Please enter a valid amount", + ))?; + } + + Ok(()) +} + +pub(crate) fn validate_quantity<'v>(quantity: &str) -> rocket::form::Result<'v, ()> { + if quantity.is_empty() { + Err(rocket::form::Error::validation("Please enter a quantity"))?; + } + if !QUANTITY_REGEX.is_match(quantity) { + Err(rocket::form::Error::validation( + "Please enter a valid quantity", + ))?; + } + + Ok(()) +} diff --git a/src/line_item_dates/controller.rs b/src/line_item_dates/controller.rs index 2939d6d..fe78c5e 100644 --- a/src/line_item_dates/controller.rs +++ b/src/line_item_dates/controller.rs @@ -1,179 +1,163 @@ use crate::{ line_item_dates::{ self, - model::{DeleteForm, LineItemDateForm, LineItemDatePresenter}, - view::{Create, Destroy, EditForm, LineItemDateInfo, NewForm, Update}, + model::{DeleteForm, EditLineItemDateForm, LineItemDatePresenter, NewLineItemDateForm}, + view::*, }, line_items::{self, model::LineItemPresenter}, - quotes, Result, + quotes, + rocket_ext::HtmxResponder, + Db, Result, }; -use axum::{ - extract::{Path, State}, - response::{Html, IntoResponse}, +use rocket::{ + fairing::AdHoc, + form::{Contextual, Form}, + http::Header, + response::content::RawHtml, }; -use diesel::prelude::SqliteConnection; -use diesel::r2d2::{ConnectionManager, Pool}; -use std::time::Instant; -use tracing::info; -use validator::Validate; -pub(crate) async fn line_item_date( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let record = line_item_dates::query::read(&pool, id).await?; - let duration = start.elapsed().as_micros(); - info!("lid - read duration: {duration} μs"); +pub(crate) fn stage() -> AdHoc { + AdHoc::on_ignite("LineItemDate Stage", |rocket| async { + rocket.mount( + "/line_item_dates", + routes![line_item_date, new, create, edit, update, delete], + ) + }) +} + +#[get("/")] +async fn line_item_date(db: Db, id: String) -> Result> { + let record = db + .run(move |conn| { + let quote = line_item_dates::query::read(conn, &id)?; + Result::Ok(quote) + }) + .await?; let template = LineItemDateInfo { line_item_date: &record.into(), }; - Ok(Html(template.to_string())) + Ok(RawHtml(template.to_string())) } -pub(crate) async fn new( - State(pool): State>>, - Path(quote_id): Path, -) -> Result { - let start = Instant::now(); - let quote = quotes::query::read(&pool, quote_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - let line_item_date = LineItemDatePresenter::from_quote_with_total(quote); - let duration = start.elapsed().as_micros(); - info!("lid - read duration: {duration} μs"); - Ok(Html( - NewForm { - dom_id: &line_item_date.dom_id(), - line_item_date: &line_item_date, - error_message: None, - } - .to_string(), - )) +#[get("/new/")] +pub(crate) async fn new(quote_id: &str) -> Result> { + let template = NewForm { quote_id }; + let html = template.to_string(); + Ok(RawHtml(html)) } +#[post("/create", data = "
")] pub(crate) async fn create( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let result = form.validate(); - match result { - Ok(_) => { - let start = Instant::now(); - let line_item_date = line_item_dates::query::insert(&pool, &form).await?; - let duration = start.elapsed().as_micros(); - info!("lid - insert duration: {duration} μs"); - Ok(Html( - Create { - line_item_date: &line_item_date.into(), - line_items: &Vec::new(), - message: "Date was successfully created.", - } - .to_string(), - ) - .into_response()) + db: Db, + form: Form>, +) -> Result { + print!("Form:\n{form:?}"); + match form.value { + Some(ref lid_form) => { + let lid_form = lid_form.clone(); + let line_item_date = db + .run(move |conn| { + let record = line_item_dates::query::insert(conn, &lid_form)?; + Result::Ok(record) + }) + .await?; + + let content = Create { + line_item_date: &line_item_date.into(), + line_items: &Vec::new(), + message: "Date was successfully created.", + } + .to_string(); + + Ok(HtmxResponder::Ok(content)) } - Err(errors) => { - info!("ValidationErrors:\n{:?}", errors); - let error_message = String::from("Test"); - let line_item_date: &LineItemDatePresenter = &form.into(); - Ok(Html( - NewForm { - dom_id: &line_item_date.dom_id(), - line_item_date, - error_message: Some(error_message), - } - .to_string(), - ) - .into_response()) + None => { + let template = NewFormWithErrors { form: &form }; + let content = template.to_string(); + Ok(HtmxResponder::Retarget { + content, + retarget: Header::new("HX-Retarget", "#line_item_date_new".to_string()), + reswap: Header::new("HX-Reswap", "outerhtml".to_string()), + }) } } } -pub(crate) async fn edit( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let record = line_item_dates::query::read(&pool, id).await?; - let duration = start.elapsed().as_micros(); - info!("lid - read duration: {duration} μs"); +#[get("/edit/")] +pub(crate) async fn edit(db: Db, id: String) -> Result> { + let record = db + .run(move |conn| { + let quote = line_item_dates::query::read(conn, &id)?; + Result::Ok(quote) + }) + .await?; + let line_item_date: &LineItemDatePresenter = &record.into(); - Ok(Html( - EditForm { - dom_id: &line_item_date.edit_dom_id(), - line_item_date, - error_message: None, - } - .to_string(), - )) + let template = EditForm { line_item_date }; + let html = template.to_string(); + Ok(RawHtml(html)) } +#[post("/update", data = "")] pub(crate) async fn update( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let result = form.validate(); - match result { - Ok(_) => { - let start = Instant::now(); - let line_item_date = line_item_dates::query::update(&pool, &form).await?; - let duration = start.elapsed().as_micros(); - info!("lid - update duration: {duration} μs"); - let start = Instant::now(); - let line_items = line_items::query::all_for_line_item_date(&pool, &line_item_date.id) - .await? - .into_iter() - .map(|record| record.into()) - .collect::>(); - let duration = start.elapsed().as_micros(); - info!("li - read all duration: {duration} μs"); - Ok(Html( - Update { - line_item_date: &line_item_date.into(), - line_items: &line_items, - message: "Date was successfully updated.", - } - .to_string(), - ) - .into_response()) + db: Db, + form: Form>, +) -> Result> { + print!("Form:\n{form:?}"); + match form.value { + Some(ref lid_form) => { + let lid_form = lid_form.clone(); + let line_item_date = db + .run(move |conn| { + let record = line_item_dates::query::update(conn, &lid_form)?; + Result::Ok(record) + }) + .await?; + + let lid_id = line_item_date.id.clone(); + let line_items = db + .run(move |conn| { + let line_items = line_items::query::all_for_line_item_date(conn, &lid_id)? + .into_iter() + .map(|record| record.into()) + .collect::>(); + Result::Ok(line_items) + }) + .await?; + + let content = Update { + line_item_date: &line_item_date.into(), + line_items: &line_items, + message: "Date was successfully updated.", + } + .to_string(); + + Ok(RawHtml(content)) } - Err(errors) => { - info!("ValidationErrors:\n{:?}", errors); - let error_message = String::from("Test"); - let line_item_date: &LineItemDatePresenter = &form.into(); - Ok(Html( - EditForm { - dom_id: &line_item_date.edit_dom_id(), - line_item_date, - error_message: Some(error_message), - } - .to_string(), - ) - .into_response()) + None => { + let template = EditFormWithErrors { form: &form }; + let content = template.to_string(); + Ok(RawHtml(content)) } } } -pub(crate) async fn delete( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let start = Instant::now(); - let line_item_date = line_item_dates::query::delete(&pool, &form.id).await?; - let duration = start.elapsed().as_micros(); - info!("lid - delete duration: {duration} μs"); - let start = Instant::now(); - let quote = quotes::query::read(&pool, &line_item_date.quote_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - Ok(Html( +#[post("/delete", data = "")] +async fn delete(db: Db, form: Form) -> Result> { + let quote = db + .run(move |conn| { + let line_item_date = line_item_dates::query::delete(conn, &form.id)?; + let quote = quotes::query::read(conn, &line_item_date.quote_id)?; + Result::Ok(quote) + }) + .await?; + + Ok(RawHtml( Destroy { quote: "e.into(), message: "Date was successfully destroyed.", } .to_string(), - ) - .into_response()) + )) } diff --git a/src/line_item_dates/model.rs b/src/line_item_dates/model.rs index ddcf5e7..dd008f3 100644 --- a/src/line_item_dates/model.rs +++ b/src/line_item_dates/model.rs @@ -1,13 +1,12 @@ use crate::{ + forms::validate_date, quotes::model::QuoteWithTotal, schema::line_item_dates, - time::{long_form, parse_date, short_form, DATE_REGEX}, + time::{long_form, parse_date, short_form}, }; use diesel::prelude::*; -use serde::Deserialize; use time::{Date, OffsetDateTime}; use ulid::Ulid; -use validator::Validate; #[derive(Debug, Insertable, Queryable)] pub struct LineItemDate { @@ -18,11 +17,11 @@ pub struct LineItemDate { pub updated_at: OffsetDateTime, } -impl From<&LineItemDateForm> for LineItemDate { - fn from(value: &LineItemDateForm) -> Self { +impl From<&EditLineItemDateForm> for LineItemDate { + fn from(value: &EditLineItemDateForm) -> Self { let date = parse_date(&value.date); LineItemDate { - id: value.id.clone().unwrap_or(Ulid::new().to_string()), + id: value.id.clone(), quote_id: value.quote_id.clone(), date, created_at: OffsetDateTime::now_utc(), @@ -31,14 +30,35 @@ impl From<&LineItemDateForm> for LineItemDate { } } -#[derive(Debug, Deserialize, Validate)] -pub(crate) struct LineItemDateForm { - #[validate(length(min = 1, message = "can't be blank"))] - pub(crate) id: Option, - #[validate(length(min = 1, message = "can't be blank"))] - pub(crate) quote_id: String, - #[validate(regex(path = "DATE_REGEX"))] - pub(crate) date: String, +impl From<&NewLineItemDateForm> for LineItemDate { + fn from(value: &NewLineItemDateForm) -> Self { + let date = parse_date(&value.date); + LineItemDate { + id: Ulid::new().to_string(), + quote_id: value.quote_id.clone(), + date, + created_at: OffsetDateTime::now_utc(), + updated_at: OffsetDateTime::now_utc(), + } + } +} + +#[derive(Clone, Debug, FromForm)] +pub struct EditLineItemDateForm { + #[field(validate = len(1..))] + pub id: String, + #[field(validate = len(1..))] + pub quote_id: String, + #[field(validate = validate_date())] + pub date: String, +} + +#[derive(Clone, Debug, FromForm)] +pub struct NewLineItemDateForm { + #[field(validate = len(1..))] + pub quote_id: String, + #[field(validate = validate_date())] + pub date: String, } #[derive(Debug, Default)] @@ -96,18 +116,29 @@ impl From for LineItemDatePresenter { } } -impl From for LineItemDatePresenter { - fn from(value: LineItemDateForm) -> Self { +impl From for LineItemDatePresenter { + fn from(value: EditLineItemDateForm) -> Self { + let date = parse_date(&value.date); + LineItemDatePresenter { + id: Some(value.id), + quote_id: value.quote_id, + date: Some(date), + } + } +} + +impl From for LineItemDatePresenter { + fn from(value: NewLineItemDateForm) -> Self { let date = parse_date(&value.date); LineItemDatePresenter { - id: value.id, + id: None, quote_id: value.quote_id, date: Some(date), } } } -#[derive(Debug, Deserialize)] +#[derive(Debug, FromForm)] pub(crate) struct DeleteForm { pub(crate) id: String, } diff --git a/src/line_item_dates/query.rs b/src/line_item_dates/query.rs index e8ae1e2..158708b 100644 --- a/src/line_item_dates/query.rs +++ b/src/line_item_dates/query.rs @@ -1,33 +1,23 @@ use crate::{ - line_item_dates::model::{LineItemDate, LineItemDateForm}, + line_item_dates::model::{EditLineItemDateForm, LineItemDate, NewLineItemDateForm}, line_items, schema::line_item_dates, Result, }; use diesel::prelude::*; -use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; -pub(crate) async fn all>( - pool: &Pool>, +pub(crate) fn all>( + connection: &mut SqliteConnection, id: S, ) -> Result> { - let mut connection = pool.get()?; let records = line_item_dates::table .filter(line_item_dates::quote_id.eq(&id.as_ref())) - .get_results(&mut connection)?; + .get_results(connection)?; Ok(records) } -pub(crate) async fn read>( - pool: &Pool>, - id: S, -) -> Result { - let mut connection = pool.get()?; - read_from_connection(&mut connection, id) -} - -fn read_from_connection>( - connection: &mut PooledConnection>, +pub(crate) fn read>( + connection: &mut SqliteConnection, id: S, ) -> Result { let record = line_item_dates::table @@ -36,45 +26,41 @@ fn read_from_connection>( Ok(record) } -pub(crate) async fn insert( - pool: &Pool>, - form: &LineItemDateForm, +pub(crate) fn insert( + connection: &mut SqliteConnection, + form: &NewLineItemDateForm, ) -> Result { let record: LineItemDate = form.into(); - let mut connection = pool.get()?; diesel::dsl::insert_into(line_item_dates::table) .values(&record) - .execute(&mut connection)?; + .execute(connection)?; Ok(record) } -pub(crate) async fn update( - pool: &Pool>, - form: &LineItemDateForm, +pub(crate) fn update( + connection: &mut SqliteConnection, + form: &EditLineItemDateForm, ) -> Result { let record: LineItemDate = form.into(); - let mut connection = pool.get()?; diesel::dsl::update(line_item_dates::table) .set(( line_item_dates::date.eq(&record.date), line_item_dates::updated_at.eq(&record.updated_at), )) .filter(line_item_dates::id.eq(&record.id)) - .execute(&mut connection)?; + .execute(connection)?; - read_from_connection(&mut connection, &record.id) + read(connection, &record.id) } -pub(crate) async fn delete>( - pool: &Pool>, +pub(crate) fn delete>( + connection: &mut SqliteConnection, id: S, ) -> Result { - let mut connection = pool.get()?; - - let record = read_from_connection(&mut connection, &id)?; + let record = read(connection, &id)?; _ = connection.transaction::<_, _, _>(|tx| { line_items::query::delete_all_for_date(tx, &id)?; @@ -88,10 +74,7 @@ pub(crate) async fn delete>( Ok(record) } -pub(crate) fn delete_all_for_quote>( - tx: &mut PooledConnection>, - id: S, -) -> Result { +pub(crate) fn delete_all_for_quote>(tx: &mut SqliteConnection, id: S) -> Result { line_items::query::delete_all_for_quote(tx, &id)?; _ = diesel::dsl::delete(line_item_dates::table) diff --git a/src/line_item_dates/view.rs b/src/line_item_dates/view.rs index e9e6e22..306635c 100644 --- a/src/line_item_dates/view.rs +++ b/src/line_item_dates/view.rs @@ -1,9 +1,11 @@ use crate::{ + forms::css_for_field, layout::Flash, - line_item_dates::model::LineItemDatePresenter, + line_item_dates::model::{EditLineItemDateForm, LineItemDatePresenter, NewLineItemDateForm}, line_items::{model::LineItemPresenter, view::LineItem}, quotes::{model::QuotePresenter, view::SwapFooter}, }; +use rocket::form::{Contextual, Form}; markup::define! { LineItemDate<'a>(line_item_date: &'a LineItemDatePresenter, line_items: &'a Vec) { @@ -73,34 +75,32 @@ markup::define! { } } - EditForm<'a>(dom_id: &'a String, line_item_date: &'a LineItemDatePresenter, error_message: Option) { - div[id = dom_id] { - form[id = {format!("form_{}", dom_id)}, + EditForm<'a>(line_item_date: &'a LineItemDatePresenter) { + div[id = line_item_date.edit_dom_id()] { + form[id = {format!("form_{}", &line_item_date.edit_dom_id())}, "hx-post" = "/line_item_dates/update", "hx-target" = {format!("#{}", &line_item_date.dom_id())}, "hx-swap" = "outerHTML", class = "flex flex-wrap justify-between items-center gap-2 mt-8 mb-1.5", autocomplete = "off", + novalidate, "accept-charset" = "UTF-8"] { - @let form_input_class = if error_message.is_some() { "form-input border-primary" } else { "form-input" }; - @if let Some(message) = error_message { - div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { @message } - } - @if let Some(id) = &line_item_date.id { - input[id = "line_item_date_id", - name = "id", - "type" = "hidden", - value = id] {} - } + + input[id = "id", + name = "id", + disabled, + "type" = "hidden", + value = &line_item_date.id.clone().unwrap()] {} input[id = "quote_id", name = "quote_id", + disabled, "type" = "hidden", value = &line_item_date.quote_id] {} div[class = "[flex:1]"] { label[class = "visually-hidden", "for" = "line_item_date_date"] { "Date" } input[id = "line_item_date_date", name = "date", - class = form_input_class, + class = "form-input", autofocus = "autofocus", required, "type" = "date", @@ -120,38 +120,130 @@ markup::define! { } } - NewForm<'a>(dom_id: &'a String, line_item_date: &'a LineItemDatePresenter, error_message: Option) { - div[id = dom_id] { + EditFormWithErrors<'a, 'r>(form: &'a Form>) { + @let context = &form.context; + @let id = context.field_value("id").unwrap_or(""); + @let quote_id = context.field_value("quote_id").unwrap_or(""); + @let date = context.field_value("date").unwrap_or(""); + @let dom_id = format!("line_item_date_{}", &id); + @let edit_dom_id = format!("edit_line_item_date_{}", &id); + div[id = &dom_id] { + form[id = {format!("form_{}", &edit_dom_id)}, + "hx-post" = "/line_item_dates/update", + "hx-target" = {format!("#{}", &dom_id)}, + "hx-swap" = "outerHTML", + class = "flex flex-wrap justify-between items-center gap-2 mt-8 mb-1.5", + autocomplete = "off", + novalidate, + "accept-charset" = "UTF-8"] { + + @let messages = context.errors().map(|item| item.to_string()).collect::>(); + div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { + @for message in messages { + p { @message } + } + } + + input[id = "id", + name = "id", + "type" = "hidden", + value = &id] {} + input[id = "quote_id", + name = "quote_id", + "type" = "hidden", + value = "e_id] {} + div[class = "[flex:1]"] { + label[class = "visually-hidden", "for" = "line_item_date_date"] { "Date" } + input[id = "line_item_date_date", + name = "date", + class = css_for_field(form, "date", "form-input", "border-primary"), + autofocus = "autofocus", + required, + "type" = "date", + value = &date] {} + } + a[class = "button button-light", + "hx-get" = {format!("/line_item_dates/{}", id)}, + "hx-target" = {format!("#{}", edit_dom_id)}, + "hx-trigger" = "click", + "hx-swap" = "outerHTML"] { "Cancel" } + input[name = "commit", + "type" = "submit", + value = "Update date", + class = "button button-secondary", + "_" = "on click add { pointer-events: none }"] {} + } + } + } + + NewForm<'a>(quote_id: &'a str) { + div[id = "line_item_date_new"] { form[id = "form_new", "hx-post" = "/line_item_dates/create", "hx-target" = "#line_item_dates", "hx-swap" = "afterbegin", class = "flex flex-wrap justify-between items-center gap-2 mt-8 mb-1.5", autocomplete = "off", + novalidate, "accept-charset" = "UTF-8"] { - @let form_input_class = if error_message.is_some() { "form-input border-primary" } else { "form-input" }; - @if let Some(message) = error_message { - div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { @message } + + input[id = "quote_id", + name = "quote_id", + "type" = "hidden", + value = "e_id] {} + div[class = "[flex:1]"] { + label[class = "visually-hidden", "for" = "line_item_date_date"] { "Date" } + input[id = "line_item_date_date", + name = "date", + class = "form-input", + autofocus = "autofocus", + required, + "type" = "date"] {} } - @if let Some(id) = &line_item_date.id { - input[id = "line_item_date_id", - name = "id", - "type" = "hidden", - value = id] {} + a[class = "button button-light", + "_" = "on click remove #form_new"] { "Cancel" } + input[name = "commit", + "type" = "submit", + value = "Create date", + class = "button button-secondary", + "_" = "on click add { pointer-events: none }"] {} + } + } + } + + NewFormWithErrors<'a, 'r>(form: &'a Form>) { + @let quote_id = form.context.field_value("quote_id").unwrap_or(""); + @let date = form.context.field_value("date").unwrap_or(""); + div[id = "line_item_date_new"] { + form[id = "form_new", + "hx-post" = "/line_item_dates/create", + "hx-target" = "#line_item_dates", + "hx-swap" = "afterbegin", + class = "flex flex-wrap justify-between items-center gap-2 mt-8 mb-1.5", + autocomplete = "off", + novalidate, + "accept-charset" = "UTF-8"] { + + @let messages = form.context.errors().map(|item| item.to_string()).collect::>(); + div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { + @for message in messages { + p { @message } + } } + input[id = "quote_id", name = "quote_id", "type" = "hidden", - value = &line_item_date.quote_id] {} + value = "e_id] {} div[class = "[flex:1]"] { label[class = "visually-hidden", "for" = "line_item_date_date"] { "Date" } input[id = "line_item_date_date", name = "date", - class = form_input_class, + class = css_for_field(form, "date", "form-input", "border-primary"), autofocus = "autofocus", required, "type" = "date", - value = line_item_date.date_short_form()] {} + value = &date] {} } a[class = "button button-light", "_" = "on click remove #form_new"] { "Cancel" } diff --git a/src/line_items/controller.rs b/src/line_items/controller.rs index 08c638a..90bc4b0 100644 --- a/src/line_items/controller.rs +++ b/src/line_items/controller.rs @@ -2,191 +2,187 @@ use crate::{ line_item_dates, line_items::{ self, - model::{DeleteForm, LineItemForm, LineItemPresenter}, - view::{Create, Destroy, EditForm, LineItem, NewForm, Update}, + model::{DeleteForm, EditLineItemForm, LineItemPresenter, NewLineItemForm}, + view::*, }, - quotes, Result, + quotes, + rocket_ext::HtmxResponder, + Db, Result, }; -use axum::{ - extract::{Path, State}, - response::{Html, IntoResponse}, +use rocket::{ + fairing::AdHoc, + form::{Contextual, Form}, + http::Header, + response::content::RawHtml, }; -use diesel::prelude::SqliteConnection; -use diesel::r2d2::{ConnectionManager, Pool}; -use std::time::Instant; -use tracing::info; -use validator::Validate; - -pub(crate) async fn line_item( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let line_item = line_items::query::read(&pool, id).await?; - let duration = start.elapsed().as_micros(); - info!("li - read duration: {duration} μs"); + +pub(crate) fn stage() -> AdHoc { + AdHoc::on_ignite("LineItem Stage", |rocket| async { + rocket.mount( + "/line_items", + routes![line_item, new, create, edit, update, delete], + ) + }) +} + +#[get("/")] +async fn line_item(db: Db, id: String) -> Result> { + let line_item = db + .run(move |conn| { + let line_item = line_items::query::read(conn, id)?; + Result::Ok(line_item) + }) + .await?; let template = LineItem { line_item: &line_item.into(), }; - Ok(Html(template.to_string())) + Ok(RawHtml(template.to_string())) } -pub(crate) async fn new( - State(pool): State>>, - Path(line_item_date_id): Path, -) -> Result { - let start = Instant::now(); - let quote = quotes::query::from_line_item_date_id(&pool, &line_item_date_id).await?; - let duration = start.elapsed().as_micros(); - info!("lid - read duration: {duration} μs"); - Ok(Html( +#[get("/new/")] +async fn new(db: Db, line_item_date_id: String) -> Result> { + let lid_id = line_item_date_id.clone(); + let quote = db + .run(move |conn| { + let quote = quotes::query::from_line_item_date_id(conn, &lid_id)?; + Result::Ok(quote) + }) + .await?; + + Ok(RawHtml( NewForm { line_item: &LineItemPresenter::from_line_item_date(line_item_date_id), quote: "e.into(), - error_message: None, } .to_string(), )) } -pub(crate) async fn create( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let result = form.validate(); - match result { - Ok(_) => { - let start = Instant::now(); - let line_item = line_items::query::insert(&pool, &form).await?; - let duration = start.elapsed().as_micros(); - info!("li - insert duration: {duration} μs"); - let start = Instant::now(); - let quote = quotes::query::read(&pool, &form.quote_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - Ok(Html( - Create { - line_item: &line_item.into(), - quote: "e.into(), - message: "Item was successfully created.", - } - .to_string(), - ) - .into_response()) +#[post("/create", data = "")] +async fn create(db: Db, form: Form>) -> Result { + print!("Form:\n{form:?}"); + match form.value { + Some(ref li_form) => { + let quote_id = li_form.quote_id.clone(); + let li_form = li_form.clone(); + let line_item = db + .run(move |conn| { + let line_item = line_items::query::insert(conn, &li_form)?; + Result::Ok(line_item) + }) + .await?; + + let quote = db + .run(move |conn| { + let quote = quotes::query::read(conn, "e_id)?; + Result::Ok(quote) + }) + .await?; + + let content = Create { + line_item: &line_item.into(), + quote: "e.into(), + message: "Item was successfully created.", + } + .to_string(); + + Ok(HtmxResponder::Ok(content)) } - Err(errors) => { - info!("ValidationErrors:\n{:?}", errors); - let start = Instant::now(); - let quote = quotes::query::read(&pool, &form.quote_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - let error_message = String::from("Test"); - Ok(Html( - NewForm { - line_item: &form.into(), - quote: "e.into(), - error_message: Some(error_message), - } - .to_string(), - ) - .into_response()) + None => { + let template = NewFormWithErrors { form: &form }; + let content = template.to_string(); + let line_item_date_id = form.context.field_value("line_item_date_id").unwrap_or(""); + let retarget = format!("#line_item_date_{}_line_item_new", line_item_date_id); + Ok(HtmxResponder::Retarget { + content, + retarget: Header::new("HX-Retarget", retarget), + reswap: Header::new("HX-Reswap", "outerhtml".to_string()), + }) } } } -pub(crate) async fn edit( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let line_item = line_items::query::read(&pool, id).await?; - let duration = start.elapsed().as_micros(); - info!("li - read duration: {duration} μs"); - let start = Instant::now(); - let line_item_date = line_item_dates::query::read(&pool, &line_item.line_item_date_id).await?; - let duration = start.elapsed().as_micros(); - info!("lid - read duration: {duration} μs"); - let start = Instant::now(); - let quote = quotes::query::read(&pool, &line_item_date.quote_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - Ok(Html( +#[get("/edit/")] +async fn edit<'a>(db: Db, id: String) -> Result> { + let line_item = db + .run(move |conn| { + let line_item = line_items::query::read(conn, id)?; + Result::Ok(line_item) + }) + .await?; + + let lid_id = line_item.line_item_date_id.clone(); + let quote = db + .run(move |conn| { + let line_item_date = line_item_dates::query::read(conn, &lid_id)?; + let quote = quotes::query::read(conn, &line_item_date.quote_id)?; + Result::Ok(quote) + }) + .await?; + + Ok(RawHtml( EditForm { line_item: &line_item.into(), quote: "e.into(), - error_message: None, } .to_string(), )) } -pub(crate) async fn update( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let result = form.validate(); - match result { - Ok(_) => { - let start = Instant::now(); - let line_item = line_items::query::update(&pool, &form).await?; - let duration = start.elapsed().as_micros(); - info!("li - update duration: {duration} μs"); - let start = Instant::now(); - let quote = - quotes::query::from_line_item_date_id(&pool, &form.line_item_date_id).await?; - info!("Quote total after update: {}", quote.total); - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - Ok(Html( - Update { - line_item: &line_item.into(), - quote: "e.into(), - message: "Item was successfully updated.", - } - .to_string(), - ) - .into_response()) +#[post("/update", data = "")] +async fn update(db: Db, form: Form>) -> Result> { + print!("Form:\n{form:?}"); + match form.value { + Some(ref li_form) => { + let quote_id = li_form.quote_id.clone(); + let li_form = li_form.clone(); + let line_item = db + .run(move |conn| { + let line_item = line_items::query::update(conn, &li_form)?; + Result::Ok(line_item) + }) + .await?; + + let quote = db + .run(move |conn| { + let quote = quotes::query::read(conn, "e_id)?; + Result::Ok(quote) + }) + .await?; + + let content = Update { + line_item: &line_item.into(), + quote: "e.into(), + message: "Item was successfully updated.", + } + .to_string(); + + Ok(RawHtml(content)) } - Err(errors) => { - info!("ValidationErrors:\n{:?}", errors); - let start = Instant::now(); - let quote = - quotes::query::from_line_item_date_id(&pool, &form.line_item_date_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - let error_message = String::from("Test"); - Ok(Html( - EditForm { - line_item: &form.into(), - quote: "e.into(), - error_message: Some(error_message), - } - .to_string(), - ) - .into_response()) + None => { + let template = EditFormWithErrors { form: &form }; + let content = template.to_string(); + Ok(RawHtml(content)) } } } -pub(crate) async fn delete( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let start = Instant::now(); - let line_item = line_items::query::delete(&pool, &form.id).await?; - let duration = start.elapsed().as_micros(); - info!("li - delete duration: {duration} μs"); - let start = Instant::now(); - let quote = quotes::query::from_line_item_date_id(&pool, &line_item.line_item_date_id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - Ok(Html( +#[post("/delete", data = "")] +async fn delete(db: Db, form: Form) -> Result> { + let quote = db + .run(move |conn| { + let line_item = line_items::query::delete(conn, &form.id)?; + let quote = quotes::query::from_line_item_date_id(conn, &line_item.line_item_date_id)?; + Result::Ok(quote) + }) + .await?; + + Ok(RawHtml( Destroy { quote: "e.into(), message: "Item was successfully destroyed.", } .to_string(), - ) - .into_response()) + )) } diff --git a/src/line_items/model.rs b/src/line_items/model.rs index 289554b..f3667a0 100644 --- a/src/line_items/model.rs +++ b/src/line_items/model.rs @@ -1,11 +1,11 @@ -use crate::currency::FORM_CURRENCY_REGEX; -use crate::schema::line_items; +use crate::{ + forms::{validate_amount, validate_quantity}, + schema::line_items, +}; use currency_rs::Currency; use diesel::prelude::*; -use serde::Deserialize; use time::OffsetDateTime; use ulid::Ulid; -use validator::Validate; #[derive(Debug, Insertable, Queryable, Selectable)] pub(crate) struct LineItem { @@ -19,8 +19,9 @@ pub(crate) struct LineItem { pub(crate) updated_at: OffsetDateTime, } -impl From<&LineItemForm> for LineItem { - fn from(value: &LineItemForm) -> Self { +// FIXME: Should be TryFrom due to potential bad parse from quantity +impl From<&EditLineItemForm> for LineItem { + fn from(value: &EditLineItemForm) -> Self { let description = value.description.clone().unwrap_or(String::from("")); let description = if description.is_empty() { None @@ -28,11 +29,11 @@ impl From<&LineItemForm> for LineItem { Some(description) }; LineItem { - id: value.id.clone().unwrap_or(Ulid::new().to_string()), + id: value.id.clone(), line_item_date_id: value.line_item_date_id.clone(), name: value.name.clone(), description, - quantity: value.quantity, + quantity: value.quantity.parse::().unwrap_or(0), unit_price: Currency::new_string(value.unit_price.as_str(), None) .unwrap_or(Currency::new_float(0f64, None)), created_at: OffsetDateTime::now_utc(), @@ -41,19 +42,58 @@ impl From<&LineItemForm> for LineItem { } } -#[derive(Debug, Deserialize, Validate)] -pub(crate) struct LineItemForm { - #[validate(length(min = 1, message = "can't be blank"))] - pub(crate) id: Option, - #[validate(length(min = 1, message = "can't be blank"))] +// FIXME: Should be TryFrom due to potential bad parse from quantity +impl From<&NewLineItemForm> for LineItem { + fn from(value: &NewLineItemForm) -> Self { + let description = value.description.clone().unwrap_or(String::from("")); + let description = if description.is_empty() { + None + } else { + Some(description) + }; + LineItem { + id: Ulid::new().to_string(), + line_item_date_id: value.line_item_date_id.clone(), + name: value.name.clone(), + description, + quantity: value.quantity.parse::().unwrap_or(0), + unit_price: Currency::new_string(value.unit_price.as_str(), None) + .unwrap_or(Currency::new_float(0f64, None)), + created_at: OffsetDateTime::now_utc(), + updated_at: OffsetDateTime::now_utc(), + } + } +} + +#[derive(Clone, Debug, FromForm)] +pub struct EditLineItemForm { + #[field(validate = len(1..))] + pub(crate) id: String, + #[field(validate = len(1..))] pub(crate) line_item_date_id: String, - #[validate(length(min = 1, message = "can't be blank"))] + #[field(validate = len(1..))] pub(crate) quote_id: String, - #[validate(length(min = 1, message = "can't be blank"))] + #[field(validate = len(1..).or_else(msg!("Please enter a name")))] pub(crate) name: String, pub(crate) description: Option, - pub(crate) quantity: i32, - #[validate(regex(path = "FORM_CURRENCY_REGEX"))] + #[field(validate = validate_quantity())] + pub(crate) quantity: String, + #[field(validate = validate_amount())] + pub(crate) unit_price: String, +} + +#[derive(Clone, Debug, FromForm)] +pub struct NewLineItemForm { + #[field(validate = len(1..))] + pub(crate) line_item_date_id: String, + #[field(validate = len(1..))] + pub(crate) quote_id: String, + #[field(validate = len(1..).or_else(msg!("Please enter a name")))] + pub(crate) name: String, + pub(crate) description: Option, + #[field(validate = validate_quantity())] + pub(crate) quantity: String, + #[field(validate = validate_amount())] pub(crate) unit_price: String, } @@ -107,12 +147,27 @@ impl From for LineItemPresenter { } } -impl From for LineItemPresenter { - fn from(value: LineItemForm) -> Self { +impl From for LineItemPresenter { + fn from(value: EditLineItemForm) -> Self { + let unit_price = Currency::new_string(value.unit_price.as_str(), None) + .unwrap_or(Currency::new_float(0f64, None)); + LineItemPresenter { + id: Some(value.id), + line_item_date_id: value.line_item_date_id, + name: value.name, + description: value.description.unwrap_or(String::from("")), + quantity: value.quantity.to_string(), + unit_price, + } + } +} + +impl From for LineItemPresenter { + fn from(value: NewLineItemForm) -> Self { let unit_price = Currency::new_string(value.unit_price.as_str(), None) .unwrap_or(Currency::new_float(0f64, None)); LineItemPresenter { - id: value.id, + id: None, line_item_date_id: value.line_item_date_id, name: value.name, description: value.description.unwrap_or(String::from("")), @@ -122,7 +177,7 @@ impl From for LineItemPresenter { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, FromForm)] pub(crate) struct DeleteForm { pub(crate) id: String, } diff --git a/src/line_items/query.rs b/src/line_items/query.rs index 04f2d5f..4424024 100644 --- a/src/line_items/query.rs +++ b/src/line_items/query.rs @@ -1,75 +1,59 @@ use crate::{ - line_items::model::{LineItem, LineItemForm}, + line_items::model::{EditLineItemForm, LineItem, NewLineItemForm}, schema::{line_item_dates, line_items}, Result, }; use diesel::prelude::*; -use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; -pub(crate) async fn all_for_quote>( - pool: &Pool>, +pub(crate) fn all_for_quote>( + connection: &mut SqliteConnection, quote_id: S, ) -> Result> { - let mut connection = pool.get()?; let records = line_items::table .inner_join(line_item_dates::table) .select(LineItem::as_select()) .filter(line_item_dates::quote_id.eq("e_id.as_ref())) - .get_results(&mut connection)?; + .get_results(connection)?; Ok(records) } -pub(crate) async fn all_for_line_item_date>( - pool: &Pool>, +pub(crate) fn all_for_line_item_date>( + connection: &mut SqliteConnection, line_item_date_id: S, ) -> Result> { - let mut connection = pool.get()?; let records = line_items::table .filter(line_items::line_item_date_id.eq(&line_item_date_id.as_ref())) - .get_results(&mut connection)?; + .get_results(connection)?; Ok(records) } -pub(crate) async fn read>( - pool: &Pool>, - id: S, -) -> Result { - let mut connection = pool.get()?; - read_from_connection(&mut connection, id) -} - -fn read_from_connection>( - connection: &mut PooledConnection>, - id: S, -) -> Result { +pub(crate) fn read>(connection: &mut SqliteConnection, id: S) -> Result { let record = line_items::table .filter(line_items::id.eq(&id.as_ref())) .get_result(connection)?; Ok(record) } -pub(crate) async fn insert( - pool: &Pool>, - form: &LineItemForm, +pub(crate) fn insert( + connection: &mut SqliteConnection, + form: &NewLineItemForm, ) -> Result { let record: LineItem = form.into(); - let mut connection = pool.get()?; diesel::dsl::insert_into(line_items::table) .values(&record) - .execute(&mut connection)?; + .execute(connection)?; Ok(record) } -pub(crate) async fn update( - pool: &Pool>, - form: &LineItemForm, +pub(crate) fn update( + connection: &mut SqliteConnection, + form: &EditLineItemForm, ) -> Result { let record: LineItem = form.into(); - let mut connection = pool.get()?; diesel::dsl::update(line_items::table) .set(( line_items::name.eq(&record.name), @@ -79,27 +63,23 @@ pub(crate) async fn update( line_items::updated_at.eq(&record.updated_at), )) .filter(line_items::id.eq(&record.id)) - .execute(&mut connection)?; + .execute(connection)?; - read_from_connection(&mut connection, &record.id) + read(connection, &record.id) } -pub(crate) async fn delete>( - pool: &Pool>, - id: S, -) -> Result { - let mut connection = pool.get()?; - let record = read_from_connection(&mut connection, &id)?; +pub(crate) fn delete>(connection: &mut SqliteConnection, id: S) -> Result { + let record = read(connection, &id)?; _ = diesel::dsl::delete(line_items::table) .filter(line_items::id.eq(&id.as_ref())) - .execute(&mut connection)?; + .execute(connection)?; Ok(record) } pub(crate) fn delete_all_for_quote>( - tx: &mut PooledConnection>, + tx: &mut SqliteConnection, quote_id: S, ) -> Result { let line_items1 = diesel::alias!(line_items as line_items1); @@ -118,10 +98,7 @@ pub(crate) fn delete_all_for_quote>( Ok(()) } -pub(crate) fn delete_all_for_date>( - tx: &mut PooledConnection>, - id: S, -) -> Result { +pub(crate) fn delete_all_for_date>(tx: &mut SqliteConnection, id: S) -> Result { _ = diesel::dsl::delete(line_items::table) .filter(line_items::line_item_date_id.eq(&id.as_ref())) .execute(tx)?; diff --git a/src/line_items/view.rs b/src/line_items/view.rs index 7c5dd9b..e357c72 100644 --- a/src/line_items/view.rs +++ b/src/line_items/view.rs @@ -1,5 +1,10 @@ -use crate::quotes::view::SwapFooter; -use crate::{layout::Flash, line_items::model::LineItemPresenter, quotes::model::QuotePresenter}; +use crate::{ + forms::css_for_field, + layout::Flash, + line_items::model::{EditLineItemForm, LineItemPresenter, NewLineItemForm}, + quotes::{model::QuotePresenter, view::SwapFooter}, +}; +use rocket::form::{Contextual, Form}; markup::define! { LineItem<'a>(line_item: &'a LineItemPresenter) { @@ -37,9 +42,7 @@ markup::define! { } } - EditForm<'a>(line_item: &'a LineItemPresenter, - quote: &'a QuotePresenter, - error_message: Option) { + EditForm<'a>(line_item: &'a LineItemPresenter, quote: &'a QuotePresenter) { div[id = &line_item.dom_id()] { form[id = &line_item.dom_id(), "hx-post" = "/line_items/update", @@ -47,17 +50,13 @@ markup::define! { "hx-swap" = "outerHTML", class = "flex flex-wrap items-start bg-white gap-2 mb-3 p-2 rounded-md", autocomplete = "off", + novalidate, "accept-charset" = "UTF-8"] { - @let form_input_class = if error_message.is_some() { "form-input border-primary" } else { "form-input" }; - @if let Some(message) = error_message { - div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { @message } - } - @if let Some(id) = &line_item.id { - input[id = "line_item_id", - name = "id", - "type" = "hidden", - value = id] {} - } + + input[id = "id", + name = "id", + "type" = "hidden", + value = &line_item.id.clone().unwrap()] {} input[id = "quote_id", name = "quote_id", "type" = "hidden", @@ -66,10 +65,11 @@ markup::define! { name = "line_item_date_id", "type" = "hidden", value = &line_item.line_item_date_id] {} + div[class = "flex-1 font-bold mb-0"] { input[id = "line_item_name", name = "name", - class = form_input_class, + class = "form-input", autofocus = "autofocus", placeholder = "Name of your item", required, @@ -79,7 +79,7 @@ markup::define! { div[class = "block flex-[0_0_7rem] mb-0"] { input[id = "line_item_quantity", name = "quantity", - class = form_input_class, + class = "form-input", placeholder = "1", required, "type" = "number", @@ -90,7 +90,7 @@ markup::define! { div[class = "block flex-[0_0_9rem] mb-0"] { input[id = "line_item_price", name = "unit_price", - class = form_input_class, + class = "form-input", placeholder = "$100.00", required, "type" = "number", @@ -101,7 +101,7 @@ markup::define! { div[class = "basis-full order-2 m-w-100 font-normal text-[0.875rem] text-[hsl(0,1%,44%)] mb-0"] { textarea[id = "line_item_description", name = "description", - class = {format!("resize-none {}", form_input_class)}, + class = "resize-none form-input", placeholder = "Description (optional)"] { @line_item.description } } a[class = "button button-light", @@ -118,9 +118,97 @@ markup::define! { } } + EditFormWithErrors<'a, 'r>(form: &'a Form>) { + @let context = &form.context; + @let id = context.field_value("id").unwrap_or(""); + @let quote_id = context.field_value("quote_id").unwrap_or(""); + @let line_item_date_id = context.field_value("line_item_date_id").unwrap_or(""); + @let name = context.field_value("name").unwrap_or(""); + @let quantity = context.field_value("quantity").unwrap_or(""); + @let unit_price = context.field_value("unit_price").unwrap_or(""); + @let description = context.field_value("description").unwrap_or(""); + @let dom_id = format!("line_item_{}", &id); + + div[id = &dom_id] { + form[id = &dom_id, + "hx-post" = "/line_items/update", + "hx-target" = {format!("#{}", &dom_id)}, + "hx-swap" = "outerHTML", + class = "flex flex-wrap items-start bg-white gap-2 mb-3 p-2 rounded-md", + autocomplete = "off", + novalidate, + "accept-charset" = "UTF-8"] { + + @let messages = context.errors().map(|item| item.to_string()).collect::>(); + div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { + @for message in messages { + p { @message } + } + } + + input[id = "id", + name = "id", + "type" = "hidden", + value = &id] {} + input[id = "quote_id", + name = "quote_id", + "type" = "hidden", + value = "e_id] {} + input[id = "line_item_date_id", + name = "line_item_date_id", + "type" = "hidden", + value = &line_item_date_id] {} + div[class = "flex-1 font-bold mb-0"] { + input[id = "line_item_name", + name = "name", + class = css_for_field(form, "name", "form-input", "border-primary"), + autofocus = "autofocus", + placeholder = "Name of your item", + "type" = "text", + value = &name] {} + } + div[class = "block flex-[0_0_7rem] mb-0"] { + input[id = "line_item_quantity", + name = "quantity", + class = css_for_field(form, "quantity", "form-input", "border-primary"), + placeholder = "1", + "type" = "number", + min = "1", + step = "1", + value = &quantity] {} + } + div[class = "block flex-[0_0_9rem] mb-0"] { + input[id = "line_item_price", + name = "unit_price", + class = css_for_field(form, "unit_price", "form-input", "border-primary"), + placeholder = "$100.00", + "type" = "number", + min = "0.01", + step = "0.01", + value = &unit_price] {} + } + div[class = "basis-full order-2 m-w-100 font-normal text-[0.875rem] text-[hsl(0,1%,44%)] mb-0"] { + textarea[id = "line_item_description", + name = "description", + class = css_for_field(form, "description", "resize-none form-input", "border-primary"), + placeholder = "Description (optional)"] { @description } + } + a[class = "button button-light", + "hx-get" = {format!("/line_items/{}", &id)}, + "hx-target" = {format!("#{}", &dom_id)}, + "hx-trigger" = "click", + "hx-swap" = "outerHTML"] { "Cancel" } + input[name = "commit", + "type" = "submit", + value = "Update item", + class = "button button-secondary", + "_" = "on click add { pointer-events: none }"] {} + } + } + } + NewForm<'a>(line_item: &'a LineItemPresenter, - quote: &'a QuotePresenter, - error_message: Option) { + quote: &'a QuotePresenter) { div[id = &line_item.dom_id()] { @let line_item_new_dom_id = format!("#line_item_date_{}_line_items", &line_item.line_item_date_id); form[id = "form_new", @@ -129,17 +217,9 @@ markup::define! { "hx-swap" = "beforeend", class = "flex flex-wrap items-start bg-white gap-2 mb-3 p-2 rounded-md", autocomplete = "off", + novalidate, "accept-charset" = "UTF-8"] { - @let form_input_class = if error_message.is_some() { "form-input border-primary" } else { "form-input" }; - @if let Some(message) = error_message { - div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { @message } - } - @if let Some(id) = &line_item.id { - input[id = "line_item_id", - name = "id", - "type" = "hidden", - value = id] {} - } + input[id = "quote_id", name = "quote_id", "type" = "hidden", @@ -151,19 +231,17 @@ markup::define! { div[class = "flex-1 font-bold mb-0"] { input[id = "line_item_name", name = "name", - class = form_input_class, + class = "form-input", autofocus = "autofocus", placeholder = "Name of your item", - required, "type" = "text", value = &line_item.name] {} } div[class = "block flex-[0_0_7rem] mb-0"] { input[id = "line_item_quantity", name = "quantity", - class = form_input_class, + class = "form-input", placeholder = "1", - required, "type" = "number", min = "1", step = "1", @@ -172,9 +250,8 @@ markup::define! { div[class = "block flex-[0_0_9rem] mb-0"] { input[id = "line_item_price", name = "unit_price", - class = form_input_class, + class = "form-input", placeholder = "$100.00", - required, "type" = "number", min = "0.01", step = "0.01", @@ -183,7 +260,7 @@ markup::define! { div[class = "basis-full order-2 m-w-100 font-normal text-[0.875rem] text-[hsl(0,1%,44%)] mb-0"] { textarea[id = "line_item_description", name = "description", - class = {format!("resize-none {}", form_input_class)}, + class = "resize-none form-input", placeholder = "Description (optional)"] { @line_item.description } } a[class = "button button-light", @@ -197,6 +274,90 @@ markup::define! { } } + NewFormWithErrors<'a, 'r>(form: &'a Form>) { + @let context = &form.context; + @let quote_id = context.field_value("quote_id").unwrap_or(""); + @let line_item_date_id = context.field_value("line_item_date_id").unwrap_or(""); + @let name = context.field_value("name").unwrap_or(""); + @let quantity = context.field_value("quantity").unwrap_or(""); + @let unit_price = context.field_value("unit_price").unwrap_or(""); + @let description = context.field_value("description").unwrap_or(""); + + div[id = "line_item_new"] { + @let line_item_new_dom_id = format!("#line_item_date_{}_line_items", &line_item_date_id); + form[id = "form_new", + "hx-post" = "/line_items/create", + "hx-target" = line_item_new_dom_id, + "hx-swap" = "beforeend", + class = "flex flex-wrap items-start bg-white gap-2 mb-3 p-2 rounded-md", + autocomplete = "off", + novalidate, + "accept-charset" = "UTF-8"] { + + @let messages = context.errors().map(|item| item.to_string()).collect::>(); + div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { + @for message in messages { + p { @message } + } + } + + input[id = "quote_id", + name = "quote_id", + "type" = "hidden", + value = "e_id] {} + input[id = "line_item_date_id", + name = "line_item_date_id", + "type" = "hidden", + value = &line_item_date_id] {} + div[class = "flex-1 font-bold mb-0"] { + input[id = "line_item_name", + name = "name", + class = css_for_field(form, "name", "form-input", "border-primary"), + autofocus = "autofocus", + placeholder = "Name of your item", + required, + "type" = "text", + value = &name] {} + } + div[class = "block flex-[0_0_7rem] mb-0"] { + input[id = "line_item_quantity", + name = "quantity", + class = css_for_field(form, "quantity", "form-input", "border-primary"), + placeholder = "1", + required, + "type" = "number", + min = "1", + step = "1", + value = &quantity] {} + } + div[class = "block flex-[0_0_9rem] mb-0"] { + input[id = "line_item_price", + name = "unit_price", + class = css_for_field(form, "unit_price", "form-input", "border-primary"), + placeholder = "$100.00", + required, + "type" = "number", + min = "0.01", + step = "0.01", + value = &unit_price] {} + } + div[class = "basis-full order-2 m-w-100 font-normal text-[0.875rem] text-[hsl(0,1%,44%)] mb-0"] { + textarea[id = "line_item_description", + name = "description", + class = css_for_field(form, "description", "resize-none form-input", "border-primary"), + placeholder = "Description (optional)"] { @description } + } + a[class = "button button-light", + "_" = "on click remove #form_new"] { "Cancel" } + input[name = "commit", + "type" = "submit", + value = "Create item", + class = "button button-secondary", + "_" = "on click add { pointer-events: none }"] {} + } + } + } + Create<'a>(line_item: &'a LineItemPresenter, quote: &'a QuotePresenter, message: &'a str) { @let line_item_new_dom_id = format!("line_item_date_{}_line_item_new", &line_item.line_item_date_id); @LineItem{ line_item } diff --git a/src/main.rs b/src/main.rs index ddaa1f1..cee8678 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,9 @@ #![deny(unreachable_pub, private_bounds, private_interfaces)] #![forbid(unsafe_code)] +#[macro_use] +extern crate rocket; + mod assets; mod currency; mod error; @@ -9,125 +12,54 @@ pub mod layout; pub mod line_item_dates; pub mod line_items; pub mod quotes; +mod rocket_ext; mod schema; mod time; +mod forms; -use assets::asset_handler; -use axum::{ - handler::HandlerWithoutStateExt, - response::Redirect, - routing::{get, post, Router}, -}; -use diesel::connection::SimpleConnection; -use diesel::prelude::*; -use diesel::r2d2::{ConnectionManager, Pool}; -use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; -use dotenvy::dotenv; -use std::env; -use tower_http::trace::{DefaultOnResponse, TraceLayer}; -use tower_http::LatencyUnit; -use tracing::{info, Level}; -use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +use diesel::sqlite::SqliteConnection; +use rocket::response::Redirect; +use rocket::{fairing::AdHoc, Build, Rocket}; +use rocket_sync_db_pools::database; -const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); +#[database("demo")] +struct Db(SqliteConnection); pub(crate) type Result = std::result::Result; -#[tokio::main] -async fn main() { - dotenv().ok(); - - let rust_log = env::var("RUST_LOG").unwrap_or_else(|_| { - let value = "INFO,tower_http=info"; - env::set_var("RUST_LOG", value); - value.into() - }); - println!("RUST_LOG={rust_log}"); - - tracing_subscriber::registry() - .with(EnvFilter::from_default_env()) - .with(fmt::layer()) - .init(); - - let database_url = env::var("DATABASE_URL").unwrap(); - println!("DATABASE_URL={database_url}"); - - let manager = ConnectionManager::::new(database_url); - let pool = Pool::builder() - .max_size(10) - .build(manager) - .expect("Could not build connection pool"); - - let mut conn = pool.get().unwrap(); +#[launch] +fn rocket() -> _ { + rocket::build() + .attach(AdHoc::on_ignite("Diesel SQLite Stage", |rocket| async { + rocket + .attach(Db::fairing()) + .attach(AdHoc::on_ignite("Diesel Migrations", run_migrations)) + })) + .mount("/", routes![index]) + .attach(quotes::controller::stage()) + .attach(line_item_dates::controller::stage()) + .attach(line_items::controller::stage()) + .attach(assets::stage()) +} - conn.batch_execute(" - PRAGMA journal_mode = WAL; -- better write-concurrency - PRAGMA synchronous = NORMAL; -- fsync only in critical moments - PRAGMA wal_autocheckpoint = 1000; -- write WAL changes back every 1000 pages, for an in average 1MB WAL file. May affect readers if number is increased - PRAGMA wal_checkpoint(TRUNCATE); -- free some space by truncating possibly massive WAL files from the last run. - PRAGMA busy_timeout = 250; -- sleep if the database is busy - PRAGMA foreign_keys = ON; -- enforce foreign keys - ").unwrap(); +#[get("/")] +fn index() -> Redirect { + Redirect::to(uri!("/quotes")) +} - conn.run_pending_migrations(MIGRATIONS).unwrap(); - drop(conn); +async fn run_migrations(rocket: Rocket) -> Rocket { + use diesel_migrations::{embed_migrations, EmbeddedMigrations, MigrationHarness}; - let trace_layer = TraceLayer::new_for_http().on_response( - DefaultOnResponse::new() - .level(Level::INFO) - .latency_unit(LatencyUnit::Micros), - ); + const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); - let app = Router::new() - .route("/", get(|| async { Redirect::to("/quotes") })) - .route("/quotes", get(quotes::controller::index)) - .route("/quotes/:id", get(quotes::controller::quote)) - .route("/quotes/show/:id", get(quotes::controller::show)) - .route("/quotes/new", get(quotes::controller::new)) - .route("/quotes/create", post(quotes::controller::create)) - .route("/quotes/edit/:id", get(quotes::controller::edit)) - .route("/quotes/update", post(quotes::controller::update)) - .route("/quotes/delete", post(quotes::controller::delete)) - .route_service("/dist/*file", asset_handler.into_service()) - .route( - "/line_item_dates/:id", - get(line_item_dates::controller::line_item_date), - ) - .route( - "/line_item_dates/new/:quote_id", - get(line_item_dates::controller::new), - ) - .route( - "/line_item_dates/create", - post(line_item_dates::controller::create), - ) - .route( - "/line_item_dates/edit/:id", - get(line_item_dates::controller::edit), - ) - .route( - "/line_item_dates/update", - post(line_item_dates::controller::update), - ) - .route( - "/line_item_dates/delete", - post(line_item_dates::controller::delete), - ) - .route( - "/line_items/new/:line_item_date_id", - get(line_items::controller::new), - ) - .route("/line_items/:id", get(line_items::controller::line_item)) - .route("/line_items/create", post(line_items::controller::create)) - .route("/line_items/edit/:id", get(line_items::controller::edit)) - .route("/line_items/update", post(line_items::controller::update)) - .route("/line_items/delete", post(line_items::controller::delete)) - .with_state(pool) - .layer(trace_layer) - .fallback_service(asset_handler.into_service()); + Db::get_one(&rocket) + .await + .expect("failure obtaining database connection") + .run(|conn| { + conn.run_pending_migrations(MIGRATIONS) + .expect("failure running diesel migrations"); + }) + .await; - let addr: std::net::SocketAddr = "[::]:8081".parse().unwrap(); - info!("listening on {addr}"); - let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, app).await.unwrap(); + rocket } diff --git a/src/quotes/controller.rs b/src/quotes/controller.rs index 8e518c8..4972a02 100644 --- a/src/quotes/controller.rs +++ b/src/quotes/controller.rs @@ -4,33 +4,40 @@ use crate::{ line_items::{self, model::LineItemPresenter}, quotes::{ self, - model::{DeleteForm, QuoteForm, QuotePresenter}, - view::{Create, EditForm, Index, NewForm, Quote, Show, Update}, + model::{DeleteForm, EditQuoteForm, NewQuoteForm, QuotePresenter}, + view::*, }, - Result, + rocket_ext::HtmxResponder, + Db, Result, }; -use axum::{ - extract::{Path, State}, - response::{Html, IntoResponse}, -}; -use diesel::prelude::SqliteConnection; -use diesel::r2d2::{ConnectionManager, Pool}; use itertools::Itertools; -use std::time::Instant; -use tracing::info; -use validator::Validate; - -pub(crate) async fn index( - State(pool): State>>, -) -> Result> { - let start = Instant::now(); - let quotes = quotes::query::all(&pool) - .await? - .into_iter() - .map(|record| record.into()) - .collect::>(); - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); +use rocket::{ + fairing::AdHoc, + form::{Contextual, Form}, + http::Header, + response::content::RawHtml, +}; + +pub(crate) fn stage() -> AdHoc { + AdHoc::on_ignite("Quote Stage", |rocket| async { + rocket.mount( + "/quotes", + routes![index, quote, show, new, create, edit, update, delete], + ) + }) +} + +#[get("/")] +async fn index(db: Db) -> Result> { + let quotes = db + .run(move |conn| { + let records = quotes::query::all(conn)? + .into_iter() + .map(|record| record.into()) + .collect::>(); + Result::Ok(records) + }) + .await?; let template = Layout { head: markup::new! { @@ -39,50 +46,56 @@ pub(crate) async fn index( body: Index { quotes }, }; - Ok(Html(template.to_string())) + Ok(RawHtml(template.to_string())) } -pub(crate) async fn quote( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let quote = quotes::query::read(&pool, &id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); +#[get("/")] +async fn quote(db: Db, id: String) -> Result> { + let quote = db + .run(move |conn| { + let quote = quotes::query::read(conn, &id)?; + Result::Ok(quote) + }) + .await?; let quote = Quote { quote: "e.into(), }; - Ok(Html(quote.to_string())) + Ok(RawHtml(quote.to_string())) } -pub(crate) async fn show( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let quote = quotes::query::read(&pool, &id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - let start = Instant::now(); - let line_item_dates = line_item_dates::query::all(&pool, "e.id) - .await? - .into_iter() - .map(|record| record.into()) - .collect::>(); - let duration = start.elapsed().as_micros(); - info!("lid - read all duration: {duration} μs"); - let start = Instant::now(); - let line_items = line_items::query::all_for_quote(&pool, "e.id) - .await? - .into_iter() - .map(|record| record.into()) - .collect::>() - .into_iter() - .into_group_map_by(|line_item| line_item.line_item_date_id.clone()); - let duration = start.elapsed().as_micros(); - info!("li - read all duration: {duration} μs"); +#[get("/show/")] +async fn show(db: Db, id: String) -> Result> { + let quote = db + .run(move |conn| { + let quote = quotes::query::read(conn, &id)?; + Result::Ok(quote) + }) + .await?; + + let quote_id = quote.id.clone(); + let line_item_dates = db + .run(move |conn| { + let records = line_item_dates::query::all(conn, quote_id)? + .into_iter() + .map(|record| record.into()) + .collect::>(); + Result::Ok(records) + }) + .await?; + + let quote_id = quote.id.clone(); + let line_items = db + .run(move |conn| { + let index = line_items::query::all_for_quote(conn, quote_id)? + .into_iter() + .map(|record| record.into()) + .collect::>() + .into_iter() + .into_group_map_by(|line_item| line_item.line_item_date_id.clone()); + Result::Ok(index) + }) + .await?; let quote_name = quote.name.clone(); let template = Layout { @@ -96,119 +109,104 @@ pub(crate) async fn show( }, }; - Ok(Html(template.to_string())) + Ok(RawHtml(template.to_string())) } -pub(crate) async fn new() -> impl IntoResponse { - Html( - NewForm { - quote: &QuotePresenter::default(), - error_message: None, - } - .to_string(), - ) +#[get("/new")] +async fn new() -> RawHtml { + RawHtml(NewForm {}.to_string()) } -pub(crate) async fn create( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let result = form.validate(); - match result { - Ok(_) => { - let start = Instant::now(); - let quote = quotes::query::insert(&pool, &form).await?; - let duration = start.elapsed().as_micros(); - info!("quo - insert duration: {duration} μs"); - Ok(Html( - Create { - quote: "e.into(), - message: "Quote was successfully created.", - } - .to_string(), - ) - .into_response()) +#[post("/create", data = "")] +async fn create(db: Db, form: Form>) -> Result { + print!("Form:\n{form:?}"); + match form.value { + Some(ref quote_form) => { + let quote_form = quote_form.clone(); + let quote = db + .run(move |conn| { + let record = quotes::query::insert(conn, "e_form)?; + Result::Ok(record) + }) + .await?; + + let content = Create { + quote: "e.into(), + message: "Quote was successfully created.", + } + .to_string(); + + Ok(HtmxResponder::Ok(content)) } - Err(errors) => { - info!("ValidationErrors:\n{:?}", errors); - let error_message = String::from("Test"); - Ok(Html( - NewForm { - quote: &form.into(), - error_message: Some(error_message), - } - .to_string(), - ) - .into_response()) + None => { + let template = NewFormWithErrors { form: &form }; + let content = template.to_string(); + Ok(HtmxResponder::Retarget { + content, + retarget: Header::new("HX-Retarget", "#quote_new".to_string()), + reswap: Header::new("HX-Reswap", "outerhtml".to_string()), + }) } } } -pub(crate) async fn edit( - State(pool): State>>, - Path(id): Path, -) -> Result { - let start = Instant::now(); - let quote = quotes::query::read(&pool, &id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - read duration: {duration} μs"); - Ok(Html( +#[get("/edit/")] +async fn edit(db: Db, id: String) -> Result> { + let quote = db + .run(move |conn| { + let quote = quotes::query::read(conn, &id)?; + Result::Ok(quote) + }) + .await?; + + Ok(RawHtml( EditForm { quote: "e.into(), - error_message: None, } .to_string(), )) } -pub(crate) async fn update( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let result = form.validate(); - match result { - Ok(_) => { - let start = Instant::now(); - let quote = quotes::query::update(&pool, &form).await?; - let duration = start.elapsed().as_micros(); - info!("quo - update duration: {duration} μs"); - Ok(Html( +#[post("/update", data = "")] +async fn update(db: Db, form: Form>) -> Result> { + match form.value { + Some(ref quote_form) => { + let quote_form = quote_form.clone(); + let quote = db + .run(move |conn| { + let record = quotes::query::update(conn, "e_form)?; + Result::Ok(record) + }) + .await?; + + Ok(RawHtml( Update { quote: "e.into(), message: "Quote was successfully updated.", } .to_string(), - ) - .into_response()) + )) } - Err(errors) => { - info!("ValidationErrors:\n{:?}", errors); - let error_message = String::from("Test"); - Ok(Html( - EditForm { - quote: &form.into(), - error_message: Some(error_message), - } - .to_string(), - ) - .into_response()) + None => { + let template = EditFormWithErrors { form: &form }; + let html = template.to_string(); + Ok(RawHtml(html)) } } } -pub(crate) async fn delete( - State(pool): State>>, - axum::Form(form): axum::Form, -) -> Result { - let start = Instant::now(); - quotes::query::delete(&pool, &form.id).await?; - let duration = start.elapsed().as_micros(); - info!("quo - delete duration: {duration} μs"); - Ok(Html( +#[post("/delete", data = "")] +async fn delete(db: Db, form: Form) -> Result> { + db.run(move |conn| { + quotes::query::delete(conn, &form.id)?; + Result::Ok(()) + }) + .await?; + + Ok(RawHtml( Flash { message: "Quote was successfully destroyed.", } .to_string(), - ) - .into_response()) + )) } diff --git a/src/quotes/model.rs b/src/quotes/model.rs index a6eaa92..b183361 100644 --- a/src/quotes/model.rs +++ b/src/quotes/model.rs @@ -2,10 +2,8 @@ use crate::schema::quotes; use currency_rs::Currency; use diesel::prelude::*; use diesel::sql_types::*; -use serde::Deserialize; use time::OffsetDateTime; use ulid::Ulid; -use validator::Validate; #[derive(Debug, QueryableByName)] pub struct QuoteWithTotal { @@ -30,10 +28,10 @@ pub struct Quote { pub updated_at: OffsetDateTime, } -impl From<&QuoteForm> for Quote { - fn from(value: &QuoteForm) -> Self { +impl From<&NewQuoteForm> for Quote { + fn from(value: &NewQuoteForm) -> Self { Quote { - id: value.id.clone().unwrap_or(Ulid::new().to_string()), + id: Ulid::new().to_string(), name: value.name.clone(), created_at: OffsetDateTime::now_utc(), updated_at: OffsetDateTime::now_utc(), @@ -41,11 +39,28 @@ impl From<&QuoteForm> for Quote { } } -#[derive(Debug, Deserialize, Validate)] -pub(crate) struct QuoteForm { - #[validate(length(min = 1, message = "can't be blank"))] - pub(crate) id: Option, - #[validate(length(min = 1, message = "Can't be blank"))] +impl From<&EditQuoteForm> for Quote { + fn from(value: &EditQuoteForm) -> Self { + Quote { + id: value.id.clone(), + name: value.name.clone(), + created_at: OffsetDateTime::now_utc(), + updated_at: OffsetDateTime::now_utc(), + } + } +} + +#[derive(Clone, Debug, FromForm)] +pub struct NewQuoteForm { + #[field(validate = len(1..).or_else(msg!("Please enter a name")))] + pub(crate) name: String, +} + +#[derive(Clone, Debug, FromForm)] +pub struct EditQuoteForm { + #[field(validate = len(1..))] + pub(crate) id: String, + #[field(validate = len(1..).or_else(msg!("Please enter a name")))] pub(crate) name: String, } @@ -99,17 +114,27 @@ impl From for QuotePresenter { } } -impl From for QuotePresenter { - fn from(value: QuoteForm) -> Self { +impl From for QuotePresenter { + fn from(value: NewQuoteForm) -> Self { + QuotePresenter { + id: None, + name: value.name, + total: Currency::new_float(0f64, None), + } + } +} + +impl From for QuotePresenter { + fn from(value: EditQuoteForm) -> Self { QuotePresenter { - id: value.id, + id: Some(value.id), name: value.name, total: Currency::new_float(0f64, None), } } } -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, FromForm)] pub(crate) struct DeleteForm { pub(crate) id: String, } diff --git a/src/quotes/query.rs b/src/quotes/query.rs index f9f8477..73ecaf9 100644 --- a/src/quotes/query.rs +++ b/src/quotes/query.rs @@ -1,30 +1,25 @@ use crate::{ line_item_dates, - quotes::model::{Quote, QuoteForm, QuoteWithTotal}, + quotes::model::{EditQuoteForm, NewQuoteForm, Quote, QuoteWithTotal}, schema::quotes, Result, }; use diesel::prelude::*; -use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; -pub(crate) async fn all(pool: &Pool>) -> Result> { - let mut connection = pool.get()?; - let records = quotes::table - .order_by(quotes::id) - .get_results(&mut connection)?; +pub(crate) fn all(connection: &mut SqliteConnection) -> Result> { + let records = quotes::table.order_by(quotes::id).get_results(connection)?; Ok(records) } -pub(crate) async fn read>( - pool: &Pool>, +pub(crate) fn read>( + connection: &mut SqliteConnection, id: S, ) -> Result { - let mut connection = pool.get()?; - read_from_connection(&mut connection, id) + read_from_connection(connection, id) } fn read_from_connection>( - connection: &mut PooledConnection>, + connection: &mut SqliteConnection, id: S, ) -> Result { // language=SQL @@ -48,8 +43,8 @@ fn read_from_connection>( Ok(record) } -pub(crate) async fn from_line_item_date_id>( - pool: &Pool>, +pub(crate) fn from_line_item_date_id>( + connection: &mut SqliteConnection, id: S, ) -> Result { // language=SQL @@ -68,51 +63,44 @@ pub(crate) async fn from_line_item_date_id>( inner join quotes q on lid.quote_id = q.id where lid.id = ? "#; - let mut connection = pool.get()?; let record = diesel::dsl::sql_query(sql) .bind::(id.as_ref()) - .get_result(&mut connection)?; + .get_result(connection)?; Ok(record) } -pub(crate) async fn insert( - pool: &Pool>, - form: &QuoteForm, -) -> Result { +pub(crate) fn insert(connection: &mut SqliteConnection, form: &NewQuoteForm) -> Result { let record: Quote = form.into(); - let mut connection = pool.get()?; diesel::dsl::insert_into(quotes::table) .values(&record) - .execute(&mut connection)?; + .execute(connection)?; Ok(record) } -pub(crate) async fn update( - pool: &Pool>, - form: &QuoteForm, +pub(crate) fn update( + connection: &mut SqliteConnection, + form: &EditQuoteForm, ) -> Result { let record: Quote = form.into(); - let mut connection = pool.get()?; diesel::dsl::update(quotes::table) .set(( quotes::name.eq(&record.name), quotes::updated_at.eq(&record.updated_at), )) .filter(quotes::id.eq(&record.id)) - .execute(&mut connection)?; + .execute(connection)?; - read_from_connection(&mut connection, &record.id) + read_from_connection(connection, &record.id) } -pub(crate) async fn delete>( - pool: &Pool>, +pub(crate) fn delete>( + connection: &mut SqliteConnection, id: S, ) -> Result { - let mut connection = pool.get()?; - let record = read_from_connection(&mut connection, &id)?; + let record = read_from_connection(connection, &id)?; _ = connection.transaction::<_, _, _>(|tx| { line_item_dates::query::delete_all_for_quote(tx, &id)?; diff --git a/src/quotes/view.rs b/src/quotes/view.rs index fd454f5..350761f 100644 --- a/src/quotes/view.rs +++ b/src/quotes/view.rs @@ -1,9 +1,11 @@ use crate::{ + forms::css_for_field, layout::Flash, line_item_dates::{model::LineItemDatePresenter, view::LineItemDate}, line_items::model::LineItemPresenter, - quotes::model::QuotePresenter, + quotes::model::{EditQuoteForm, NewQuoteForm, QuotePresenter}, }; +use rocket::form::{Contextual, Form}; use std::collections::HashMap; markup::define! { @@ -96,7 +98,7 @@ markup::define! { @InitialFooter { quote } } - EditForm<'a>(quote: &'a QuotePresenter, error_message: Option) { + EditForm<'a>(quote: &'a QuotePresenter) { div[id = "e.dom_id()] { form[id = format!("form_{}", "e.id()), "hx-post" = "/quotes/update", @@ -104,26 +106,20 @@ markup::define! { "hx-swap" = "outerHTML", class = "flex flex-wrap justify-between items-center gap-3 bg-white rounded-md mb-4 px-4 py-2 shadow-[1px_3px_6px_hsl(0,0%,0%,0.1)]", autocomplete = "off", + novalidate, "accept-charset" = "UTF-8"] { - @let form_input_class = if error_message.is_some() { "form-input border-primary" } else { "form-input" }; - @if let Some(message) = error_message { - div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { @message } - } div[class = "[flex:1]"] { - @if let Some(id) = "e.id { - input[id = "quote_id", - name = "id", - "type" = "hidden", - value = id] {} - } + input[id = "quote_id", + name = "id", + "type" = "hidden", + value = "e.id.clone().unwrap()] {} label[class = "visually-hidden", "for" = "quote_name"] { "Name" } input[id = "quote_name", name = "name", - class = form_input_class, + class = "form-input", autofocus = "autofocus", placeholder = "Name of your quote", - required, "type" = "text", value = "e.name] {} } @@ -140,36 +136,113 @@ markup::define! { } } - NewForm<'a>(quote: &'a QuotePresenter, error_message: Option) { - div[id = "e.dom_id()] { + EditFormWithErrors<'a, 'r>(form: &'a Form>) { + @let id = form.context.field_value("id").unwrap_or(""); + @let name = form.context.field_value("name").unwrap_or(""); + @let dom_id = format!("quote_{}", &id); + div[id = &dom_id] { + form[id = format!("form_{}", &id), + "hx-post" = "/quotes/update", + "hx-target" = {format!("#{}", &dom_id)}, + "hx-swap" = "outerHTML", + class = "flex flex-wrap justify-between items-center gap-3 bg-white rounded-md mb-4 px-4 py-2 shadow-[1px_3px_6px_hsl(0,0%,0%,0.1)]", + autocomplete = "off", + novalidate, + "accept-charset" = "UTF-8"] { + + @let messages = form.context.errors().map(|item| item.to_string()).collect::>(); + div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { + @for message in messages { + p { @message } + } + } + + div[class = "[flex:1]"] { + input[id = "quote_id", + name = "id", + "type" = "hidden", + value = &id] {} + label[class = "visually-hidden", "for" = "quote_name"] { "Name" } + input[id = "quote_name", + name = "name", + class = css_for_field(form, "name", "form-input", "border-primary"), + autofocus = "autofocus", + placeholder = "Name of your quote", + "type" = "text", + value = &name] {} + } + a[class = "button button-light", + "hx-get" = {format!("/quotes/{}", &id)}, + "hx-target" = {format!("#{}", &dom_id)}, + "hx-trigger" = "click"] { "Cancel" } + input[name = "commit", + "type" = "submit", + value = "Update quote", + class = "button button-secondary", + "_" = "on click add { pointer-events: none }"] {} + } + } + } + + NewForm() { + div[id = "quote_new"] { form[id = "form_new", "hx-post" = "/quotes/create", "hx-target" = "#quotes_empty", "hx-swap" = "afterend", class = "flex flex-wrap justify-between items-center gap-3 bg-white rounded-md mb-4 px-4 py-2 shadow-[1px_3px_6px_hsl(0,0%,0%,0.1)]", autocomplete = "off", + novalidate, "accept-charset" = "UTF-8"] { - @let form_input_class = if error_message.is_some() { "form-input border-primary" } else { "form-input" }; - @if let Some(message) = error_message { - div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { @message } - } div[class = "[flex:1]"] { - @if let Some(id) = "e.id { - input[id = "quote_id", - name = "id", - "type" = "hidden", - value = id] {} + label[class = "visually-hidden", "for" = "quote_name"] { "Name" } + input[id = "quote_name", + name = "name", + class = "form-input", + autofocus = "autofocus", + placeholder = "Name of your quote", + "type" = "text"] {} + } + a[class = "button button-light", + "_" = "on click remove #form_new"] { "Cancel" } + input[name = "commit", + "type" = "submit", + value = "Create quote", + class = "button button-secondary", + "_" = "on click add { pointer-events: none }"] {} + } + } + } + + NewFormWithErrors<'a, 'r>(form: &'a Form>) { + @let name = form.context.field_value("name").unwrap_or(""); + div[id = "quote_new"] { + form[id = "form_new", + "hx-post" = "/quotes/create", + "hx-target" = "#quotes_empty", + "hx-swap" = "afterend", + class = "flex flex-wrap justify-between items-center gap-3 bg-white rounded-md mb-4 px-4 py-2 shadow-[1px_3px_6px_hsl(0,0%,0%,0.1)]", + autocomplete = "off", + novalidate, + "accept-charset" = "UTF-8"] { + + @let messages = form.context.errors().map(|item| item.to_string()).collect::>(); + div[class = "w-full text-primary bg-primary-bg p-2 rounded-md"] { + @for message in messages { + p { @message } } + } + + div[class = "[flex:1]"] { label[class = "visually-hidden", "for" = "quote_name"] { "Name" } input[id = "quote_name", name = "name", - class = form_input_class, + class = "form-input border-primary", autofocus = "autofocus", placeholder = "Name of your quote", - required, "type" = "text", - value = "e.name] {} + value = &name] {} } a[class = "button button-light", "_" = "on click remove #form_new"] { "Cancel" } diff --git a/src/rocket_ext.rs b/src/rocket_ext.rs new file mode 100644 index 0000000..5da1198 --- /dev/null +++ b/src/rocket_ext.rs @@ -0,0 +1,12 @@ +use rocket::http::Header; + +#[derive(Responder)] +#[response(status = 200, content_type = "html")] +pub(crate) enum HtmxResponder { + Ok(String), + Retarget { + content: String, + retarget: Header<'static>, + reswap: Header<'static>, + }, +} diff --git a/ui/src/app.js b/ui/src/app.js index fdfd8c3..740acaa 100644 --- a/ui/src/app.js +++ b/ui/src/app.js @@ -1,16 +1,3 @@ import "htmx.org"; import * as hyperscript from "hyperscript.org"; hyperscript.browserInit(); - -// document.addEventListener("turbo:before-frame-render", (event) => { -// const inputs = event.detail.newFrame.querySelectorAll("input, select, textarea"); -// inputs.forEach(input => { -// input.addEventListener( -// "invalid", -// _event => { -// input.classList.add("error"); -// }, -// false -// ); -// }); -// }) \ No newline at end of file