diff --git a/.gitignore b/.gitignore index 307add8..bde1463 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target +/webui/dist /.vscode install -secrets \ No newline at end of file +secrets diff --git a/Cargo.lock b/Cargo.lock index cb01574..2408f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -54,6 +54,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -71,9 +77,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" + +[[package]] +name = "anymap2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c" [[package]] name = "arc-swap" @@ -93,11 +105,23 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy 0.5.2", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-compression" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e9eabd7a98fe442131a17c316bd9349c43695e49e730c3c8e12cfb5f4da2693" +checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" dependencies = [ "brotli", "flate2", @@ -109,6 +133,17 @@ dependencies = [ "zstd-safe", ] +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -128,9 +163,15 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.80" @@ -139,9 +180,15 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "auto_enums" version = "0.8.5" @@ -151,7 +198,7 @@ dependencies = [ "derive_utils", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -327,6 +374,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -338,6 +394,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -348,11 +407,25 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" -version = "5.0.0" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67" +checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -387,6 +460,38 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "camino" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cc" version = "1.0.97" @@ -398,6 +503,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -417,6 +531,33 @@ dependencies = [ "windows-targets 0.52.5", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "combine" version = "3.8.1" @@ -430,6 +571,51 @@ dependencies = [ "unreachable", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const_format" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a214c7af3d04997541b18d432afaff4c455e79e2029079647e72fc2bd27673" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f6ff08fd20f4f299298a28e2dfa8a8ba1036e6cd2460ac1de7b425d76f2500" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constcat" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7e35aee659887cbfb97aaf227ac12cad1a9d7c71e55ff3376839ed4e282d08" + [[package]] name = "convert_case" version = "0.6.0" @@ -479,6 +665,12 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -509,7 +701,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.60", + "syn 2.0.64", +] + +[[package]] +name = "darling" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.64", +] + +[[package]] +name = "darling_macro" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.64", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", ] [[package]] @@ -546,17 +785,324 @@ checksum = "61bb5a1014ce6dfc2a378578509abe775a5aa06bff584a547555d9efdb81b926" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dioxus" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e50735a28b303b0d67e1b5637fb57e4711bf2776266290cbc987c0adfdabb55" +dependencies = [ + "dioxus-config-macro", + "dioxus-core", + "dioxus-core-macro", + "dioxus-fullstack", + "dioxus-hooks", + "dioxus-hot-reload", + "dioxus-html", + "dioxus-router", + "dioxus-signals", + "dioxus-web", +] + +[[package]] +name = "dioxus-cli-config" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4661064bad2f0b12929faf6c9cea4d94e60217ba6b11ff4146b505a57124b" +dependencies = [ + "once_cell", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "dioxus-config-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe244197b320dec9e9f38742985fe98c058136ada770df73e9429878ed92863" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dioxus-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "088daa3f45aaa729e9eef32dc0a9393dd709ee906b092089e5839cad1cad7c85" +dependencies = [ + "futures-channel", + "futures-util", + "generational-box", + "longest-increasing-subsequence", + "rustc-hash", + "serde", + "slab", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "dioxus-core-macro" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e29a07448245451334eec2883a394e207f28caedf0a57fd1a903e9ccea0b9531" +dependencies = [ + "constcat", + "convert_case", + "dioxus-rsx", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.64", +] + +[[package]] +name = "dioxus-debug-cell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2" + +[[package]] +name = "dioxus-fullstack" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7fb1a22ff7af8756bc9506eebfbecf374b1c8c57f087c85c752ba8bd767fce" +dependencies = [ + "async-trait", + "base64 0.21.7", + "bytes", + "ciborium", + "dioxus-hot-reload", + "dioxus-lib", + "dioxus-web", + "dioxus_server_macro", + "futures-util", + "once_cell", + "serde", + "serde_json", + "server_fn", + "tracing", + "web-sys", +] + +[[package]] +name = "dioxus-hooks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8c7019308a6d8381fce84a51006f207407af265aebc5425871399c98d788e4" +dependencies = [ + "dioxus-core", + "dioxus-debug-cell", + "dioxus-signals", + "futures-channel", + "futures-util", + "generational-box", + "slab", + "thiserror", + "tracing", +] + +[[package]] +name = "dioxus-hot-reload" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d5a28a2af6655473c6521fb5a428538807b985e8e5f1a8c30e2ab71bd54e637" +dependencies = [ + "dioxus-core", + "dioxus-html", + "dioxus-rsx", + "interprocess-docfix", + "serde", + "serde_json", +] + +[[package]] +name = "dioxus-html" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d612d9732f32adc1852e13e1387a9d5baa710b0b004641b5123def53065c8d" +dependencies = [ + "async-trait", + "dioxus-core", + "dioxus-html-internal-macro", + "enumset", + "euclid", + "futures-channel", + "generational-box", + "keyboard-types", + "serde", + "serde-value", + "serde_json", + "serde_repr", + "tracing", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "dioxus-html-internal-macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1799f34affdb158f6ebec23b46b11f9e65de0bbadbbb781dc68c3eddfe6fd32b" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.64", +] + +[[package]] +name = "dioxus-interpreter-js" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc68a22e33562317b40ccc7b2d140017e510745c5d7e062e911c6a4f9042e4b1" +dependencies = [ + "js-sys", + "md5", + "sledgehammer_bindgen", + "sledgehammer_utils", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "dioxus-lib" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af36a9f985ad68783acf62dc276b0a8f0aa928f1c3b42f78e2ae222b19d445c" +dependencies = [ + "dioxus-core", + "dioxus-core-macro", + "dioxus-hooks", + "dioxus-html", + "dioxus-rsx", + "dioxus-signals", +] + +[[package]] +name = "dioxus-logger" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe09dc9773dc1f1bb0d38529203d6555d08f67aadca5cf955ac3d1a9e69880" +dependencies = [ + "console_error_panic_hook", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + +[[package]] +name = "dioxus-router" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4cbbc1aff811aa3715c94a7ca0375be0be34356aba7f3897328d844323519b" +dependencies = [ + "dioxus-cli-config", + "dioxus-lib", + "dioxus-router-macro", + "gloo", + "gloo-utils 0.1.7", + "js-sys", + "tracing", + "url", + "urlencoding", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "dioxus-router-macro" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fcb6e4a203dc816bca720c638562d5a782695dc71d6598de088ce50ba2a0f8" +dependencies = [ + "proc-macro2", + "quote", + "slab", + "syn 2.0.64", +] + +[[package]] +name = "dioxus-rsx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa83056104f63fdc6f7f9fc1137208c7b7648bf88d6c86db1e095f15297a0f3" +dependencies = [ + "dioxus-core", + "internment", + "krates", + "proc-macro2", + "quote", + "syn 2.0.64", + "tracing", +] + +[[package]] +name = "dioxus-signals" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fd04e2b3739d5c12255005cbf3185446e750fc2b3eeee9fa4e83c989132415c" +dependencies = [ + "dioxus-core", + "futures-channel", + "futures-util", + "generational-box", + "once_cell", + "parking_lot", + "rustc-hash", + "tracing", +] + +[[package]] +name = "dioxus-web" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75cfbe115b193a05c649a80a54a90a6bdd88779694a617daf27d287d6fb944f" +dependencies = [ + "async-trait", + "console_error_panic_hook", + "dioxus-core", + "dioxus-html", + "dioxus-interpreter-js", + "futures-channel", + "futures-util", + "generational-box", + "js-sys", + "rustc-hash", + "serde", + "serde-wasm-bindgen", + "serde_json", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "dioxus_server_macro" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b723da95503f739f9fc5fb23b6ad1e456f1438a496a0427210fa94e4e0d5fe9" dependencies = [ - "block-buffer", - "crypto-common", + "convert_case", + "proc-macro2", + "quote", + "server_fn_macro", + "syn 2.0.64", ] [[package]] @@ -582,9 +1128,9 @@ checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" [[package]] name = "either" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "encoding_rs" @@ -595,6 +1141,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enumset" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226c0da7462c13fb57e5cc9e0dc8f0635e7d27f276a3a7fd30054647f669007d" +dependencies = [ + "enumset_derive", +] + +[[package]] +name = "enumset_derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08b6c6ab82d70f08844964ba10c7babb716de2ecaeab9be5717918a5177d3af" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.64", +] + [[package]] name = "env_logger" version = "0.10.2" @@ -614,6 +1181,64 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "euclid" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f253bc5c813ca05792837a0ff4b3a580336b224512d48f7eda1d7dd9210787" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + [[package]] name = "firestore" version = "0.41.0" @@ -636,6 +1261,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.30" @@ -719,6 +1350,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "futures-locks" version = "0.7.1" @@ -738,7 +1379,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -829,6 +1470,15 @@ dependencies = [ "url", ] +[[package]] +name = "generational-box" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f132919c96b85c02a067ceae965fd50ace57111e2f55c7384b95ac191f4d966b" +dependencies = [ + "parking_lot", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -850,9 +1500,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -867,6 +1517,206 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "gloo" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d" +dependencies = [ + "gloo-console", + "gloo-dialogs", + "gloo-events", + "gloo-file", + "gloo-history", + "gloo-net 0.3.1", + "gloo-render", + "gloo-storage", + "gloo-timers", + "gloo-utils 0.1.7", + "gloo-worker", +] + +[[package]] +name = "gloo-console" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-dialogs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-events" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-file" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7" +dependencies = [ + "gloo-events", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-history" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f" +dependencies = [ + "gloo-events", + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen", + "serde_urlencoded", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.1.7", + "http 0.2.12", + "js-sys", + "pin-project 1.1.5", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-net" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43aaa242d1239a8822c15c645f02166398da4f8b5c4bae795c1f5b44e9eee173" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils 0.2.0", + "http 0.2.12", + "js-sys", + "pin-project 1.1.5", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-render" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-storage" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480" +dependencies = [ + "gloo-utils 0.1.7", + "js-sys", + "serde", + "serde_json", + "thiserror", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-worker" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a" +dependencies = [ + "anymap2", + "bincode", + "gloo-console", + "gloo-utils 0.1.7", + "js-sys", + "serde", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "governor" version = "0.6.3" @@ -934,6 +1784,16 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -945,6 +1805,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] name = "hermit-abi" @@ -1155,6 +2019,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1195,6 +2065,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "internment" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8e537b529b8674e97e9fb82c10ff168a290ac3867a0295f112061ffbca1ef" +dependencies = [ + "hashbrown 0.14.5", + "parking_lot", +] + +[[package]] +name = "interprocess-docfix" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b84ee245c606aeb0841649a9288e3eae8c61b853a8cd5c0e14450e96d53d28f" +dependencies = [ + "blocking", + "cfg-if", + "futures-core", + "futures-io", + "intmap", + "libc", + "once_cell", + "rustc_version", + "spinning", + "thiserror", + "to_method", + "winapi", +] + +[[package]] +name = "intmap" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9" + [[package]] name = "ipnet" version = "2.9.0" @@ -1303,7 +2209,7 @@ checksum = "760dbe46660494d469023d661e8d268f413b2cb68c999975dcc237407096a693" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", "url", ] @@ -1329,11 +2235,41 @@ dependencies = [ "juniper", ] +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.5.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "krates" +version = "0.16.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcb3baf2360eb25ad31f0ada3add63927ada6db457791979b82ac199f835cb9" +dependencies = [ + "cargo-platform", + "cargo_metadata", + "cfg-expr", + "petgraph", + "semver", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + [[package]] name = "libc" -version = "0.2.154" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libmimalloc-sys" @@ -1361,6 +2297,21 @@ version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +[[package]] +name = "longest-increasing-subsequence" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bd0dd2cd90571056fdb71f6275fada10131182f84899f4b2a916e565d81d86" + +[[package]] +name = "lru" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "mac" version = "0.1.1" @@ -1387,6 +2338,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.2" @@ -1456,13 +2413,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[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-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", ] @@ -1522,6 +2488,27 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "parking_lot" version = "0.12.2" @@ -1561,6 +2548,16 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + [[package]] name = "phf" version = "0.10.1" @@ -1620,7 +2617,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -1678,7 +2675,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -1693,6 +2690,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.30" @@ -1733,11 +2741,21 @@ dependencies = [ "log", ] +[[package]] +name = "prettyplease" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" +dependencies = [ + "proc-macro2", + "syn 2.0.64", +] + [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" dependencies = [ "unicode-ident", ] @@ -1762,7 +2780,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -1969,9 +2987,24 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] [[package]] name = "rustls" @@ -1994,7 +3027,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.3", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] @@ -2045,9 +3078,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -2061,9 +3094,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.3" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -2072,9 +3105,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rusty-money" @@ -2108,9 +3141,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "schannel" @@ -2208,31 +3241,70 @@ dependencies = [ "smallvec", ] +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" +dependencies = [ + "futures-core", +] + [[package]] name = "serde" -version = "1.0.200" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" -version = "1.0.200" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2249,6 +3321,28 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431a35568651e363364210c91983c1da5eb29404d9f0928b67d4ebcfa7d330c" +dependencies = [ + "percent-encoding", + "serde", + "thiserror", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.64", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2261,6 +3355,58 @@ dependencies = [ "serde", ] +[[package]] +name = "server_fn" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "536a5b959673643ee01e59ae41bf01425482c8070dee95d7061ee2d45296b59c" +dependencies = [ + "bytes", + "const_format", + "dashmap", + "futures", + "gloo-net 0.5.0", + "http 1.1.0", + "js-sys", + "once_cell", + "send_wrapper", + "serde", + "serde_json", + "serde_qs", + "server_fn_macro_default", + "thiserror", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064dd9b256e78bf2886774f265cc34d2aefdd05b430c58c78a69eceef21b5e60" +dependencies = [ + "const_format", + "convert_case", + "proc-macro2", + "quote", + "syn 2.0.64", + "xxhash-rust", +] + +[[package]] +name = "server_fn_macro_default" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad11700cbccdbd313703916eb8c97301ee423c4a06e5421b77956fdcb36a9f" +dependencies = [ + "server_fn_macro", + "syn 2.0.64", +] + [[package]] name = "servo_arc" version = "0.3.0" @@ -2281,6 +3427,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -2317,6 +3472,37 @@ dependencies = [ "autocfg", ] +[[package]] +name = "sledgehammer_bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcfaf791ff02f48f3518ce825d32cf419c13a43c1d8b1232f74ac89f339c46d2" +dependencies = [ + "sledgehammer_bindgen_macro", + "wasm-bindgen", +] + +[[package]] +name = "sledgehammer_bindgen_macro" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd941cc539bd3dc694edaf9d0c4e1221d02baa67c6b45ec04fad1024d9e8139" +dependencies = [ + "quote", + "syn 2.0.64", +] + +[[package]] +name = "sledgehammer_utils" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f20798defa0e9d4eff9ca451c7f84774c7378a9c3b5a40112cfa2b3eadb97ae2" +dependencies = [ + "lru", + "once_cell", + "rustc-hash", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -2350,6 +3536,15 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "spinning" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b" +dependencies = [ + "lock_api", +] + [[package]] name = "spinning_top" version = "0.3.0" @@ -2425,9 +3620,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f" dependencies = [ "proc-macro2", "quote", @@ -2489,22 +3684,32 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -2553,6 +3758,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.37.0" @@ -2590,7 +3801,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2775,7 +3986,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -2785,6 +3996,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", ] [[package]] @@ -2892,6 +4140,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + [[package]] name = "unreachable" version = "1.0.0" @@ -2918,12 +4172,24 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" @@ -2972,7 +4238,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", "wasm-bindgen-shared", ] @@ -3006,7 +4272,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3046,6 +4312,15 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webui" +version = "0.1.0" +dependencies = [ + "dioxus", + "dioxus-logger", + "tracing", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3235,24 +4510,30 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xxhash-rust" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927da81e25be1e1a2901d59b81b37dd2efd1fc9c9345a55007f09bf5a2d3ee03" + [[package]] name = "zerocopy" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087eca3c1eaf8c47b94d02790dd086cd594b912d2043d4de4bfdd466b3befb7c" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.33" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4b6c273f496d8fd4eaf18853e6b448760225dc030ff2c485a786859aea6393" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] @@ -3272,7 +4553,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.64", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b951f0c..6794b6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,6 @@ +[workspace] +members = [".", "webui"] + [package] name = "ucsc_menu" version = "0.1.0" @@ -60,3 +63,6 @@ tokio-scoped = "0.2.0" [target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.mimalloc] version = "0.1" default-features = false + +[features] +dump-schema = [] diff --git a/src/cache/firestore.rs b/src/cache/firestore.rs new file mode 100644 index 0000000..0df97e4 --- /dev/null +++ b/src/cache/firestore.rs @@ -0,0 +1,128 @@ +use crate::parse::Locations; +use async_compression::tokio::bufread; +use chrono::{DateTime, Utc}; +use firestore::FirestoreDb; +use log::info; +use tokio::io::AsyncReadExt; + +use super::MenuCache; + +const CACHES_COLLECTION: &str = "caches"; + +#[derive(Debug)] +pub struct Firestore { + db: FirestoreDb, + // data: String, +} + +impl Firestore { + pub async fn open() -> crate::Result { + let db = FirestoreDb::new("ucsc-menu").await?; + Ok(Self { + db, + // data: String::new(), + }) + } + + pub async fn load(&mut self) -> crate::Result> { + if let Some(cache) = self + .db + .fluent() + .select() + .by_id_in(CACHES_COLLECTION) + .obj::() + .one("menu") + .await? + { + Ok(Some(cache.decompress().await)) + } else { + Ok(None) + } + // Ok(cache.decompress(&mut self.data).await) + } + + pub async fn save(&self, cache: &MenuCache) -> crate::Result<()> { + let compressed = GCloudMenuCache::compress(cache).await; + self.db + .fluent() + .update() + .in_col(CACHES_COLLECTION) + .document_id("menu") + .object(&compressed) + // need to specify type because of dependency_on_unit_never_type_fallback + .execute::<()>() + .await?; + Ok(()) + } +} + +#[derive(serde::Serialize, serde::Deserialize, Default)] +struct GCloudMenuCache { + cached_at: DateTime, + data: Vec, +} + +impl GCloudMenuCache { + async fn decompress<'a>(self /* dst: &mut String */) -> MenuCache { + if self.data.is_empty() { + return MenuCache { + cached_at: self.cached_at, + locations: Locations::default(), + }; + } + let mut decompress = bufread::GzipDecoder::new(self.data.as_slice()); + info!("Size of data compressed: {}", self.data.len()); + let mut dst = String::new(); + dst.reserve(self.data.len() * 8); + let _len = decompress + .read_to_string(&mut dst) + .await + .expect("should succeed"); + info!("Size of data uncompressed: {}", dst.len()); + let locations: Locations = + serde_json::from_str(&*dst).expect("Data parse should always be valid"); + MenuCache { + cached_at: self.cached_at, + locations, + } + } + + // TODO: better name + async fn compress(data: &MenuCache) -> Self { + let json = serde_json::to_string(data.locations()).unwrap(); + let mut compressed = Vec::with_capacity(json.len() / 4); + let mut compress = bufread::GzipEncoder::new(std::io::Cursor::new(json)); + compress + .read_buf(&mut compressed) + .await + .expect("This should succeed"); + Self { + cached_at: data.cached_at, + data: compressed, + } + } +} + +#[cfg(test)] +mod tests { + + use std::time::Instant; + + use super::*; + + #[tokio::test] + async fn test_open() { + pretty_env_logger::init(); + let mut db = Firestore::open().await.unwrap(); + let _mc = db.load().await.unwrap(); + } + + #[tokio::test] + async fn test_refresh() { + let mut db = Firestore::open().await.unwrap(); + let mut mc = db.load().await.unwrap(); + let start = Instant::now(); + todo!(); + println!("{:?}", start.elapsed()); + } +} diff --git a/src/cache/local.rs b/src/cache/local.rs new file mode 100644 index 0000000..dadb294 --- /dev/null +++ b/src/cache/local.rs @@ -0,0 +1,42 @@ +use std::path::{Path, PathBuf}; +use tokio::fs; + +use super::MenuCache; + +#[derive(Debug)] +pub struct FileStore(PathBuf); + +impl FileStore { + pub async fn open(p: impl AsRef) -> crate::Result { + let p = p.as_ref(); + // fs::File::options() + // .create(true) + // .write(true) + // .read(true) + // .open(p) + // .await?; + + Ok(Self(p.to_owned())) + } + + pub async fn load(&self) -> crate::Result> { + if fs::try_exists(&self.0).await? { + let f = fs::File::open(&self.0).await?; + let mut f = f.into_std().await; + serde_json::from_reader(&mut f).map_err(From::from) + } else { + Ok(None) + } + } + + pub async fn save(&self, value: &MenuCache) -> crate::Result<()> { + let f = fs::File::options() + .create(true) + .write(true) + .open(&self.0) + .await?; + // TODO + let mut f = f.into_std().await; + serde_json::to_writer_pretty(&mut f, value).map_err(From::from) + } +} diff --git a/src/cache/menu_cache.rs b/src/cache/menu_cache.rs deleted file mode 100644 index 3d3e223..0000000 --- a/src/cache/menu_cache.rs +++ /dev/null @@ -1,185 +0,0 @@ -use crate::{ - error::Error, - fetch::{date_iter, locations_page, make_client, menus_on_date}, - parse::Locations, - transpose::transposed, -}; -use chrono::{DateTime, Utc}; -use firestore::FirestoreDb; -use futures::{stream::FuturesUnordered, StreamExt}; -use log::info; -use tokio::io::AsyncReadExt; -const CACHES_COLLECTION: &str = "caches"; -#[derive(Debug)] -pub struct MenuCache<'a> { - cached_at: DateTime, - locations: Locations<'a>, -} -#[derive(serde::Serialize, serde::Deserialize, Default)] -struct GCloudMenuCache { - cached_at: DateTime, - data: Vec, -} - -pub static REFRESH_INTERVAL: chrono::Duration = chrono::Duration::minutes(15); - -impl<'a> MenuCache<'a> { - async fn from_async(cache: GCloudMenuCache) -> Self { - if cache.data.is_empty() { - return MenuCache { - cached_at: cache.cached_at, - locations: Locations::default(), - }; - } - let mut decompress = - async_compression::tokio::bufread::GzipDecoder::new(cache.data.as_slice()); - info!("Size of data compressed: {}", cache.data.len()); - let mut dst = String::with_capacity(cache.data.len() * 8); - let _len = decompress - .read_to_string(&mut dst) - .await - .expect("should succeed"); - info!("Size of data uncompressed: {}", dst.len()); - let locations: Locations = - serde_json::from_str(&dst).expect("Data parse should always be valid"); - MenuCache { - cached_at: cache.cached_at, - locations, - } - } -} - -impl<'a> Default for MenuCache<'a> { - fn default() -> Self { - Self { - cached_at: Utc::now(), - locations: Locations::default(), - } - } -} -impl<'a> MenuCache<'a> { - pub async fn open() -> Result { - let cache = Self::fetch_from_db().await?; - Ok(cache) - } - - pub async fn maybe_refresh(&mut self) -> Result { - if self.get_time_since_refresh() > chrono::Duration::minutes(15) { - self.refresh().await?; - Ok(true) - } else { - Ok(false) - } - } - - pub fn get_time_since_refresh(&self) -> chrono::Duration { - Utc::now().signed_duration_since(self.cached_at) - } - - pub fn get_time_until_refresh(&self) -> chrono::Duration { - REFRESH_INTERVAL - self.get_time_since_refresh() - } - - async fn fetch_from_db() -> Result { - let db = FirestoreDb::new("ucsc-menu").await?; - let cache: GCloudMenuCache = db - .fluent() - .select() - .by_id_in(CACHES_COLLECTION) - .obj() - .one("menu") - .await? - .unwrap_or_default(); // default is an empty cache - Ok(MenuCache::from_async(cache).await) - } - - async fn to_db_representation(&self) -> GCloudMenuCache { - let json = serde_json::to_string(self.locations()).unwrap(); - let mut compressed = Vec::with_capacity(json.len() / 4); - let mut compress = - async_compression::tokio::bufread::GzipEncoder::new(std::io::Cursor::new(json)); - compress - .read_buf(&mut compressed) - .await - .expect("This should succeed"); - GCloudMenuCache { - cached_at: self.cached_at, - data: compressed, - } - } - - async fn save_to_db(&self) -> Result<(), firestore::errors::FirestoreError> { - let cache: GCloudMenuCache = self.to_db_representation().await; - let db = FirestoreDb::new("ucsc-menu").await?; - db.fluent() - .update() - .in_col(CACHES_COLLECTION) - .document_id("menu") - .object(&cache) - .execute() - .await?; - Ok(()) - } - /// Returns whether or not it refreshed. Will return error if it fails - async fn refresh(&mut self) -> Result<(), crate::error::Error> { - let client = make_client(); - let locations_page = locations_page(&client).await?; - let mut locations = { - let parsed = scraper::Html::parse_document(&locations_page); - let locations: Locations = Locations::from_html_element(parsed.root_element())?; - locations - }; - { - let start_date = chrono::Utc::now().date_naive() - chrono::Duration::days(1); // subtract one day to make sure we try to get today's menu due to timezones - let week_menus: FuturesUnordered<_> = date_iter(start_date, 10) - .map(|x| menus_on_date(&client, &locations, Some(x))) - .collect(); - let week_menus: Vec<_> = week_menus.collect().await; - let valid_week_menus = week_menus.into_iter().filter_map(Result::ok).collect(); - - let valid_week_menus: Vec<_> = transposed(valid_week_menus) - .into_iter() - .map(|v| -> Vec<_> { - v.into_iter() - .map(|s| scraper::Html::parse_document(&s)) - .collect() - }) - .collect(); - let parsed_week_menus_iter = valid_week_menus.iter(); - for (location, htmls) in locations.iter_mut().zip(parsed_week_menus_iter) { - location.add_meals(htmls.iter())?; - } - self.locations = - serde_json::from_str(&serde_json::to_string(&locations).unwrap()).unwrap(); - }; - self.cached_at = Utc::now(); - self.save_to_db().await?; - Ok(()) - } - - pub const fn locations(&self) -> &Locations<'a> { - &self.locations - } -} - -#[cfg(test)] -mod tests { - - use std::time::Instant; - - use super::*; - - #[tokio::test] - async fn test_open() { - pretty_env_logger::init(); - let _mc = MenuCache::open().await.unwrap(); - } - - #[tokio::test] - async fn test_refresh() { - let mut mc = MenuCache::open().await.unwrap(); - let start = Instant::now(); - mc.refresh().await.unwrap(); - println!("{:?}", start.elapsed()); - } -} diff --git a/src/cache/mod.rs b/src/cache/mod.rs index 541dff8..eadd2c4 100644 --- a/src/cache/mod.rs +++ b/src/cache/mod.rs @@ -1,5 +1,125 @@ -mod menu_cache; +mod firestore; +mod local; mod multithreaded_cache; -pub use menu_cache::REFRESH_INTERVAL; +use std::path::Path; + +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; + +use crate::{fetch, parse::Locations}; + +use firestore::Firestore; +use local::FileStore; pub use multithreaded_cache::MultithreadedCache as Multithreaded; + +pub const REFRESH_INTERVAL: Duration = Duration::minutes(15); + +#[derive(Debug, Serialize, Deserialize)] +pub struct MenuCache { + cached_at: DateTime, + locations: Locations, +} + +impl Default for MenuCache { + fn default() -> Self { + Self { + cached_at: Utc::now(), + locations: Locations::default(), + } + } +} + +impl MenuCache { + #[inline] + #[must_use] + pub fn time_since_refresh(&self) -> chrono::Duration { + Utc::now().signed_duration_since(self.cached_at) + } + + #[inline] + #[must_use] + pub fn time_until_refresh(&self) -> chrono::Duration { + REFRESH_INTERVAL - self.time_since_refresh() + } + + #[inline] + #[must_use] + pub fn needs_refresh(&self) -> bool { + self.time_since_refresh() > REFRESH_INTERVAL + } + + #[inline] + #[must_use] + pub const fn locations(&self) -> &Locations { + &self.locations + } + + pub async fn load() -> crate::Result { + let client = fetch::make_client(); + let locations = Locations::load(&client).await?; + Ok(Self { + locations, + cached_at: Utc::now(), + }) + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum Store { + Cloud(Firestore), + Local(FileStore), + AdHoc, +} + +impl Store { + #[inline] + pub async fn cloud() -> crate::Result { + Firestore::open().await.map(Self::Cloud) + } + + #[inline] + pub async fn local(p: impl AsRef) -> crate::Result { + FileStore::open(p).await.map(Self::Local) + } + + pub async fn load(&mut self) -> crate::Result { + let value = match self { + Self::Cloud(fs) => fs.load().await?, + Self::Local(f) => f.load().await?, + Self::AdHoc => None, + }; + + match value { + Some(v) => Ok(v), + None => { + let v = MenuCache::load().await?; + self.save(&v).await?; + Ok(v) + } + } + } + + pub async fn save(&mut self, data: &MenuCache) -> crate::Result<()> { + match self { + Self::Cloud(fs) => fs.save(data).await, + Self::Local(f) => f.save(data).await, + Self::AdHoc => Ok(()), + } + } + + pub async fn refresh(&mut self, data: &mut MenuCache) -> crate::Result<()> { + *data = MenuCache::load().await?; + self.save(data).await + } + + pub async fn maybe_refresh(&mut self, data: &mut MenuCache) -> crate::Result { + if data.needs_refresh() { + self.refresh(data).await?; + Ok(true) + } else { + Ok(false) + } + } +} diff --git a/src/cache/multithreaded_cache.rs b/src/cache/multithreaded_cache.rs index aaba339..53e8b75 100644 --- a/src/cache/multithreaded_cache.rs +++ b/src/cache/multithreaded_cache.rs @@ -1,36 +1,54 @@ -use super::menu_cache::MenuCache; +use super::{MenuCache, Store}; use crate::error::Error; use std::ops::Deref; use futures_locks::RwLock; #[derive(Debug)] -pub struct MultithreadedCache<'a>(RwLock>); +pub struct MultithreadedCache(RwLock); -impl<'a> MultithreadedCache<'a> { - pub async fn new() -> Result { - let menu = MenuCache::open().await?; +#[derive(Debug)] +pub struct MultithreadedInner { + store: Store, + data: MenuCache, +} - Ok(Self(RwLock::new(menu))) +impl MultithreadedCache { + #[must_use] + pub async fn new(mut store: Store) -> crate::Result { + let data = store.load().await?; + Ok(Self(RwLock::new(MultithreadedInner { store, data }))) } pub async fn refresh(&self) -> Result { - // spawn local thread to do the refreshing - let mut new_menu = MenuCache::open().await?; - let refreshed = new_menu.maybe_refresh().await?; - if refreshed { - let mut guard = self.0.write().await; - *guard = new_menu; + let needs_refresh = self.0.read().await.data.needs_refresh(); + + if needs_refresh { + // don't lock until we want to actually write to the db + // that is, don't use Store::refresh + let new_data = MenuCache::load().await?; + let mut grd = self.0.write().await; + grd.store.save(&new_data).await?; + grd.data = new_data; } - Ok(refreshed) + Ok(needs_refresh) + } + + pub async fn get(&self) -> impl Deref + '_ { + DataGuard(self.0.read().await) } +} + +#[derive(Debug)] +struct DataGuard(futures_locks::RwLockReadGuard); + +impl Deref for DataGuard { + type Target = MenuCache; - pub async fn get<'b>(&'b self) -> impl Deref> + 'b - where - 'a: 'b, - { - self.0.read().await + #[inline] + fn deref(&self) -> &Self::Target { + &self.0.data } } @@ -41,7 +59,7 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_refresh() { - let menu = MultithreadedCache::new().await.unwrap(); + let menu = MultithreadedCache::new(Store::AdHoc).await.unwrap(); menu.refresh().await.unwrap(); // try having multiple threads read from menu at the same time // using the get() function diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/error.rs b/src/error.rs index 621e9ee..27faf35 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,12 +3,15 @@ use firestore::errors::FirestoreError; use crate::parse; use std::fmt::{self, Display, Formatter}; +pub type Result = core::result::Result; + #[derive(Debug)] pub enum Error { Parse(parse::Error), Request(reqwest::Error), Database(FirestoreError), Json(serde_json::Error), + Io(std::io::Error), } impl From for Error { @@ -35,6 +38,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: std::io::Error) -> Self { + Self::Io(value) + } +} + impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { @@ -42,6 +51,9 @@ impl Display for Error { Self::Request(e) => write!(f, "Request error: {e}"), Self::Database(e) => write!(f, "Database error: {e}"), Self::Json(e) => write!(f, "Json error: {e}"), + Self::Io(e) => write!(f, "IO error: {e}"), } } } + +impl std::error::Error for Error {} diff --git a/src/fetch/mod.rs b/src/fetch/mod.rs index 9006a57..d0d571e 100644 --- a/src/fetch/mod.rs +++ b/src/fetch/mod.rs @@ -70,7 +70,7 @@ pub async fn fetch_location_page( pub async fn menus_on_date( client: &reqwest::Client, - locations: &Locations<'_>, + locations: &Locations, date: Option, ) -> Result, RequestError> { futures::future::try_join_all( diff --git a/src/main.rs b/src/main.rs index 4a0aa89..522b329 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use axum_server as _; // to use rustls over openssl bc alpine linux mod cache; +mod config; mod error; mod fetch; mod parse; @@ -21,36 +22,41 @@ use std::{ use axum::{ body::Body, + extract::State, http::Method, response::Response, routing::{get, on, MethodFilter}, Extension, Router, }; -use crate::{cache::Multithreaded, fetch::make_client}; +use crate::{ + cache::{Multithreaded, Store}, + fetch::make_client, +}; use juniper::{graphql_object, EmptyMutation, EmptySubscription, RootNode}; use juniper_axum::{graphiql, graphql, playground, ws}; use juniper_graphql_ws::ConnectionConfig; use parse::Locations; -use tokio::{net::TcpListener, sync::OnceCell, time::sleep}; +use tokio::{net::TcpListener, time::sleep}; use tower_http::cors::CorsLayer; use tower_http::{compression::CompressionLayer, cors::Any}; -#[derive(Clone, Copy, Debug)] -pub struct Query; +pub use error::Result; + +#[derive(Clone, Debug)] +pub struct Query(Arc); -static CACHE: OnceCell> = OnceCell::const_new(); #[graphql_object] impl Query { /// Adds two `a` and `b` numbers. - async fn query(&self) -> Locations<'static> { - let c = CACHE.get_or_init(|| async { Multithreaded::new().await.unwrap() }); - c.await.get().await.locations().to_owned() + async fn query(&self) -> Locations { + self.0.get().await.locations().to_owned() } #[graphql(ignore)] pub async fn refresh(self) { - let c = CACHE.get_or_init(|| async { Multithreaded::new().await.unwrap() }); - let _ = c.await.refresh().await; + if let Err(e) = self.0.refresh().await { + tracing::warn!("Error while refreshing cache: {e:?}"); + } } } @@ -65,31 +71,43 @@ type Schema = RootNode<'static, Query, EmptyMutation, EmptySubscription>; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; -async fn refresh<'a>() -> Response { - let cache = CACHE - .get_or_init(|| async { Multithreaded::new().await.unwrap() }) - .await; +async fn refresh(State(cache): State>) -> Response { let _res = cache.refresh().await; let c = cache.get().await; Response::builder() .status(201) .body(Body::from(format!( "Last refresh: {}\nNext refresh: {}", - c.get_time_since_refresh(), - c.get_time_until_refresh(), + c.time_since_refresh(), + c.time_until_refresh(), ))) .unwrap() } +#[cfg(not(feature = "dump-schema"))] #[tokio::main(flavor = "current_thread")] -async fn main() { - CACHE - .get_or_init(|| async { Multithreaded::new().await.unwrap() }) - .await; +async fn main() -> core::result::Result<(), Box> { + pretty_env_logger::init(); + let store = match env::var("CACHE").as_deref() { + Ok(":firestore:") => Store::cloud().await?, + Ok(":memory:") => Store::AdHoc, + Ok(p) => Store::local(p).await?, + Err(_) => { + log::warn!("env var CACHE not set, using ad-hoc memory cache."); + Store::AdHoc + } + }; + println!("{store:?}"); + let cache = Arc::new(Multithreaded::new(store).await?); + println!("{cache:?}"); let host = env::var("HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string()); let addr = SocketAddr::from_str(format!("{host}:{port}").as_str()).unwrap(); - let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + let schema = Schema::new( + Query(Arc::clone(&cache)), + EmptyMutation::new(), + EmptySubscription::new(), + ); let comression_layer: CompressionLayer = CompressionLayer::new() .br(true) .deflate(true) @@ -98,7 +116,6 @@ async fn main() { let cors_layer = CorsLayer::new() .allow_methods([Method::GET, Method::POST]) // intentionally excludes request-refresh/PUT .allow_origin(Any); - pretty_env_logger::init(); let app = Router::new() .route( @@ -112,9 +129,10 @@ async fn main() { "/subscriptions", get(ws::>(ConnectionConfig::new(()))), ) - .route("/request-refresh", on(MethodFilter::PUT, refresh)) .route("/graphiql", get(graphiql("/graphql", "/subscriptions"))) .route("/playground", get(playground("/graphql", "/subscriptions"))) + .route("/request-refresh", on(MethodFilter::PUT, refresh)) + .with_state(Arc::clone(&cache)) .layer(cors_layer) .layer(Extension(Arc::new(schema))) .layer(comression_layer); @@ -139,7 +157,13 @@ async fn main() { .await .unwrap_or_else(|e| panic!("failed to listen on {addr}: {e}")); log::info!("listening on http://{addr}"); - axum::serve(listener, app) - .await - .unwrap_or_else(|e| panic!("failed to run `axum::serve`: {e}")); + axum::serve(listener, app).await?; + Ok(()) +} + +#[cfg(feature = "dump-schema")] +fn main() { + let schema = Schema::new(Query, EmptyMutation::new(), EmptySubscription::new()); + std::fs::write("ucsc_menu.graphql", schema.as_sdl().as_bytes()) + .expect("error writing schema to file"); } diff --git a/src/parse/location_page/location_data.rs b/src/parse/location_page/location_data.rs index baa6038..67bcde5 100644 --- a/src/parse/location_page/location_data.rs +++ b/src/parse/location_page/location_data.rs @@ -7,12 +7,12 @@ use crate::parse::Error; pub const NUM_MEALS: usize = 10; #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)] -pub struct LocationData<'a> { - menus: [Option>; NUM_MEALS], // keep track of up to 10 days of meals +pub struct LocationData { + menus: [Option; NUM_MEALS], // keep track of up to 10 days of meals } -const ARRAY_REPEAT_VALUE: std::option::Option> = None; -impl<'a> LocationData<'a> { +const ARRAY_REPEAT_VALUE: std::option::Option = None; +impl LocationData { pub const fn new() -> Self { Self { menus: [ARRAY_REPEAT_VALUE; NUM_MEALS], @@ -28,11 +28,11 @@ impl<'a> LocationData<'a> { } #[cfg(test)] - pub fn menus_mut(&mut self) -> impl Iterator> { + pub fn menus_mut(&mut self) -> impl Iterator { self.menus.iter_mut().filter_map(|x| x.as_mut()) } - pub fn menus(&self) -> impl Iterator> { + pub fn menus(&self) -> impl Iterator { self.menus.iter().filter_map(|x| x.as_ref()) } @@ -47,7 +47,7 @@ impl<'a> LocationData<'a> { } } - pub fn add_meal(&mut self, html: &'a Html) -> Result<()> { + pub fn add_meal(&mut self, html: &Html) -> Result<()> { let menu = DailyMenu::from_html_element(html.root_element())?; self.menus diff --git a/src/parse/location_page/locations.rs b/src/parse/location_page/locations.rs index e08bf4e..ca95684 100644 --- a/src/parse/location_page/locations.rs +++ b/src/parse/location_page/locations.rs @@ -1,10 +1,14 @@ use std::slice::{Iter, IterMut}; use chrono::NaiveDate; +use futures::stream::FuturesUnordered; +use futures::StreamExt as _; use juniper::{graphql_object, GraphQLInputObject}; use scraper::Html; +use crate::fetch; use crate::parse::menu_page::DailyMenu; +use crate::transpose::transposed; use crate::{parse::Error, static_selector}; use super::location_meta::LocationMeta; @@ -12,7 +16,7 @@ use super::location_meta::LocationMeta; use super::location_data::LocationData; #[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)] -pub struct Location<'a>(LocationData<'a>, LocationMeta); +pub struct Location(LocationData, LocationMeta); #[derive(GraphQLInputObject, Debug)] pub struct DateRange { @@ -21,7 +25,7 @@ pub struct DateRange { } #[graphql_object] -impl<'a> Location<'a> { +impl Location { pub fn id(&self) -> &str { self.1.id() } @@ -29,7 +33,7 @@ impl<'a> Location<'a> { self.1.name() } #[allow(clippy::needless_pass_by_value)] // ignored because graphql doesn't support pass by reference - pub fn menus(&self, date_range: Option) -> Vec<&DailyMenu<'a>> { + pub fn menus(&self, date_range: Option) -> Vec<&DailyMenu> { if let Some(DateRange { start, end }) = date_range { self.0 .menus() @@ -46,15 +50,12 @@ impl<'a> Location<'a> { } } -impl<'a> Location<'a> { +impl Location { pub fn new(location_meta: LocationMeta) -> Self { Self(LocationData::new(), location_meta) } - pub fn add_meals<'b: 'a>( - &mut self, - htmls: impl Iterator, - ) -> Result<(), Error> { + pub fn add_meals<'a>(&mut self, htmls: impl Iterator) -> Result<(), Error> { // TODO: instead of immediately clearing, diff the similar meals first self.clear(); for html in htmls { @@ -76,14 +77,14 @@ impl<'a> Location<'a> { } } #[derive(Debug, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq, Clone)] -pub struct Locations<'a> { - locations: Vec>, +pub struct Locations { + locations: Vec, } #[graphql_object] -impl<'a> Locations<'a> { +impl Locations { #[allow(clippy::needless_pass_by_value)] // ignored because graphql doesn't support pass by reference - pub fn locations(&self, ids: Option>) -> Vec<&Location<'a>> { + pub fn locations(&self, ids: Option>) -> Vec<&Location> { ids.map_or_else( || self.locations.iter().collect(), |ids| { @@ -97,7 +98,7 @@ impl<'a> Locations<'a> { } } -impl<'a> Locations<'a> { +impl Locations { pub fn from_html_element(element: scraper::ElementRef) -> Result { static_selector!(LOCATION_CHOICES_SELECTOR <- "div#locationchoices"); static_selector!(LOCATION_SELECTOR <- "li.locations"); @@ -118,11 +119,11 @@ impl<'a> Locations<'a> { Ok(Self { locations }) } - pub fn iter_mut(&mut self) -> IterMut> { + pub fn iter_mut(&mut self) -> IterMut { self.locations.iter_mut() } - pub fn iter(&self) -> Iter> { + pub fn iter(&self) -> Iter { self.locations.iter() } // might eventually be used for diffing @@ -147,6 +148,38 @@ impl<'a> Locations<'a> { Ok(()) } + + pub async fn load(client: &reqwest::Client) -> crate::Result { + let locations_page = fetch::locations_page(&client).await?; + let mut locations = { + let parsed = scraper::Html::parse_document(&locations_page); + let locations: Locations = Locations::from_html_element(parsed.root_element())?; + locations + }; + let start_date = chrono::Utc::now().date_naive() - chrono::Duration::days(1); // subtract one day to make sure we try to get today's menu due to timezones + let week_menus: FuturesUnordered<_> = fetch::date_iter(start_date, 10) + .map(|x| fetch::menus_on_date(&client, &locations, Some(x))) + .collect(); + let week_menus: Vec<_> = week_menus.collect().await; + let valid_week_menus = week_menus.into_iter().filter_map(Result::ok).collect(); + + let valid_week_menus: Vec<_> = transposed(valid_week_menus) + .into_iter() + .map(|v| -> Vec<_> { + v.into_iter() + .map(|s| scraper::Html::parse_document(&s)) + .collect() + }) + .collect(); + let parsed_week_menus_iter = valid_week_menus.iter(); + for (location, htmls) in locations.iter_mut().zip(parsed_week_menus_iter) { + location.add_meals(htmls.iter())?; + } + + Ok(serde_json::from_str( + &serde_json::to_string(&locations).unwrap(), + )?) + } } #[cfg(test)] @@ -194,7 +227,7 @@ mod tests { name # If you change the start or end date then the query will return an empty array # you can also set either start or end to null or simply omit them to not filter by that constraint - menus(dateRange: {start: "2024-04-05", end: "2024-04-05"}) { + menus(dateRange: {start: "2024-04-05", end: "2024-04-05"}) { date meals { mealType diff --git a/src/parse/menu_page/daily_menu.rs b/src/parse/menu_page/daily_menu.rs index 1f3b1bd..515069c 100644 --- a/src/parse/menu_page/daily_menu.rs +++ b/src/parse/menu_page/daily_menu.rs @@ -7,19 +7,19 @@ use crate::parse::Error; use crate::static_selector; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct DailyMenu<'a> { +pub struct DailyMenu { // graphql representation: yyyy-MM-dd date: NaiveDate, - meals: Vec>, + meals: Vec, } #[graphql_object] -impl<'a> DailyMenu<'a> { +impl DailyMenu { pub const fn date(&self) -> NaiveDate { self.date } - pub fn meals(&self, meal_type: Option) -> Vec> { + pub fn meals(&self, meal_type: Option) -> Vec { meal_type.map_or_else( || self.meals.clone(), |meal_type| { @@ -33,28 +33,28 @@ impl<'a> DailyMenu<'a> { } } -impl PartialEq for DailyMenu<'_> { +impl PartialEq for DailyMenu { fn eq(&self, other: &Self) -> bool { self.date == other.date } } -impl PartialOrd for DailyMenu<'_> { +impl PartialOrd for DailyMenu { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Eq for DailyMenu<'_> {} +impl Eq for DailyMenu {} -impl Ord for DailyMenu<'_> { +impl Ord for DailyMenu { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.date.cmp(&other.date) } } -impl<'a> DailyMenu<'a> { - pub fn from_html_element(element: scraper::ElementRef<'a>) -> Result { +impl DailyMenu { + pub fn from_html_element(element: scraper::ElementRef<'_>) -> Result { static_selector!(DATE_SELECTOR <- "input[name=strCurSearchDays]"); static_selector!(MEAL_SELECTOR <- r##"table[bordercolor="#CCC"] table[bordercolor="#FFFF00"]"##); let date_str = element diff --git a/src/parse/menu_page/food_item.rs b/src/parse/menu_page/food_item.rs index a3cb877..946ea25 100644 --- a/src/parse/menu_page/food_item.rs +++ b/src/parse/menu_page/food_item.rs @@ -1,5 +1,3 @@ -use std::borrow::Cow; - use super::allergens::{AllergenFlags, AllergenInfo, Allergens}; use super::money::Usd; use crate::parse::text_from_selection::{get_inner_text, text_from_selection}; @@ -8,30 +6,30 @@ use crate::static_selector; use juniper::graphql_object; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct FoodItem<'a> { - name: Cow<'a, str>, +pub struct FoodItem { + name: String, allergen_info: AllergenInfo, #[serde(skip_serializing, skip_deserializing)] - price: Option>, // in cents + price: Option, // in cents } -impl PartialEq for FoodItem<'_> { +impl PartialEq for FoodItem { fn eq(&self, other: &Self) -> bool { // we ignore meal_type, category and price intentionally in checking equality self.name == other.name && self.allergen_info == other.allergen_info } } -impl Eq for FoodItem<'_> {} +impl Eq for FoodItem {} -impl<'a> FoodItem<'a> { - pub fn from_html_element(element: scraper::ElementRef<'a>) -> Result { +impl FoodItem { + pub fn from_html_element(element: scraper::ElementRef<'_>) -> Result { // example html tr element at ./html_examples/food_item.html // get name with css selector .shortmenurecipes > span static_selector!(NAME_SELECTOR <- ".shortmenurecipes > span"); let name = text_from_selection(&NAME_SELECTOR, element, "foodItem", "name")?.trim_end(); - let name = remove_excess_whitespace(name); + let name = remove_excess_whitespace(name).into_owned(); // get allergen info with css selector td > img static_selector!(ALLERGEN_INFO_SELECTOR <- "td > img"); let allergen_info = @@ -74,7 +72,7 @@ impl From> for AllergenFlags { } #[graphql_object] -impl<'a> FoodItem<'a> { +impl FoodItem { pub fn allergens(&self) -> Vec { self.allergen_info.into() } diff --git a/src/parse/menu_page/meal.rs b/src/parse/menu_page/meal.rs index d8c2ac9..93d0593 100644 --- a/src/parse/menu_page/meal.rs +++ b/src/parse/menu_page/meal.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, iter::Peekable, vec}; +use std::{iter::Peekable, vec}; use juniper::{graphql_object, GraphQLEnum, GraphQLObject}; use regex::RegexBuilder; @@ -27,13 +27,13 @@ pub enum Type { BananaJoes, // Late Night @ Banana Joes - only for crown } #[derive(Debug, GraphQLObject, Clone, serde::Serialize, serde::Deserialize)] -pub struct Meal<'a> { +pub struct Meal { pub meal_type: Type, - pub sections: Vec>, + pub sections: Vec
, } -impl<'a> Meal<'a> { - pub fn from_html_element(element: scraper::ElementRef<'a>) -> Result { +impl Meal { + pub fn from_html_element(element: scraper::ElementRef<'_>) -> Result { // example html div element at ./html_examples/meal.html static_selector!(ROW_SELECTOR <- r##"table[bordercolor="#FFFF00"] > tbody > tr"##); let mut top_level_row_iter = element.select(&ROW_SELECTOR); @@ -92,7 +92,7 @@ impl<'a> SectionIterator<'a> { } impl<'a> Iterator for SectionIterator<'a> { - type Item = Result, Error>; + type Item = Result; fn next(&mut self) -> Option { let elements = &mut self.elements; @@ -104,13 +104,13 @@ impl<'a> Iterator for SectionIterator<'a> { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct Section<'a> { - pub name: Cow<'a, str>, - pub food_items: Vec>, +pub struct Section { + pub name: String, + pub food_items: Vec, } #[graphql_object] -impl<'a> Section<'a> { +impl Section { pub fn name(&self) -> &str { &self.name } @@ -121,14 +121,14 @@ impl<'a> Section<'a> { excludes_all_allergens: Option>, contains_any_allergens: Option>, name_contains: Option, - ) -> Vec> { + ) -> Vec { let contains_all_mask: Option = contains_all_allergens.map(std::convert::Into::into); let excludes_all_mask: Option = excludes_all_allergens.map(std::convert::Into::into); let contains_any_mask: Option = contains_any_allergens.map(std::convert::Into::into); - let allergen_filter = |food_item: &&FoodItem<'a>| { + let allergen_filter = |food_item: &&FoodItem| { let mask = food_item.get_allergen_mask(); let mut out = true; out &= contains_all_mask.map_or(true, |contains_all| mask.contains(contains_all)); @@ -157,9 +157,9 @@ impl<'a> Section<'a> { } } -impl<'a> Section<'a> { +impl Section { // takes in an iterator of tr elements of a specific meal and consumes the elements to create a MealSection - pub fn from_html_elements(elements: &mut Peekable>) -> Result { + pub fn from_html_elements(elements: &mut Peekable>) -> Result { static_selector!(SECTION_NAME_SELECTOR <- ".shortmenucats > span"); // if the first element does not match the section name selector, then return an error @@ -171,7 +171,7 @@ impl<'a> Section<'a> { // trim off first and last three characters since the name looks like -- name -- let name = &name[3..name.len() - 3]; - let name = remove_excess_whitespace(name); + let name = remove_excess_whitespace(name).into_owned(); // iterate through by peeking and calling handle_element let mut food_items = vec![]; @@ -188,7 +188,7 @@ impl<'a> Section<'a> { Ok(Section { name, food_items }) } - fn handle_element(element: scraper::ElementRef<'a>) -> Result>, Error> { + fn handle_element(element: scraper::ElementRef<'_>) -> Result, Error> { static_selector!(SECTION_NAME_SELECTOR <- ".shortmenucats > span"); if element.select(&SECTION_NAME_SELECTOR).next().is_some() { Ok(None) diff --git a/src/parse/menu_page/money.rs b/src/parse/menu_page/money.rs index 830b333..f4f835a 100644 --- a/src/parse/menu_page/money.rs +++ b/src/parse/menu_page/money.rs @@ -1,9 +1,9 @@ use std::fmt::Display; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Usd<'a>(rusty_money::Money<'a, rusty_money::iso::Currency>); +pub struct Usd(rusty_money::Money<'static, rusty_money::iso::Currency>); -impl Usd<'_> { +impl Usd { pub fn from_str(s: &str) -> Result { Ok(Self(rusty_money::Money::from_str( s, @@ -12,19 +12,19 @@ impl Usd<'_> { } } -impl Display for Usd<'_> { +impl Display for Usd { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } -impl serde::Serialize for Usd<'_> { +impl serde::Serialize for Usd { fn serialize(&self, serializer: S) -> Result { self.to_string().serialize(serializer) } } -impl<'de> serde::Deserialize<'de> for Usd<'_> { +impl<'de> serde::Deserialize<'de> for Usd { fn deserialize>(deserializer: D) -> Result { let s = String::deserialize(deserializer)?; // remove quotes diff --git a/ucsc_menu.graphql b/ucsc_menu.graphql new file mode 100644 index 0000000..16e6cc2 --- /dev/null +++ b/ucsc_menu.graphql @@ -0,0 +1,88 @@ +schema { + query: Query +} + +enum Allergens { + EGG + FISH + GLUTEN_FRIENDLY + MILK + PEANUT + SOY + TREE_NUT + ALCOHOL + VEGAN + VEGETARIAN + PORK + BEEF + HALAL + SHELLFISH + SESAME +} + +enum Type { + BREAKFAST + LUNCH + DINNER + LATE_NIGHT + MENU + UNKNOWN + ALL_DAY + BANANA_JOES +} + +input DateRange { + start: Date + end: Date +} + +""" + Date in the proleptic Gregorian calendar (without time zone). + + Represents a description of the date (as used for birthdays, for example). + It cannot represent an instant on the time-line. + + [`Date` scalar][1] compliant. + + See also [`chrono::NaiveDate`][2] for details. + + [1]: https://graphql-scalars.dev/docs/scalars/date + [2]: https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html +""" +scalar Date + +type DailyMenu { + date: Date! + meals(mealType: Type): [Meal!]! +} + +type FoodItem { + allergens: [Allergens!]! + name: String! + price: String +} + +type Location { + id: String! + name: String! + menus(dateRange: DateRange): [DailyMenu!]! +} + +type Locations { + locations(ids: [String!]): [Location!]! +} + +type Meal { + mealType: Type! + sections: [Section!]! +} + +type Query { + "Adds two `a` and `b` numbers." + query: Locations! +} + +type Section { + name: String! + foodItems(containsAllAllergens: [Allergens!], excludesAllAllergens: [Allergens!], containsAnyAllergens: [Allergens!], nameContains: String): [FoodItem!]! +} diff --git a/webui/.gitignore b/webui/.gitignore new file mode 100644 index 0000000..884ebca --- /dev/null +++ b/webui/.gitignore @@ -0,0 +1,9 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/dist/ +/static/ +/.dioxus/ + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/webui/Cargo.toml b/webui/Cargo.toml new file mode 100644 index 0000000..00208d6 --- /dev/null +++ b/webui/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "webui" +version = "0.1.0" +authors = ["m "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] + +dioxus = { version = "0.5", features = ["web", "router"] } + +# Debug +tracing = "0.1.40" +dioxus-logger = "0.5.0" diff --git a/webui/Dioxus.toml b/webui/Dioxus.toml new file mode 100644 index 0000000..1c39178 --- /dev/null +++ b/webui/Dioxus.toml @@ -0,0 +1,43 @@ +[application] + +# App (Project) Name +name = "webui" + +# Dioxus App Default Platform +# desktop, web +default_platform = "web" + +# `build` & `serve` dist path +out_dir = "dist" + +# resource (assets) file folder +asset_dir = "assets" + +[web.app] + +# HTML title tag content +title = "webui" + +[web.watcher] + +# when watcher trigger, regenerate the `index.html` +reload_html = true + +# which files or dirs will be watcher monitoring +watch_path = ["src", "assets"] + +# include `assets` in web platform +[web.resource] + +# CSS style file + +style = ["/tailwind.css"] + +# Javascript code file +script = [] + +[web.resource.dev] + +# Javascript code file +# serve: [dev-server] only +script = [] diff --git a/webui/README.md b/webui/README.md new file mode 100644 index 0000000..0458151 --- /dev/null +++ b/webui/README.md @@ -0,0 +1,17 @@ +# Development + +1. Install npm: https://docs.npmjs.com/downloading-and-installing-node-js-and-npm +2. Install the tailwind css cli: https://tailwindcss.com/docs/installation +3. Run the following command in the root of the project to start the tailwind CSS compiler: + +```bash +npx tailwindcss -i ./input.css -o ./assets/tailwind.css --watch +``` + +Run the following command in the root of the project to start the Dioxus dev server: + +```bash +dx serve --hot-reload +``` + +- Open the browser to http://localhost:8080 \ No newline at end of file diff --git a/webui/assets/favicon.ico b/webui/assets/favicon.ico new file mode 100644 index 0000000..eed0c09 Binary files /dev/null and b/webui/assets/favicon.ico differ diff --git a/webui/assets/header.svg b/webui/assets/header.svg new file mode 100644 index 0000000..59c96f2 --- /dev/null +++ b/webui/assets/header.svg @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/webui/assets/main.css b/webui/assets/main.css new file mode 100644 index 0000000..affbeb0 --- /dev/null +++ b/webui/assets/main.css @@ -0,0 +1,40 @@ +body { + background-color: #111216; +} + +#main { + margin: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif +} + +#links { + width: 400px; + text-align: left; + font-size: x-large; + color: white; + display: flex; + flex-direction: column; +} + +#links a { + color: white; + text-decoration: none; + margin-top: 20px; + margin: 10px; + border: white 1px solid; + border-radius: 5px; + padding: 10px; +} + +#links a:hover { + background-color: #1f1f1f; + cursor: pointer; +} + +#header { + max-width: 1200px; +} diff --git a/webui/input.css b/webui/input.css new file mode 100644 index 0000000..bd6213e --- /dev/null +++ b/webui/input.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; \ No newline at end of file diff --git a/webui/src/main.rs b/webui/src/main.rs new file mode 100644 index 0000000..1cb0c03 --- /dev/null +++ b/webui/src/main.rs @@ -0,0 +1,51 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; +use tracing::Level; + +#[derive(Clone, Routable, Debug, PartialEq)] +enum Route { + #[route("/")] + Home {}, + #[route("/blog/:id")] + Blog { id: i32 }, +} + +fn main() { + // Init logger + dioxus_logger::init(Level::INFO).expect("failed to init logger"); + launch(App); +} + +fn App() -> Element { + rsx! { + Router:: {} + } +} + +#[component] +fn Blog(id: i32) -> Element { + rsx! { + Link { to: Route::Home {}, "Go to counter" } + "Blog post {id}" + } +} + +#[component] +fn Home() -> Element { + let mut count = use_signal(|| 0); + + rsx! { + Link { + to: Route::Blog { + id: count() + }, + "Go to blog" + } + div { + h1 { "High-Five counter: {count}" } + button { onclick: move |_| count += 1, "Up high!" } + button { onclick: move |_| count -= 1, "Down low!" } + } + } +} diff --git a/webui/tailwind.config.js b/webui/tailwind.config.js new file mode 100644 index 0000000..2a69d58 --- /dev/null +++ b/webui/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + mode: "all", + content: ["./src/**/*.{rs,html,css}", "./dist/**/*.html"], + theme: { + extend: {}, + }, + plugins: [], +};