diff --git a/.github/workflows/rust.yaml b/.github/workflows/rust.yaml new file mode 100644 index 0000000..a337eed --- /dev/null +++ b/.github/workflows/rust.yaml @@ -0,0 +1,48 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + CARGO_NET_RETRY: 10 + RUST_BACKTRACE: 1 + RUSTUP_MAX_RETRIES: 10 + RUSTFLAGS: "-D warnings" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +jobs: + tests: + name: Rust + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - uses: taiki-e/install-action@v2 + with: + tool: just@1.43.1 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.90.0 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: "v1" + cache-workspace-crates: true + cache-on-failure: true + workspaces: | + . + - name: Compile + run: just cargo-compile + - name: Clippy + run: just cargo-clippy-check + - name: Rustfmt + run: just cargo-fmt-check diff --git a/Cargo.lock b/Cargo.lock index 4ff3cef..684e0eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,41 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" [[package]] name = "askama" @@ -80,17 +50,11 @@ dependencies = [ "winnow", ] -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "axum" -version = "0.8.4" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" dependencies = [ "axum-core", "bytes", @@ -107,8 +71,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", @@ -122,9 +85,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" dependencies = [ "bytes", "futures-core", @@ -133,28 +96,12 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", "tracing", ] -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - [[package]] name = "basic-toml" version = "0.1.10" @@ -170,54 +117,18 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "cc" -version = "1.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" -dependencies = [ - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" -[[package]] -name = "chrono" -version = "0.4.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - [[package]] name = "fnv" version = "1.0.7" @@ -278,12 +189,6 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "http" version = "1.3.1" @@ -366,45 +271,51 @@ dependencies = [ ] [[package]] -name = "iana-time-zone" -version = "0.1.63" +name = "itoa" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jiff" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89a5b5e10d5a9ad6e5d1f4bd58225f655d6fe9767575a5e8ac5a6fe64e04495" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", + "jiff-static", + "jiff-tzdb-platform", "log", - "wasm-bindgen", - "windows-core", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.59.0", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "jiff-static" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "ff7a39c8862fc1369215ccf0a8f12dd4598c7f6484704359f0351bd617034dbf" dependencies = [ - "cc", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "io-uring" -version = "0.7.9" +name = "jiff-tzdb" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" -dependencies = [ - "bitflags", - "cfg-if", - "libc", -] +checksum = "68971ebff725b9e2ca27a601c5eb38a4c5d64422c4cbab0c535f248087eda5c2" [[package]] -name = "itoa" -version = "1.0.15" +name = "jiff-tzdb-platform" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] [[package]] name = "job-watcher" @@ -413,7 +324,7 @@ dependencies = [ "anyhow", "askama", "axum", - "chrono", + "jiff", "rand", "serde", "tokio", @@ -422,16 +333,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - [[package]] name = "libc" version = "0.2.175" @@ -462,15 +363,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - [[package]] name = "mio" version = "1.0.4" @@ -479,25 +371,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", + "windows-sys 0.59.0", ] [[package]] @@ -524,6 +398,21 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +dependencies = [ + "portable-atomic", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -535,18 +424,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -586,24 +475,12 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - [[package]] name = "ryu" version = "1.0.20" @@ -612,18 +489,28 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -664,18 +551,6 @@ dependencies = [ "serde", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" - [[package]] name = "smallvec" version = "1.15.1" @@ -689,14 +564,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "syn" -version = "2.0.105" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -711,26 +586,23 @@ checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" [[package]] name = "tokio" -version = "1.47.1" +version = "1.49.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" dependencies = [ - "backtrace", - "io-uring", "libc", "mio", "pin-project-lite", - "slab", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", @@ -739,9 +611,9 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -755,9 +627,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ "bitflags", "bytes", @@ -785,9 +657,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -797,9 +669,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -808,18 +680,18 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "wasi" @@ -836,130 +708,28 @@ dependencies = [ "wit-bindgen-rt", ] -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-link", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.59.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets", + "windows-link", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 431daac..bbb09ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,18 +4,18 @@ version = "0.1.0" edition = "2024" [dependencies] -anyhow = "1.0.99" +anyhow = "1.0.101" askama = "0.14.0" -axum = { version = "0.8.4", features = ["json"] } -chrono = { version = "0.4.41", features = ["serde"] } +axum = { version = "0.8.8", features = ["json"] } rand = "0.9.2" -serde = { version = "1.0.219", features = ["derive", "rc"] } -tokio = { version = "1.47.1", features = ["rt-multi-thread"]} -tower = "0.5.2" -tower-http = { version = "0.6.6", features = [ +serde = { version = "1.0.228", features = ["derive", "rc"] } +jiff = { version = "0.2.19", features = ["serde"]} +tokio = { version = "1.49.0", features = ["rt-multi-thread"]} +tower = "0.5.3" +tower-http = { version = "0.6.8", features = [ "cors", "limit", "timeout", "trace", ] } -tracing = "0.1.41" +tracing = "0.1.44" diff --git a/examples/leaderboard.rs b/examples/leaderboard.rs index 864d7b0..1019c92 100644 --- a/examples/leaderboard.rs +++ b/examples/leaderboard.rs @@ -1,5 +1,5 @@ use anyhow::{Result, bail}; -use chrono::{DateTime, Utc}; +use jiff::Zoned; use job_watcher::{ AppBuilder, Heartbeat, TaskLabel, WatchedTask, WatchedTaskOutput, WatcherAppContext, config::{Delay, TaskConfig, WatcherConfig}, @@ -16,7 +16,7 @@ struct LeaderBoard; struct TaskTwo; #[derive(Clone)] -struct DummyApp; +struct DummyApp(Zoned); impl DummyApp { async fn start() -> Result<()> { @@ -24,7 +24,7 @@ impl DummyApp { println!("Starting leaderboard watcher example."); println!("The watcher will run, but the status page is not served in this example."); - let app = Arc::new(DummyApp); + let app = Arc::new(DummyApp(Zoned::now())); let mut builder = AppBuilder::new(app.clone()); builder.watch_periodic(TaskLabel::new("leaderboard"), LeaderBoard)?; @@ -39,8 +39,8 @@ impl WatcherAppContext for DummyApp { Some("test-env".to_string()) } - fn live_since(&self) -> DateTime { - Utc::now() + fn live_since(&self) -> Zoned { + self.0.clone() } fn watcher_config(&self) -> WatcherConfig { @@ -68,11 +68,7 @@ impl WatcherAppContext for DummyApp { fn triggers_alert(&self, label: &TaskLabel, _selected_label: Option<&TaskLabel>) -> bool { // In a real app, you might have different logic for different tasks. - if label.ident() == "leaderboard" { - false - } else { - true - } + label.ident() != "leaderboard" } fn show_output(&self, _label: &TaskLabel) -> bool { @@ -80,7 +76,7 @@ impl WatcherAppContext for DummyApp { } fn build_version(&self) -> Option { - Some("dfa2test".to_owned()) + Some("dfa2testdkafjakfjakjkafjkafjakfjkajkajkajk".to_owned()) } fn title(&self) -> String { diff --git a/justfile b/justfile index f65442a..6bb3513 100644 --- a/justfile +++ b/justfile @@ -2,6 +2,18 @@ default: just --list --unsorted +# cargo compile +cargo-compile: + cargo test --workspace --no-run --locked + +# Clippy check +cargo-clippy-check: + cargo clippy --no-deps --workspace --locked --tests --benches --examples -- -Dwarnings + +# Rustfmt check +cargo-fmt-check: + cargo fmt --all --check + # Run example example: cargo run --example leaderboard diff --git a/rust-toolchain.toml b/rust-toolchain.toml index c1bc0a6..ff100ed 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.85.0" +channel = "1.90.0" diff --git a/src/lib.rs b/src/lib.rs index b36cca0..2a22c48 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,18 +30,18 @@ use axum::{ http::{self, HeaderValue}, response::IntoResponse, }; -use chrono::{DateTime, Duration, Utc}; use config::{Delay, WatcherConfig}; +use jiff::{Span, Zoned}; use rand::Rng; -use std::fmt::Write; use std::{borrow::Cow, collections::HashMap, fmt::Display, pin::Pin, sync::Arc, time::Instant}; +use std::{convert::Infallible, fmt::Write}; use tokio::{net::TcpListener, sync::RwLock, task::JoinSet}; pub trait WatcherAppContext { fn title(&self) -> String; fn environment(&self) -> Option; fn build_version(&self) -> Option; - fn live_since(&self) -> DateTime; + fn live_since(&self) -> Zoned; fn watcher_config(&self) -> WatcherConfig; fn triggers_alert(&self, label: &TaskLabel, selected_label: Option<&TaskLabel>) -> bool; fn show_output(&self, label: &TaskLabel) -> bool; @@ -52,7 +52,6 @@ pub trait WatcherAppContext { )] #[serde(transparent)] pub struct TaskLabel(String); - impl TaskLabel { pub fn new(s: impl Into) -> Self { Self(s.into()) @@ -70,7 +69,8 @@ impl Display for TaskLabel { } struct ToSpawn { - future: Pin> + Send>>, + /// This future is expected to be an infinite loop and never return Ok. + future: Pin> + Send>>, label: TaskLabel, } @@ -98,7 +98,7 @@ impl TaskResultValue { #[serde(rename_all = "kebab-case")] pub struct TaskResult { pub value: Arc, - pub updated: DateTime, + pub updated: Zoned, } #[derive(Clone, serde::Serialize, Debug)] @@ -106,19 +106,7 @@ pub struct TaskResult { pub struct TaskError { #[serde(skip)] pub value: Arc, - pub updated: DateTime, -} - -impl TaskResult { - fn since(&self) -> Since { - Since(self.updated) - } -} - -impl TaskError { - fn since(&self) -> Since { - Since(self.updated) - } + pub updated: Zoned, } #[derive(Clone, Copy, Default, serde::Serialize, Debug)] @@ -140,10 +128,10 @@ impl TaskCounts { pub struct TaskStatus { pub last_result: TaskResult, pub last_retry_error: Option, - pub current_run_started: Option>, + pub current_run_started: Option, /// Is the last_result out of date ? #[serde(skip)] - pub out_of_date: Option, + pub out_of_date: Option, /// Should we expire the status of last result ? #[serde(skip)] pub expire_last_result: Option<(std::time::Duration, Instant)>, @@ -153,16 +141,10 @@ pub struct TaskStatus { pub(crate) type StatusMap = HashMap>>; -enum StoppedTask { - Background, - Periodic(TaskLabel), - RestApi, -} - #[derive(Default)] pub(crate) struct Watcher { to_spawn: Vec, - set: JoinSet>, + set: JoinSet>, statuses: StatusMap, } @@ -237,12 +219,57 @@ struct StatusTemplate<'a> { statuses: Vec, env: Cow<'a, str>, build_version: Cow<'a, str>, - live_since: DateTime, - now: DateTime, + live_since: Zoned, + now: Zoned, alert: bool, title: String, } +impl<'a> StatusTemplate<'a> { + fn live_since_human(&self) -> String { + let duration = self.now.duration_since(&self.live_since); + let secs = duration.as_secs(); + + if secs < 0 { + return "In the future".to_string(); + } + if secs == 0 { + return "just now".to_string(); + } + + let minutes = secs / 60; + let secs_rem = secs % 60; + let hours = minutes / 60; + let minutes_rem = minutes % 60; + let days = hours / 24; + let hours_rem = hours % 24; + + let mut result = String::new(); + let mut need_space = false; + + for (number, letter) in [ + (days, "d"), + (hours_rem, "h"), + (minutes_rem, "m"), + (secs_rem, "s"), + ] { + if number > 0 { + if need_space { + result.push(' '); + } + result.push_str(&format!("{}{}", number, letter)); + need_space = true; + } + } + + if result.is_empty() { + "0s".to_string() + } else { + result + } + } +} + impl TaskStatuses { async fn to_template<'a, C: WatcherAppContext>( &'a self, @@ -251,12 +278,13 @@ impl TaskStatuses { ) -> StatusTemplate<'a> { let statuses = self.statuses(app, label).await; let alert = statuses.iter().any(|x| x.short.alert()); + let now = Zoned::now(); StatusTemplate { statuses, env: app.environment().unwrap_or("Unknown".to_owned()).into(), build_version: app.build_version().unwrap_or("Unknown".to_owned()).into(), - now: Utc::now(), + now, alert, live_since: app.live_since(), title: app.title(), @@ -457,11 +485,11 @@ impl TaskStatus { } fn is_out_of_date(&self) -> OutOfDateType { - match self.current_run_started { + match &self.current_run_started { Some(started) => match self.out_of_date { Some(out_of_date) => { - let now = Utc::now(); - if started + Duration::seconds(300) <= now { + let now = Zoned::now(); + if started.clone() + Span::new().seconds(300) <= now { OutOfDateType::Very } else if started + out_of_date <= now { OutOfDateType::Slightly @@ -509,42 +537,6 @@ impl TaskStatus { } } -struct Since(DateTime); - -impl Display for Since { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let duration = Utc::now().signed_duration_since(self.0); - let secs = duration.num_seconds(); - - match secs.cmp(&0) { - std::cmp::Ordering::Less => write!(f, "{}", self.0), - std::cmp::Ordering::Equal => write!(f, "just now ({})", self.0), - std::cmp::Ordering::Greater => { - let minutes = secs / 60; - let secs = secs % 60; - let hours = minutes / 60; - let minutes = minutes % 60; - let days = hours / 24; - let hours = hours % 24; - - let mut need_space = false; - for (number, letter) in [(days, 'd'), (hours, 'h'), (minutes, 'm'), (secs, 's')] { - if number > 0 { - if need_space { - write!(f, " {number}{letter}")?; - } else { - need_space = true; - write!(f, "{number}{letter}")?; - } - } - } - - write!(f, " ({})", self.0) - } - } - } -} - pub struct AppBuilder { pub app: Arc, pub(crate) watcher: Watcher, @@ -556,23 +548,18 @@ impl Watcher { app: Arc, listener: TcpListener, ) -> Result<()> { - self.set.spawn(async { - rest_api::start_rest_api( - app, - TaskStatuses { - statuses: Arc::new(self.statuses), - }, - listener, - ) - .await?; - Ok(StoppedTask::RestApi) - }); + self.set.spawn(rest_api::start_rest_api( + app, + TaskStatuses { + statuses: Arc::new(self.statuses), + }, + listener, + )); for ToSpawn { future, label } in self.to_spawn { self.set.spawn(async move { future .await - .with_context(|| format!("Failure while running: {label}"))?; - Ok(StoppedTask::Periodic(label)) + .with_context(|| format!("Task failed: {}", label)) }); } if let Some(res) = self.set.join_next().await { @@ -581,14 +568,9 @@ impl Watcher { self.set.abort_all(); return Err(e); } - Ok(task) => { - self.set.abort_all(); - let task_label = match task { - StoppedTask::Background => "background task".into(), - StoppedTask::Periodic(task_label) => format!("task tracking {task_label}"), - StoppedTask::RestApi => "REST API".into(), - }; - anyhow::bail!("Unexpected finish of {task_label}") + Ok(_task) => { + // This branch is impossible, as Infallible can't be created. + anyhow::bail!("Impossible: Infallible witnessed!") } } } @@ -692,12 +674,9 @@ impl AppBuilder { /// Watch a background job that runs continuously, launched immediately pub fn watch_background(&mut self, task: Fut) where - Fut: std::future::Future> + Send + 'static, + Fut: std::future::Future> + Send + 'static, { - self.watcher.set.spawn(async { - task.await?; - Ok(StoppedTask::Background) - }); + self.watcher.set.spawn(task); } pub fn watch_periodic(&mut self, label: TaskLabel, mut task: T) -> Result<()> @@ -717,11 +696,11 @@ impl AppBuilder { let config = task_config_for(&self.app.watcher_config(), &label); let out_of_date = config .out_of_date - .map(|item| chrono::Duration::seconds(item.into())); + .map(|seconds| Span::new().seconds(seconds)); let task_status = Arc::new(RwLock::new(TaskStatus { last_result: TaskResult { value: TaskResultValue::NotYetRun.into(), - updated: Utc::now(), + updated: Zoned::now(), }, last_retry_error: None, current_run_started: None, @@ -750,7 +729,7 @@ impl AppBuilder { *guard = TaskStatus { last_result: old.last_result.clone(), last_retry_error: old.last_retry_error.clone(), - current_run_started: Some(Utc::now()), + current_run_started: Some(Zoned::now()), out_of_date, counts: old.counts, expire_last_result: old.expire_last_result, @@ -801,9 +780,9 @@ impl AppBuilder { } } let last_run_seconds = { - if let Some(old_run_started) = old.current_run_started { - let duration = Utc::now() - old_run_started; - Some(duration.num_seconds()) + if let Some(old_run_started) = old.current_run_started.clone() { + let duration = Zoned::now() - old_run_started; + Some(duration.get_seconds()) } else { None } @@ -817,7 +796,7 @@ impl AppBuilder { } else { TaskResultValue::Ok(message).into() }, - updated: Utc::now(), + updated: Zoned::now(), }, last_retry_error: None, current_run_started: None, @@ -875,9 +854,9 @@ impl AppBuilder { let mut guard = task_status.write().await; let old = &*guard; let last_run_seconds = { - if let Some(old_run_started) = old.current_run_started { - let duration = Utc::now() - old_run_started; - Some(duration.num_seconds()) + if let Some(old_run_started) = old.current_run_started.clone() { + let duration = Zoned::now() - old_run_started; + Some(duration.get_seconds()) } else { None } @@ -905,7 +884,7 @@ impl AppBuilder { *guard = TaskStatus { last_result: TaskResult { value: TaskResultValue::Err(new_error_message).into(), - updated: Utc::now(), + updated: Zoned::now(), }, last_retry_error: None, current_run_started: None, @@ -922,9 +901,9 @@ impl AppBuilder { let mut guard = task_status.write().await; let old = &*guard; let last_run_seconds = { - if let Some(old_run_started) = old.current_run_started { - let duration = Utc::now() - old_run_started; - Some(duration.num_seconds()) + if let Some(old_run_started) = old.current_run_started.clone() { + let duration = Zoned::now() - old_run_started; + Some(duration.get_seconds()) } else { None } @@ -933,7 +912,7 @@ impl AppBuilder { last_result: old.last_result.clone(), last_retry_error: Some(TaskError { value: format!("{err:?}").into(), - updated: Utc::now(), + updated: Zoned::now(), }), current_run_started: None, out_of_date, diff --git a/src/rest_api.rs b/src/rest_api.rs index 0b8deeb..95a96b5 100644 --- a/src/rest_api.rs +++ b/src/rest_api.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{convert::Infallible, sync::Arc}; use anyhow::Result; use axum::{ @@ -31,7 +31,7 @@ pub(crate) async fn start_rest_api, statuses: TaskStatuses, listener: TcpListener, -) -> Result<()> { +) -> Result { let service_builder = ServiceBuilder::new() .layer( TraceLayer::new_for_http() @@ -60,8 +60,9 @@ pub(crate) async fn start_rest_api &'static str { diff --git a/templates/status.html b/templates/status.html index 95e93b1..84b61f5 100644 --- a/templates/status.html +++ b/templates/status.html @@ -1,100 +1,263 @@ - + - {{ title }} - - - - - + + + + - -
-
-

{{ title }}

-
-
-
-
Environment {{env}}
-
Version {{build_version}}
-
Live since {{live_since}}
-
Current time {{now}}
-
-
-
- - - - - - + +
+ + +
+
+

{{ title }}

+
{{ now.strftime("%Y-%m-%d %H:%M:%S") }}
+
+
+ +
+ + +
+
+
Environment
+
{{env}}
+
+
+
Version
+
{{build_version}}
+
+
+
Live since
+
{{live_since_human()}}
+
+
+ + +
+
+

Task Overview

+
+
    {% for status in statuses %} -
- - - +
  • + +
    +
    +
    + + + + + +
    +

    {{ status.label }}

    +
    +
    +
    {{ status.short.as_str() }}
    + +
    +
    +
    +
  • {% endfor %} - -
    TaskStatus
    - {{ status.label }} - - {{ status.short.as_str() }} -
    -
    -
    - {% for status in statuses %} -
    -

    {{ status.label }}

    -

    Status: {{ status.short.as_str() }} {{ status.status.last_result.since() }} | Last Run time (Seconds): {{ status.status.total_run_time() }}

    -

    Successes: {{ status.status.counts.successes }} Retries: {{ status.status.counts.retries }} Errors: {{ status.status.counts.errors }}

    - {% if let Some(started) = status.status.current_run_started.as_ref() %} -

    Currently running, started at {{ started }}

    - {% endif %} + +
    + + +
    + {% for status in statuses %} +
    + +
    +
    +

    + {{ status.label }} + + {{ status.short.as_str() }} + +

    +

    + Last updated: {{ status.status.last_result.updated.strftime("%Y-%m-%d %H:%M:%S") }} | Run time: {{ status.status.total_run_time() }}s +

    +
    +
    + +
    +
    + +
    + + +
    +
    +
    Successes
    +
    {{ status.status.counts.successes }}
    +
    +
    +
    Retries
    +
    {{ status.status.counts.retries }}
    +
    +
    +
    Errors
    +
    {{ status.status.counts.errors }}
    +
    +
    + + + {% if let Some(started) = status.status.current_run_started.as_ref() %} +
    +
    +
    + + + + +
    +
    +

    Task Currently Running

    +
    +

    Started at {{ started.strftime("%Y-%m-%d %H:%M:%S") }}

    +
    +
    +
    +
    + {% endif %} + + +
    +
    + +
    +
    {{ status.status.last_result.value.as_ref().as_str() }}
    +
    + + + {% if let Some(retrying) = status.status.last_retry_error.as_ref() %} +
    +
    +
    +
    + +
    +
    +

    Retry in progress

    +
    +

    Last error occurred {{ retrying.updated.strftime("%Y-%m-%d %H:%M:%S") }}

    +
    +
    {{ retrying.value }}
    +
    +
    +
    +
    +
    +
    + {% endif %} -
    {{ status.status.last_result.value.as_ref().as_str() }}
    +
    +
    + {% endfor %} +
    + + - {% if let Some(retrying) = status.status.last_retry_error.as_ref() %} -

    Currently retrying, last error message {{ retrying.since() }}

    -
    {{ retrying.value }}
    - {% endif %} -
    - {% endfor %} +
    +