From d1198213391710ccc6f2c108b15aa4526380423d Mon Sep 17 00:00:00 2001 From: asepharyana Date: Fri, 28 Aug 2026 21:45:40 +0700 Subject: [PATCH] feat: add panic tracking and logging with PanicTracker - Implemented PanicTracker to log panics using tracing within a dedicated span. - Introduced PanicInfo struct to capture panic details. - Added tests for panic catching and hook restoration. feat: implement token-bucket rate limiter - Created RateLimiter to manage token consumption with configurable rates and burst capacity. - Added methods for acquiring tokens and checking available tokens. - Included tests for rate limiting behavior and token availability. feat: add automatic retry with exponential backoff - Developed retry function to handle transient errors with configurable retry strategies. - Introduced RetryConfig for customizing retry behavior and jitter. - Added tests for various retry scenarios and error handling. feat: implement graceful shutdown coordination - Created ShutdownManager to manage shutdown signals and task completion. - Introduced ShutdownSignal for tasks to observe shutdown requests. - Added tests for shutdown request handling and task draining. feat: add timeout functionality for async operations - Implemented with_timeout function to enforce execution time limits on futures. - Created Timeout struct for wrapping futures with a deadline. - Added tests for timeout behavior and error mapping. --- .github/workflows/ci.yml | 86 +- .github/workflows/publish.yml | 12 +- .gitignore | 3 +- .releaserc.json | 4 +- Cargo.lock | 4490 ++++++++++++++++- README.md | 177 +- crates/corex-cache/Cargo.toml | 40 + .../corex-cache/LICENSE-APACHE | 0 LICENSE-MIT => crates/corex-cache/LICENSE-MIT | 0 crates/corex-cache/README.md | 33 + crates/corex-cache/src/cache_aside.rs | 123 + crates/corex-cache/src/lib.rs | 73 + crates/corex-cache/src/memory.rs | 191 + crates/corex-cache/src/moka_cache.rs | 96 + crates/corex-cache/src/multilayer.rs | 139 + crates/corex-cache/src/redis.rs | 124 + crates/corex-cache/src/traits.rs | 77 + crates/corex-config/Cargo.toml | 42 + crates/corex-config/LICENSE-APACHE | 201 + crates/corex-config/LICENSE-MIT | 21 + crates/corex-config/README.md | 49 + crates/corex-config/src/dynamic.rs | 191 + crates/corex-config/src/error.rs | 32 + crates/corex-config/src/lib.rs | 53 + crates/corex-config/src/loader.rs | 348 ++ crates/corex-crypto/Cargo.toml | 37 + crates/corex-crypto/LICENSE-APACHE | 201 + crates/corex-crypto/LICENSE-MIT | 21 + crates/corex-crypto/README.md | 36 + crates/corex-crypto/src/encryption.rs | 146 + crates/corex-crypto/src/key_ring.rs | 96 + crates/corex-crypto/src/lib.rs | 96 + crates/corex-crypto/src/password.rs | 166 + crates/corex-crypto/src/token.rs | 182 + crates/corex-event/Cargo.toml | 43 + crates/corex-event/LICENSE-APACHE | 201 + crates/corex-event/LICENSE-MIT | 21 + crates/corex-event/README.md | 36 + crates/corex-event/src/amqp.rs | 163 + crates/corex-event/src/lib.rs | 60 + crates/corex-event/src/memory.rs | 147 + crates/corex-event/src/nats.rs | 95 + crates/corex-event/src/traits.rs | 71 + crates/corex-event/src/typed.rs | 103 + crates/corex-storage/Cargo.toml | 44 + crates/corex-storage/LICENSE-APACHE | 201 + crates/corex-storage/LICENSE-MIT | 21 + crates/corex-storage/README.md | 26 + crates/corex-storage/src/gcs.rs | 165 + crates/corex-storage/src/lib.rs | 51 + crates/corex-storage/src/local.rs | 165 + crates/corex-storage/src/s3.rs | 334 ++ crates/corex-storage/src/traits.rs | 109 + .../mytheclipse/CHANGELOG.md | 0 crates/mytheclipse/Cargo.lock | 479 ++ crates/mytheclipse/Cargo.toml | 45 + crates/mytheclipse/LICENSE-APACHE | 201 + crates/mytheclipse/LICENSE-MIT | 21 + crates/mytheclipse/README.md | 163 + .../mytheclipse/examples}/main.rs | 0 .../mytheclipse/src}/backpressure.rs | 0 {src => crates/mytheclipse/src}/bg.rs | 0 .../mytheclipse/src}/circuit_breaker.rs | 0 {src => crates/mytheclipse/src}/compute.rs | 0 .../mytheclipse/src}/concurrency.rs | 0 {src => crates/mytheclipse/src}/context.rs | 0 {src => crates/mytheclipse/src}/cron.rs | 0 {src => crates/mytheclipse/src}/error.rs | 0 {src => crates/mytheclipse/src}/io.rs | 0 {src => crates/mytheclipse/src}/lib.rs | 0 {src => crates/mytheclipse/src}/metrics.rs | 0 .../mytheclipse/src}/panic_tracker.rs | 0 {src => crates/mytheclipse/src}/ratelimit.rs | 0 {src => crates/mytheclipse/src}/retry.rs | 0 {src => crates/mytheclipse/src}/shutdown.rs | 0 {src => crates/mytheclipse/src}/timeout.rs | 0 76 files changed, 10360 insertions(+), 191 deletions(-) create mode 100644 crates/corex-cache/Cargo.toml rename LICENSE-APACHE => crates/corex-cache/LICENSE-APACHE (100%) rename LICENSE-MIT => crates/corex-cache/LICENSE-MIT (100%) create mode 100644 crates/corex-cache/README.md create mode 100644 crates/corex-cache/src/cache_aside.rs create mode 100644 crates/corex-cache/src/lib.rs create mode 100644 crates/corex-cache/src/memory.rs create mode 100644 crates/corex-cache/src/moka_cache.rs create mode 100644 crates/corex-cache/src/multilayer.rs create mode 100644 crates/corex-cache/src/redis.rs create mode 100644 crates/corex-cache/src/traits.rs create mode 100644 crates/corex-config/Cargo.toml create mode 100644 crates/corex-config/LICENSE-APACHE create mode 100644 crates/corex-config/LICENSE-MIT create mode 100644 crates/corex-config/README.md create mode 100644 crates/corex-config/src/dynamic.rs create mode 100644 crates/corex-config/src/error.rs create mode 100644 crates/corex-config/src/lib.rs create mode 100644 crates/corex-config/src/loader.rs create mode 100644 crates/corex-crypto/Cargo.toml create mode 100644 crates/corex-crypto/LICENSE-APACHE create mode 100644 crates/corex-crypto/LICENSE-MIT create mode 100644 crates/corex-crypto/README.md create mode 100644 crates/corex-crypto/src/encryption.rs create mode 100644 crates/corex-crypto/src/key_ring.rs create mode 100644 crates/corex-crypto/src/lib.rs create mode 100644 crates/corex-crypto/src/password.rs create mode 100644 crates/corex-crypto/src/token.rs create mode 100644 crates/corex-event/Cargo.toml create mode 100644 crates/corex-event/LICENSE-APACHE create mode 100644 crates/corex-event/LICENSE-MIT create mode 100644 crates/corex-event/README.md create mode 100644 crates/corex-event/src/amqp.rs create mode 100644 crates/corex-event/src/lib.rs create mode 100644 crates/corex-event/src/memory.rs create mode 100644 crates/corex-event/src/nats.rs create mode 100644 crates/corex-event/src/traits.rs create mode 100644 crates/corex-event/src/typed.rs create mode 100644 crates/corex-storage/Cargo.toml create mode 100644 crates/corex-storage/LICENSE-APACHE create mode 100644 crates/corex-storage/LICENSE-MIT create mode 100644 crates/corex-storage/README.md create mode 100644 crates/corex-storage/src/gcs.rs create mode 100644 crates/corex-storage/src/lib.rs create mode 100644 crates/corex-storage/src/local.rs create mode 100644 crates/corex-storage/src/s3.rs create mode 100644 crates/corex-storage/src/traits.rs rename CHANGELOG.md => crates/mytheclipse/CHANGELOG.md (100%) create mode 100644 crates/mytheclipse/Cargo.lock create mode 100644 crates/mytheclipse/Cargo.toml create mode 100644 crates/mytheclipse/LICENSE-APACHE create mode 100644 crates/mytheclipse/LICENSE-MIT create mode 100644 crates/mytheclipse/README.md rename {examples => crates/mytheclipse/examples}/main.rs (100%) rename {src => crates/mytheclipse/src}/backpressure.rs (100%) rename {src => crates/mytheclipse/src}/bg.rs (100%) rename {src => crates/mytheclipse/src}/circuit_breaker.rs (100%) rename {src => crates/mytheclipse/src}/compute.rs (100%) rename {src => crates/mytheclipse/src}/concurrency.rs (100%) rename {src => crates/mytheclipse/src}/context.rs (100%) rename {src => crates/mytheclipse/src}/cron.rs (100%) rename {src => crates/mytheclipse/src}/error.rs (100%) rename {src => crates/mytheclipse/src}/io.rs (100%) rename {src => crates/mytheclipse/src}/lib.rs (100%) rename {src => crates/mytheclipse/src}/metrics.rs (100%) rename {src => crates/mytheclipse/src}/panic_tracker.rs (100%) rename {src => crates/mytheclipse/src}/ratelimit.rs (100%) rename {src => crates/mytheclipse/src}/retry.rs (100%) rename {src => crates/mytheclipse/src}/shutdown.rs (100%) rename {src => crates/mytheclipse/src}/timeout.rs (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c95d23a..fffd098 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,10 +28,10 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: components: clippy - - name: Clippy (all features) - run: cargo clippy --all-features -- -D warnings - - name: Clippy (no default features) - run: cargo clippy --no-default-features -- -D warnings + - name: Clippy (workspace, all features) + run: cargo clippy --workspace --all-features -- -D warnings + - name: Clippy (workspace, no default features) + run: cargo clippy --workspace --no-default-features -- -D warnings test-matrix: name: Test (${{ matrix.name }}) @@ -40,24 +40,53 @@ jobs: fail-fast: false matrix: include: - - name: default features - flags: "" - - name: all features - flags: "--all-features" - - name: io only - flags: "--no-default-features --features io" - - name: compute only - flags: "--no-default-features --features compute" - - name: bg only - flags: "--no-default-features --features bg" - - name: resiliency only - flags: "--no-default-features --features resiliency" - - name: traffic only - flags: "--no-default-features --features traffic" - - name: lifecycle only - flags: "--no-default-features --features lifecycle" - - name: observability only - flags: "--no-default-features --features observability" + # Whole-workspace sanity passes. + - name: workspace default features + flags: "--workspace" + - name: workspace all features + flags: "--workspace --all-features" + # mytheclipse: execution primitives + resiliency/traffic/lifecycle/observability + - name: mytheclipse / io only + flags: "-p mytheclipse --no-default-features --features io" + - name: mytheclipse / compute only + flags: "-p mytheclipse --no-default-features --features compute" + - name: mytheclipse / bg only + flags: "-p mytheclipse --no-default-features --features bg" + - name: mytheclipse / resiliency only + flags: "-p mytheclipse --no-default-features --features resiliency" + - name: mytheclipse / traffic only + flags: "-p mytheclipse --no-default-features --features traffic" + - name: mytheclipse / lifecycle only + flags: "-p mytheclipse --no-default-features --features lifecycle" + - name: mytheclipse / observability only + flags: "-p mytheclipse --no-default-features --features observability" + # corex-cache + - name: corex-cache / default + flags: "-p corex-cache" + - name: corex-cache / l1-moka + flags: "-p corex-cache --no-default-features --features l1-moka" + - name: corex-cache / l2-redis + flags: "-p corex-cache --no-default-features --features l1-memory,l2-redis" + # corex-storage + - name: corex-storage / default (local) + flags: "-p corex-storage" + - name: corex-storage / s3 + flags: "-p corex-storage --no-default-features --features s3" + - name: corex-storage / gcs + flags: "-p corex-storage --no-default-features --features gcs" + # corex-event + - name: corex-event / default (mem) + flags: "-p corex-event" + - name: corex-event / amqp + flags: "-p corex-event --no-default-features --features amqp" + - name: corex-event / nats + flags: "-p corex-event --no-default-features --features nats" + # corex-config + - name: corex-config / default + flags: "-p corex-config" + # corex-crypto + - name: corex-crypto / default + flags: "-p corex-crypto" steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -67,12 +96,12 @@ jobs: run: cargo test ${{ matrix.flags }} example: - name: Run example + name: Run mytheclipse example runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - run: cargo run --example main --features full + - run: cargo run -p mytheclipse --example main --features full docs: name: Docs check @@ -80,13 +109,18 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - run: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps + - run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features --no-deps package: name: Cargo package dry-run runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + crate: + [mytheclipse, corex-cache, corex-storage, corex-event, corex-config, corex-crypto] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - name: Package - run: cargo package --no-verify \ No newline at end of file + run: cargo package -p ${{ matrix.crate }} --no-verify diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6564f54..1f346c4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,15 @@ jobs: - uses: dtolnay/rust-toolchain@stable - - name: Publish mytheclipse - run: cargo publish --allow-dirty --no-verify + # The workspace crates have no interdependencies, so publish order + # doesn't matter for crates.io dependency resolution. A brief sleep + # between publishes avoids hitting crates.io's rate limit. + - name: Publish workspace crates + run: | + for crate in mytheclipse corex-cache corex-storage corex-event corex-config corex-crypto; do + echo "Publishing $crate..." + cargo publish -p "$crate" --allow-dirty --no-verify + sleep 15 + done env: CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 88e8725..36ea21d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ -# Cargo build artifacts +# Cargo build artifacts (workspace root and any nested crate target dirs) /target +target/ # Editor / OS noise .DS_Store diff --git a/.releaserc.json b/.releaserc.json index 74bfe7e..517b631 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -9,7 +9,7 @@ [ "@semantic-release/exec", { - "prepareCmd": "sed -i 's/^version = \\\"[^\\\"]*\\\"/version = \\\"${nextRelease.version}\\\"/' Cargo.toml && cargo check", + "prepareCmd": "for f in crates/*/Cargo.toml; do sed -i 's/^version = \\\"[^\\\"]*\\\"/version = \\\"${nextRelease.version}\\\"/' \"$f\"; done && cargo check --workspace", "successCmd": "gh api -X POST repos/asepharyana/mytheclipse/actions/workflows/publish.yml/dispatches -f ref=main -f 'inputs[tag]=v${nextRelease.version}'" } ], @@ -17,7 +17,7 @@ "@semantic-release/git", { "assets": [ - "Cargo.toml", + "crates/*/Cargo.toml", "Cargo.lock", "CHANGELOG.md" ], diff --git a/Cargo.lock b/Cargo.lock index a44bd27..e5caf27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,17 +2,1002 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "amq-protocol" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "587d313f3a8b4a40f866cc84b6059fe83133bf172165ac3b583129dd211d8e1c" +dependencies = [ + "amq-protocol-tcp", + "amq-protocol-types", + "amq-protocol-uri", + "cookie-factory", + "nom", + "serde", +] + +[[package]] +name = "amq-protocol-tcp" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc707ab9aa964a85d9fc25908a3fdc486d2e619406883b3105b48bf304a8d606" +dependencies = [ + "amq-protocol-uri", + "tcp-stream", + "tracing", +] + +[[package]] +name = "amq-protocol-types" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf99351d92a161c61ec6ecb213bc7057f5b837dd4e64ba6cb6491358efd770c4" +dependencies = [ + "cookie-factory", + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "amq-protocol-uri" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89f8273826a676282208e5af38461a07fe939def57396af6ad5997fcf56577d" +dependencies = [ + "amq-protocol-types", + "percent-encoding", + "url", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arc-swap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" +dependencies = [ + "rustversion", +] + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", +] + +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.5.0", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" +dependencies = [ + "async-channel", + "async-executor", + "async-io 2.6.0", + "async-lock 3.4.2", + "blocking", + "futures-lite 2.6.1", +] + +[[package]] +name = "async-global-executor-trait" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af57045d58eeb1f7060e7025a1631cbc6399e0a1d10ad6735b3d0ea7f8346ce" +dependencies = [ + "async-global-executor", + "async-trait", + "executor-trait", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling 3.11.0", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.2", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-nats" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76433c4de73442daedb3a59e991d94e85c14ebfc33db53dfcd347a21cd6ef4f8" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "memchr", + "nkeys", + "nuid", + "once_cell", + "pin-project", + "portable-atomic", + "rand 0.8.8", + "regex", + "ring", + "rustls-native-certs 0.7.3", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "serde", + "serde_json", + "serde_nanos", + "serde_repr", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tokio-websockets", + "tracing", + "tryhard", + "url", +] + +[[package]] +name = "async-reactor-trait" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e" +dependencies = [ + "async-io 1.13.0", + "async-trait", + "futures-core", + "reactor-trait", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-config" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a767267da9e2c2e189b2f9df8b5657e850ecf5352644734ba130d4a57095cf1b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.5.0", + "hex", + "http 1.5.0", + "sha1 0.10.7", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93964ffdaf57857f544be3666a5f57570bb699e934700f11b49708f61bb556e" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "aws-runtime" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9007227e10b5fed2f3e0a2beff489211e2b5604c400b7a9d5d81ca9d64c24bb" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand 2.5.0", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.144.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30dc8bf6baaf7d46336a0ca2c69f223d9b90d7a801fb3e28f7ea17b00dc6b1de" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand 2.5.0", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.5.0", + "http-body 1.1.0", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.108.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c15301b04372832947916607983b114b3374b9db0be058a00fb7513800de1f05" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.5.0", + "http 0.2.12", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.110.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cc2c205cb27108183cf1856333f7d584c2ba0f505421b4209ca5828f9ea899" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.5.0", + "http 0.2.12", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.113.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68182ecb449f7537db0f4d5d25917789cf41e32074a9fe47b6a0b847fe1d2032" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand 2.5.0", + "http 0.2.12", + "http 1.5.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723c2234ad7511ceef63eab016b7ba6ff7c55590fefb96fa8467af014a07309f" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.5.0", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02e407fb3b54891734224b9ffac8a71fdd35f542500fa1af95754a6b2beb316" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.65.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67ecd999972b58e67cab052f5129906c08c25883bd0788ceefc55ef97d61307" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6de526c7b567420a31bc283657a7921b45c4cafe0827fdf2490713dcc770c28f" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37843d9add67c3aff5856f409c6dc315d3cdff60f9c0cb5b670dab1e9920306d" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfd138fac0337cee7516c352757ea73b9f2266e57d0bcb5bc70e9547e45aef1" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.19", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.11.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.43", + "rustls-native-certs 0.8.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dc65a121adb4b33729919fcfa14fa36fb33c1555a8f06bb0e2188dbfdc1d9ef" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e86338c869539a581bf161247762a6e87f92c5c075060057b5ed6d06632ed0c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512346c7212ab7436df2d77a16d976a468ae44a418835511d2a69269810aaf62" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-smithy-xml", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b82e438d30e02a825d363bd639a9efaed68a8089d86101054b0081e7e0d3e606" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand 2.5.0", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954c563ce84507722d2679f07a35d21b9c6466b3872d513020d0281fc8112ac9" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.5.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "221eaa237ddf1ca79b60d1372aad77e47f9c0ea5b3ce5099da8c61d027dc77b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56e0a4e53127a632224e43633b0fe045fa9e1e3cfc68b9830f1115e103f910" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.5.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce83ce9abbb198d25bc7131e468d0f9fe1257125e58c39f3f9fc9f5098c9647" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.5.0", + "http-body 0.4.6", + "http-body 1.1.0", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce84f71c72fee2cbbadde6e7d082f5fb466e3a84733855295fa7aafd1b31b7d8" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec1cd5469f328c782dc3e33d4153cf118a54e33cbb3356d60d16f89883e1f94" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + [[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] [[package]] name = "cfg-if" @@ -20,6 +1005,233 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + +[[package]] +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", + "x509-cert", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[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 = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "corex-cache" +version = "0.1.0" +dependencies = [ + "async-trait", + "moka", + "redis", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "corex-config" +version = "0.1.0" +dependencies = [ + "dotenvy", + "notify", + "serde", + "serde_json", + "serde_yaml", + "tempfile", + "tokio", + "toml", + "tracing", +] + +[[package]] +name = "corex-crypto" +version = "0.1.0" +dependencies = [ + "aead", + "aes-gcm", + "argon2", + "base64 0.22.1", + "jsonwebtoken", + "password-hash", + "rand 0.8.8", + "rand_core 0.6.4", + "serde", + "serde_json", + "tracing", +] + +[[package]] +name = "corex-event" +version = "0.1.0" +dependencies = [ + "async-nats", + "async-trait", + "bytes", + "futures-lite 2.6.1", + "futures-util", + "lapin", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "corex-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "aws-config", + "aws-credential-types", + "aws-sdk-s3", + "bytes", + "google-cloud-auth", + "google-cloud-storage", + "tempfile", + "tokio", + "tracing", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.1", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -46,10 +1258,271 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" [[package]] -name = "either" -version = "1.17.0" +name = "crypto-bigint" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "data-encoding" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2 0.10.9", + "signature", + "subtle", +] + +[[package]] +name = "either" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -58,7 +1531,272 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.2", + "pin-project-lite", +] + +[[package]] +name = "executor-trait" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c39dff9342e4e0e16ce96be751eb21a94e94a87bb2f6e63ad1961c2ce109bf" +dependencies = [ + "async-trait", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.9", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.5.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", ] [[package]] @@ -68,16 +1806,679 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "google-cloud-auth" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57a13fbacc5e9c41ded3ad8d0373175a6b7a6ad430d99e89d314ac121b7ab06" +dependencies = [ + "async-trait", + "base64 0.21.7", + "google-cloud-metadata", + "google-cloud-token", + "home", + "jsonwebtoken", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "time", + "tokio", + "tracing", + "urlencoding", +] + +[[package]] +name = "google-cloud-metadata" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d901aeb453fd80e51d64df4ee005014f6cf39f2d736dd64f7239c132d9d39a6a" +dependencies = [ + "reqwest", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "google-cloud-storage" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34a73d9e94d35665909050f02e035d8bdc82e419241b1b027ebf1ea51dc8a470" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "base64 0.21.7", + "bytes", + "futures-util", + "google-cloud-auth", + "google-cloud-metadata", + "google-cloud-token", + "hex", + "once_cell", + "percent-encoding", + "pkcs8", + "regex", + "reqwest", + "reqwest-middleware", + "ring", + "serde", + "serde_json", + "sha2 0.10.9", + "thiserror 1.0.69", + "time", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "google-cloud-token" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c12ba8b21d128a2ce8585955246977fbce4415f680ebf9199b6f9d6d725f" +dependencies = [ + "async-trait", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.5.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http 1.5.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http 1.5.0", + "http-body 1.1.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.19", + "http 1.5.0", + "http-body 1.1.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.5.0", + "hyper 1.11.1", + "hyper-util", + "rustls 0.23.43", + "rustls-native-certs 0.8.4", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.11.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "hyper 1.11.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.5", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" +dependencies = [ + "libc", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kqueue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.1", + "libc", +] + +[[package]] +name = "lapin" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2aa4725b9607915fa1a73e940710a3be6af508ce700e56897cbe8847fbb07" +dependencies = [ + "amq-protocol", + "async-global-executor-trait", + "async-reactor-trait", + "async-trait", + "executor-trait", + "flume", + "futures-core", + "futures-io", + "parking_lot", + "pinky-swear", + "reactor-trait", + "serde", + "tracing", + "waker-fn", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -90,6 +2491,24 @@ version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "lock_api" version = "0.4.14" @@ -101,9 +2520,68 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lru" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d317b4b9eb398e6acce275758ec6125535505e7a146fb1a9b8bda2451b0ff4c" +dependencies = [ + "hashbrown", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] [[package]] name = "mio" @@ -113,7 +2591,27 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "moka" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9" +dependencies = [ + "async-lock 3.4.2", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "event-listener 5.4.2", + "futures-util", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", ] [[package]] @@ -121,20 +2619,124 @@ name = "mytheclipse" version = "1.1.0" dependencies = [ "num_cpus", - "rand", + "rand 0.8.8", "rayon", "tokio", "tracing", "tracing-subscriber", ] +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe 0.2.1", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nkeys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879011babc47a1c7fdf5a935ae3cfe94f34645ca0cac1c7f6424b36fc743d1bf" +dependencies = [ + "data-encoding", + "ed25519", + "ed25519-dalek", + "getrandom 0.2.17", + "log", + "rand 0.8.8", + "signatory", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.13.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "nuid" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc895af95856f929163a0aa20c26a78d26bfdc839f51b9d5aa7a5b79e52b7e83" +dependencies = [ + "rand 0.8.8", +] + +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -143,16 +2745,126 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77823a27f0babb03091cb9ed9ef80af3b39dbc82f97e8fa530374b7dafd87a45" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p12-keystore" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cae83056e7cb770211494a0ecf66d9fa7eba7d00977e5bb91f0e925b40b937f" +dependencies = [ + "cbc", + "cms", + "der", + "des", + "hex", + "hmac 0.12.1", + "pkcs12", + "pkcs5", + "rand 0.9.5", + "rc2", + "sha1 0.10.7", + "sha2 0.10.9", + "thiserror 2.0.20", + "x509-parser", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -176,12 +2888,216 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinky-swear" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ea6e230dd3a64d61bcb8b79e597d3ab6b4c94ec7a234ce687dd718b4f2e657" +dependencies = [ + "doc-comment", + "flume", + "parking_lot", + "tracing", +] + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand 2.5.0", + "futures-io", +] + +[[package]] +name = "pkcs12" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" +dependencies = [ + "cms", + "const-oid 0.9.6", + "der", + "digest 0.10.7", + "spki", + "x509-cert", + "zeroize", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2 0.10.9", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -191,6 +3107,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -209,6 +3134,18 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.8.8" @@ -216,8 +3153,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -227,7 +3174,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -236,7 +3193,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -259,13 +3225,371 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rc2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" +dependencies = [ + "cipher", +] + +[[package]] +name = "reactor-trait" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58" +dependencies = [ + "async-trait", + "futures-core", + "futures-io", +] + +[[package]] +name = "redis" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d8f99a4090c89cc489a94833c901ead69bfbf3877b4867d5482e321ee875bc" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures-util", + "itertools", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.1", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "http-body-util", + "hyper 1.11.1", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "reqwest-middleware" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e" +dependencies = [ + "anyhow", + "async-trait", + "http 1.5.0", + "reqwest", + "serde", + "thiserror 1.0.69", + "tower-service", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "0.37.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.15", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-connector" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70cc376c6ba1823ae229bacf8ad93c136d93524eab0e4e5e0e4f96b9c4e5b212" +dependencies = [ + "log", + "rustls 0.23.43", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "rustls-webpki 0.103.15", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -274,6 +3598,224 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2 0.10.9", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_nanos" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a93142f0367a4cc53ae0fead1bcda39e85beccfad3dcd717656cacab94b12985" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -283,6 +3825,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "signal-hook-registry" version = "1.4.8" @@ -293,12 +3841,62 @@ dependencies = [ "libc", ] +[[package]] +name = "signatory" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" +dependencies = [ + "pkcs8", + "rand_core 0.6.4", + "signature", + "zeroize", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.6.5" @@ -306,9 +3904,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spin" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "023a211cb3138dbc438680b32560ad89f699977624c9f8dbb95a47d5b4c07dd3" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -322,15 +3957,106 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tcp-stream" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495b0abdce3dc1f8fd27240651c9e68890c14e9d9c61527b1ce44d8a5a7bd3d5" +dependencies = [ + "cfg-if", + "p12-keystore", + "rustls-connector", + "rustls-pemfile", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.5.0", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "thread_local" version = "1.1.10" @@ -340,6 +4066,46 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.53.1" @@ -348,13 +4114,13 @@ checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", - "mio", + "mio 1.2.2", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.5", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -365,9 +4131,160 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.43", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-websockets" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f591660438b3038dd04d16c938271c79e7e06260ad2ea2885a4861bfb238605d" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "http 1.5.0", + "httparse", + "rand 0.8.8", + "ring", + "rustls-native-certs 0.8.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http 1.5.0", + "http-body 1.1.0", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -425,30 +4342,294 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tryhard" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe58ebd5edd976e0fe0f8a14d2a04b7c81ef153ea9a54eebc42e67c2c23b4e5" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -458,6 +4639,205 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 2.0.20", + "time", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.56" @@ -477,3 +4857,69 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/README.md b/README.md index 912a0da..235bb2e 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,64 @@ # mytheclipse -[![Crates.io](https://img.shields.io/crates/v/mytheclipse.svg)](https://crates.io/crates/mytheclipse) -[![Documentation](https://docs.rs/mytheclipse/badge.svg)](https://docs.rs/mytheclipse) -[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) +A personal collection of Rust abstractions for building resource-aware, +resilient, and well-instrumented applications without hand-rolling the same +plumbing every time — organized as a Cargo workspace, one focused crate per +concern. -Resource-aware execution primitives and reliability abstractions for Rust: async I/O, heavy compute, background queue management, resiliency, traffic control, lifecycle management, and observability. Sized automatically from the host's logical core count and exposed through a single, lazily-initialized engine context alongside self-contained, constructible utilities. +[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](crates/mytheclipse/LICENSE-MIT) -## Resource Sizing +## Crates -Given $N$ logical cores (via `num_cpus::get()`): +| Crate | Description | Docs | +| :--- | :--- | :--- | +| [`mytheclipse`](crates/mytheclipse) | Resource-aware execution primitives (async I/O, compute, background queues), resiliency (retry, circuit breaker, timeout), traffic control (rate limiter, backpressure, concurrency limiter), lifecycle (graceful shutdown, cron), and observability (metrics, panic tracking). | [README](crates/mytheclipse/README.md) | +| [`corex-cache`](crates/corex-cache) | Unified multi-layer (L1/L2) cache abstraction: in-memory or Moka L1, Redis/Valkey L2, cache-aside read-through. | [README](crates/corex-cache/README.md) | +| [`corex-storage`](crates/corex-storage) | Unified storage & file system abstraction: one driver interface over local disk, S3/MinIO, and Google Cloud Storage, stream-based. | [README](crates/corex-storage/README.md) | +| [`corex-event`](crates/corex-event) | Unified events & message bus abstraction: in-memory pub/sub dispatcher plus RabbitMQ and NATS broker adapters behind one trait. | [README](crates/corex-event/README.md) | +| [`corex-config`](crates/corex-config) | Type-safe, dynamic configuration engine: load `.env`/YAML/JSON/TOML into typed structs, with hot-reload. | [README](crates/corex-config/README.md) | +| [`corex-crypto`](crates/corex-crypto) | Safe hashing (Argon2id), encryption (AES-256-GCM), and JWT tokens, with key rotation support. | [README](crates/corex-crypto/README.md) | -| Subsystem | Sizing Formula | Default on 8 cores | Backing Primitive | -| :--- | :--- | :--- | :--- | -| **Async I/O** | $N$ | 8 | Ambient `tokio::spawn` + `tracing` span | -| **Compute** | $\max(1, N - 1)$ | 7 | Sized `rayon::ThreadPool` + `catch_unwind` | -| **Background Queue** | $\max(2, \lfloor N / 2 \rfloor)$ | 4 | `tokio::sync::Semaphore` + `tokio::spawn` | +Every crate follows the same philosophy: **one small interface, pluggable +backends behind feature flags, and a working default that needs no external +service to build or test.** Distributed backends (Redis, S3, GCS, RabbitMQ, +NATS) are feature-gated and their integration tests are `#[ignore]`d unless +the corresponding environment variables point at a live service. -The three execution primitives (`io`, `compute`, `bg`) are sized from the host's CPU core count through `mytheclipse::context()`. Resiliency, traffic control, lifecycle, and observability utilities are constructible per-instance without global state. +## Getting started -## Features - -- **`io`**: enables `mytheclipse::spawn_io`, instrumented async task spawning. -- **`compute`**: enables `mytheclipse::compute`, panic-isolated execution on a sized Rayon pool. -- **`bg`**: enables `mytheclipse::spawn_bg`, semaphore-bounded background tasks. -- **`resiliency`**: fault tolerance abstractions: - - `retry` — auto-retry with exponential backoff and full/equal jitter. - - `CircuitBreaker` — failure threshold and cooldown state machine (Closed/Open/HalfOpen). - - `with_timeout` / `timeout` / `Timeout` — hard execution deadlines. -- **`traffic`**: traffic & load control: - - `RateLimiter` — lazy token-bucket rate limiter with burst capacity. - - `BackpressureQueue` — bounded queue with `DropOldest`, `Reject`, and `Block` overflow policies. - - `ConcurrencyLimiter` — synchronous RAII semaphore wrapper for capping concurrent operations. -- **`lifecycle`**: system lifecycle coordination: - - `ShutdownManager` / `ShutdownSignal` — OS signal catching (SIGINT/SIGTERM/Ctrl-C) and graceful task draining. - - `CronSchedule` / `schedule` — self-contained 5-field cron parser and async timer scheduler. -- **`observability`**: runtime visibility: - - `MetricsCollector` — thread-safe statistics collector with Prometheus text exposition format export. - - `PanicTracker` — non-fatal panic logging with tracing context and boundary isolation. -- **`full`**: enables all subsystems: `io`, `compute`, `bg`, `resiliency`, `traffic`, `lifecycle`, `observability`. - -Zero features enabled by default (`default = []`), so you only pull in the dependencies your application actually uses. - -## Quick Start - -Add to your `Cargo.toml`: +Each crate is published independently; add the ones you need: ```toml [dependencies] -mytheclipse = { version = "0.2", features = ["full"] } +mytheclipse = { version = "1", features = ["full"] } +corex-cache = "0.1" +corex-storage = { version = "0.1", features = ["s3"] } +corex-event = { version = "0.1", features = ["nats"] } +corex-config = "0.1" +corex-crypto = "0.1" ``` -### 1. Execution Primitives +See each crate's own README (linked above) for usage examples and the full +feature-flag list. -```rust -#[tokio::main] -async fn main() { - let ctx = mytheclipse::init(); +## Development - // Async I/O (instrumented with tracing) - let io = mytheclipse::spawn_io(async { 42 }); - - // Heavy Compute (isolated from worker panics) - let sum = mytheclipse::compute(|| (1..=1_000_000u64).sum::())?; - - // Background Queue (concurrency-bounded) - let bg = mytheclipse::spawn_bg(async { /* task */ }).await; - - let _ = (io.await, bg.await); -} -``` - -### 2. Resiliency & Fault Tolerance - -```rust -use std::time::Duration; - -// Auto-Retry with Exponential Backoff + Jitter -let result = mytheclipse::retry( - mytheclipse::RetryConfig::default(), - |err| err.is_transient(), - || async { make_network_request().await }, -).await?; - -// Circuit Breaker -let breaker = mytheclipse::CircuitBreaker::new(mytheclipse::CircuitBreakerConfig::default()); -let value = breaker.call(|| fetch_remote_resource())?; - -// Timeout & Deadlines -let value = mytheclipse::with_timeout(Duration::from_secs(5), async { - long_running_task().await -}).await?; -``` - -### 3. Traffic & Resource Control - -```rust -// Rate Limiter (Token Bucket) -let limiter = mytheclipse::RateLimiter::new(100.0, 10); -limiter.acquire().await?; - -// Concurrency Limiter (Sync Semaphore) -let limiter = mytheclipse::ConcurrencyLimiter::new(10); -let _permit = limiter.acquire(); // released on drop - -// Backpressure Queue (Graceful Degradation) -let queue = mytheclipse::BackpressureQueue::new(100, mytheclipse::OverflowPolicy::DropOldest); -queue.push(job).await?; -let next_job = queue.pop().await; -``` - -### 4. Lifecycle & State Management - -```rust -// Graceful Shutdown Manager -let shutdown = mytheclipse::ShutdownManager::new(); -let sig = shutdown.handle(); -tokio::spawn(async move { - let mut sig = sig; - tokio::select! { - _ = sig.wait() => { /* clean up */ } - _ = worker_loop() => {} - } -}); -shutdown.drain(Duration::from_secs(10)).await; - -// Cron Periodic Job Scheduler (Self-contained, no external crates) -let cron = mytheclipse::CronSchedule::parse("0 1 * * *")?; // 1 AM daily -let job = mytheclipse::schedule("0 1 * * *", || async { - clean_cache().await; -})?; -``` - -### 5. Telemetry & Observability - -```rust -// Centralized Metrics Collector (Prometheus text exposition format) -let metrics = mytheclipse::MetricsCollector::new(); -metrics.record_task(Duration::from_millis(15)); -metrics.inc_counter("http_requests_total", 1); -let prometheus_output = metrics.export_prometheus(); - -// Panic Isolation Tracker -let guard = mytheclipse::PanicTracker::install(); // logs panics with tracing span -let result = mytheclipse::PanicTracker::catch(|| { - risky_operation() -}); -``` - -## Running the Example +This is a Cargo workspace; run commands from the repository root: ```bash -cargo run --example main --features full +cargo build --workspace --all-features +cargo test --workspace --all-features +cargo clippy --workspace --all-features -- -D warnings +cargo fmt --all --check ``` +Or target a single crate with `-p `, e.g. `cargo test -p corex-cache`. + ## License Licensed under either of: -- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) +- Apache License, Version 2.0 ([LICENSE-APACHE](crates/mytheclipse/LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](crates/mytheclipse/LICENSE-MIT) or ) at your option. diff --git a/crates/corex-cache/Cargo.toml b/crates/corex-cache/Cargo.toml new file mode 100644 index 0000000..eb936d1 --- /dev/null +++ b/crates/corex-cache/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "corex-cache" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +license = "MIT OR Apache-2.0" +repository = "https://github.com/asepharyana/mytheclipse" +homepage = "https://github.com/asepharyana/mytheclipse" +documentation = "https://docs.rs/corex-cache" +authors = ["asepharyana "] +description = "Unified multi-layer cache abstraction: L1/L2 caching, cache-aside and auto-refresh, with pluggable backends." +readme = "README.md" +keywords = ["cache", "lru", "redis", "multilayer", "cache-aside"] +categories = ["caching", "asynchronous"] + +[features] +default = ["l1-memory", "cache-aside"] +# L1 (in-process) backends. +l1-memory = [] +l1-moka = ["l1-memory", "dep:moka"] +# L2 (distributed) backends. +l2-redis = ["l1-memory", "dep:redis"] +# Cache-aside + auto-refresh helper. +cache-aside = ["l1-memory", "dep:serde", "dep:serde_json"] + +[dependencies] +tracing = "0.1" +# Required unconditionally: the core `Cache` trait (always compiled) uses it. +async-trait = "0.1" +serde = { version = "1", features = ["derive"], optional = true } +serde_json = { version = "1", optional = true } + +# L1: Moka (high-performance in-memory cache). +moka = { version = "0.12", default-features = false, features = ["future"], optional = true } + +# L2: Redis/Valkey async client (multiplexed connection). +redis = { version = "0.27", default-features = false, features = ["tokio-comp"], optional = true } + +[dev-dependencies] +tokio = { version = "1.53", features = ["full"] } diff --git a/LICENSE-APACHE b/crates/corex-cache/LICENSE-APACHE similarity index 100% rename from LICENSE-APACHE rename to crates/corex-cache/LICENSE-APACHE diff --git a/LICENSE-MIT b/crates/corex-cache/LICENSE-MIT similarity index 100% rename from LICENSE-MIT rename to crates/corex-cache/LICENSE-MIT diff --git a/crates/corex-cache/README.md b/crates/corex-cache/README.md new file mode 100644 index 0000000..db6fa48 --- /dev/null +++ b/crates/corex-cache/README.md @@ -0,0 +1,33 @@ +# corex-cache + +A unified multi-layer cache abstraction so your app isn't locked to one cache +provider. Combines an in-process **L1** cache with a distributed **L2** cache +(e.g. Redis/Valkey) behind one simple `get`/`set`/`invalidate` API, plus a +**cache-aside / auto-refresh** helper. + +## Features + +- `l1-memory` (default) — zero-dependency in-process cache. +- `l1-moka` — high-performance Moka-backed L1 with TTL/max-capacity. +- `l2-redis` — Redis/Valkey L2 via `fred`. +- `cache-aside` (default) — read-through cache-aside helper. + +## Usage + +```rust +use corex_cache::{Cache, MemoryCache, MultiLayerCache, CacheAside}; + +let cache = MultiLayerCache::new( + MemoryCache::new(), // L1 + MemoryCache::new(), // L2 (use RedisCache in production) +); +cache.set("k", b"v".to_vec(), None).await.unwrap(); +let v = cache.get("k").await.unwrap(); + +// Cache-aside: fill misses from a source of truth. +let aside = CacheAside::new( + MemoryCache::new(), + |key| async move { Some(format!("data-for-{key}").into_bytes()) }, +); +let _ = aside.get("orders:42").await.unwrap(); +``` diff --git a/crates/corex-cache/src/cache_aside.rs b/crates/corex-cache/src/cache_aside.rs new file mode 100644 index 0000000..5e6f505 --- /dev/null +++ b/crates/corex-cache/src/cache_aside.rs @@ -0,0 +1,123 @@ +//! Cache-aside with read-through (feature `cache-aside`). +//! +//! [`CacheAside`] wires a [`Cache`] to a data source: on a miss it invokes a +//! user-provided async fetcher, stores the result (with an optional TTL), and +//! returns it. This is the standard cache-aside pattern — reads bypass a cold +//! cache by falling back to the source of truth. + +use std::time::Duration; + +use crate::traits::{Cache, CacheError}; + +/// A generic read-through cache-aside helper. +/// +/// `F` is the data source: an async closure `(owned key) -> Option>`. +/// The key is passed by value ([`String`]) so the returned future does not +/// borrow from the caller, which keeps the API simple and `'static`-friendly. +#[derive(Clone)] +pub struct CacheAside { + cache: C, + fetcher: F, + ttl: Option, +} + +impl CacheAside +where + C: Cache, + F: Fn(String) -> Fut + Send + Sync, + Fut: std::future::Future>> + Send, +{ + /// Builds a cache-aside wrapper around `cache` using `fetcher` to fill + /// misses. Entries are stored without expiry unless `with_ttl` is used. + pub fn new(cache: C, fetcher: F) -> Self { + Self { + cache, + fetcher, + ttl: None, + } + } + + /// Applies a `ttl` to every entry written by this wrapper. + pub fn with_ttl(mut self, ttl: Duration) -> Self { + self.ttl = Some(ttl); + self + } + + /// Returns a value for `key`, reading through to the fetcher on a miss and + /// caching the result. + pub async fn get(&self, key: &str) -> Result>, CacheError> { + if let Some(value) = self.cache.get(key).await? { + return Ok(Some(value)); + } + if let Some(value) = (self.fetcher)(key.to_string()).await { + self.cache.set(key, value.clone(), self.ttl).await?; + Ok(Some(value)) + } else { + Ok(None) + } + } + + /// Explicitly evicts `key`. + pub async fn invalidate(&self, key: &str) -> Result<(), CacheError> { + self.cache.invalidate(key).await + } + + /// Returns a reference to the underlying cache. + pub fn cache(&self) -> &C { + &self.cache + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::memory::MemoryCache; + use std::sync::atomic::{AtomicU64, Ordering}; + use std::sync::Arc; + + fn fetcher( + hits: Arc, + ) -> impl Fn(String) -> std::future::Ready>> + Send + Sync { + move |_key: String| { + let n = hits.fetch_add(1, Ordering::SeqCst) + 1; + std::future::ready(Some(format!("fetched-{n}").into_bytes())) + } + } + + #[tokio::test] + async fn miss_reads_through_and_caches() { + let hits = Arc::new(AtomicU64::new(0)); + let aside = CacheAside::new(MemoryCache::new(), fetcher(hits.clone())); + + let first = aside.get("k").await.unwrap().unwrap(); + let second = aside.get("k").await.unwrap().unwrap(); + assert_eq!(first, b"fetched-1"); + // Cache hit — fetcher not called again. + assert_eq!(second, b"fetched-1"); + assert_eq!(hits.load(Ordering::SeqCst), 1); + } + + #[tokio::test] + async fn invalidate_forces_refetch() { + let hits = Arc::new(AtomicU64::new(0)); + let aside = CacheAside::new(MemoryCache::new(), fetcher(hits.clone())); + let _ = aside.get("k").await.unwrap(); + aside.invalidate("k").await.unwrap(); + let again = aside.get("k").await.unwrap().unwrap(); + assert_eq!(again, b"fetched-2"); + assert_eq!(hits.load(Ordering::SeqCst), 2); + } + + #[tokio::test] + async fn ttl_applies_to_writes() { + let aside = CacheAside::new(MemoryCache::new(), fetcher(Arc::new(AtomicU64::new(0)))) + .with_ttl(Duration::from_millis(30)); + let _ = aside.get("k").await.unwrap(); + assert_eq!( + aside.cache().get("k").await.unwrap(), + Some(b"fetched-1".to_vec()) + ); + tokio::time::sleep(Duration::from_millis(60)).await; + assert_eq!(aside.cache().get("k").await.unwrap(), None); + } +} diff --git a/crates/corex-cache/src/lib.rs b/crates/corex-cache/src/lib.rs new file mode 100644 index 0000000..b2aeced --- /dev/null +++ b/crates/corex-cache/src/lib.rs @@ -0,0 +1,73 @@ +//! # corex-cache +//! +//! A unified multi-layer cache abstraction that keeps your application from +//! being locked to any single cache provider. +//! +//! - **L1 (in-process) caches**: [`memory::MemoryCache`] (zero-dependency, +//! default) or [`moka_cache::MokaL1`] (high-performance, TTL/max-capacity). +//! - **L2 (distributed) caches**: [`redis::RedisCache`] backed by Redis/Valkey. +//! - **Multi-layer composition**: [`multilayer::MultiLayerCache`] layers an L1 +//! over an L2 behind one [`Cache`] face; reads fall through to L2 and +//! backfill L1. +//! - **Cache-aside / auto-refresh**: [`cache_aside::CacheAside`] reads through +//! to a data source on a miss and caches the result. +//! +//! The core [`Cache`] trait is byte-oriented; typed convenience (JSON) is +//! layered on top via [`memory::typed::TypedCache`]. +//! +//! ## Example +//! +//! ```no_run +//! use corex_cache::{Cache, MemoryCache, MultiLayerCache, CacheAside}; +//! # async fn run() { +//! let l1 = MemoryCache::new(); +//! let l2 = MemoryCache::new(); // in a real app: a RedisCache +//! let cache = MultiLayerCache::new(l1, l2); +//! +//! cache.set("user:1", b"payload".to_vec(), None).await.unwrap(); +//! assert_eq!(cache.get("user:1").await.unwrap(), Some(b"payload".to_vec())); +//! +//! // Cache-aside: fill misses from a source of truth. +//! let aside = CacheAside::new( +//! MemoryCache::new(), +//! |key| async move { Some(format!("data-for-{key}").into_bytes()) }, +//! ); +//! let _v = aside.get("orders:42").await.unwrap(); +//! # } +//! ``` + +#![forbid(unsafe_code)] + +pub mod traits; + +#[cfg(feature = "l1-memory")] +pub mod memory; + +#[cfg(feature = "l1-moka")] +pub mod moka_cache; + +#[cfg(feature = "l2-redis")] +pub mod redis; + +#[cfg(feature = "cache-aside")] +pub mod cache_aside; + +#[cfg(feature = "cache-aside")] +pub mod multilayer; + +pub use traits::{Cache, CacheError}; + +#[cfg(feature = "l1-memory")] +pub use memory::MemoryCache; + +#[cfg(feature = "l1-moka")] +pub use moka_cache::MokaL1; + +#[cfg(feature = "l2-redis")] +pub use redis::RedisCache; + +#[cfg(feature = "cache-aside")] +pub use cache_aside::CacheAside; + +#[cfg(feature = "cache-aside")] +pub use multilayer::MultiLayerCache; diff --git a/crates/corex-cache/src/memory.rs b/crates/corex-cache/src/memory.rs new file mode 100644 index 0000000..ba8f2cc --- /dev/null +++ b/crates/corex-cache/src/memory.rs @@ -0,0 +1,191 @@ +//! A simple, dependency-free in-process cache (L1, `l1-memory`). +//! +//! Backed by a `HashMap, Instant)>` guarded by a `Mutex`. +//! Entries are lazily expired on access by comparing against `Instant`; a +//! monotonic clock keeps TTLs robust against wall-clock discontinuities. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use async_trait::async_trait; + +use crate::traits::{Cache, CacheError}; + +/// A wrapping entry: `None` expiry means the value never expires. +type Entry = (Vec, Option); + +/// An in-process [`Cache`] implementation for L1 caching. +#[derive(Clone, Default)] +pub struct MemoryCache { + inner: Arc>>, +} + +impl MemoryCache { + /// Builds an empty in-memory cache. + pub fn new() -> Self { + Self::default() + } + + /// Pre-allocates space for `capacity` entries to reduce reallocation. + pub fn with_capacity(self, capacity: usize) -> Self { + self.inner.lock().unwrap().reserve(capacity); + self + } +} + +#[async_trait] +impl Cache for MemoryCache { + async fn get(&self, key: &str) -> Result>, CacheError> { + let mut map = self.inner.lock().unwrap(); + match map.get(key) { + Some((value, Some(expires))) if *expires <= Instant::now() => { + map.remove(key); + Ok(None) + } + Some((value, _)) => Ok(Some(value.clone())), + None => Ok(None), + } + } + + async fn set( + &self, + key: &str, + value: Vec, + ttl: Option, + ) -> Result<(), CacheError> { + let expires = ttl.map(|d| Instant::now() + d); + self.inner + .lock() + .unwrap() + .insert(key.to_string(), (value, expires)); + Ok(()) + } + + async fn invalidate(&self, key: &str) -> Result<(), CacheError> { + self.inner.lock().unwrap().remove(key); + Ok(()) + } + + async fn clear(&self) -> Result<(), CacheError> { + self.inner.lock().unwrap().clear(); + Ok(()) + } +} + +/// A typed view over a byte cache using `serde`-compatible (JSON) encoding. +/// +/// Only enabled with the `cache-aside` feature, which pulls in `serde`. +#[cfg(feature = "cache-aside")] +pub mod typed { + use serde::{de::DeserializeOwned, Serialize}; + + use super::*; + + /// Wraps a [`Cache`] with JSON-based typed get/set. + #[derive(Clone)] + pub struct TypedCache { + inner: C, + } + + impl TypedCache { + /// Wraps `inner`. + pub fn new(inner: C) -> Self { + Self { inner } + } + + /// Fetches and deserializes a value. + pub async fn get(&self, key: &str) -> Result, CacheError> { + match self.inner.get(key).await? { + Some(bytes) => serde_json::from_slice(&bytes) + .map(Some) + .map_err(|e| CacheError::Serialization(e.to_string())), + None => Ok(None), + } + } + + /// Serializes and stores a value. + pub async fn set( + &self, + key: &str, + value: &T, + ttl: Option, + ) -> Result<(), CacheError> { + let bytes = + serde_json::to_vec(value).map_err(|e| CacheError::Serialization(e.to_string()))?; + self.inner.set(key, bytes, ttl).await + } + + /// Returns the underlying byte cache. + pub fn into_inner(self) -> C { + self.inner + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn set_get_roundtrip() { + let c = MemoryCache::new(); + c.set("k", b"v".to_vec(), None).await.unwrap(); + assert_eq!(c.get("k").await.unwrap(), Some(b"v".to_vec())); + assert_eq!(c.get("missing").await.unwrap(), None); + } + + #[tokio::test] + async fn ttl_expires_entry() { + let c = MemoryCache::new(); + c.set("k", b"v".to_vec(), Some(Duration::from_millis(30))) + .await + .unwrap(); + assert_eq!(c.get("k").await.unwrap(), Some(b"v".to_vec())); + tokio::time::sleep(Duration::from_millis(60)).await; + assert_eq!(c.get("k").await.unwrap(), None); + } + + #[tokio::test] + async fn invalidate_and_clear() { + let c = MemoryCache::new(); + c.set("a", b"1".to_vec(), None).await.unwrap(); + c.set("b", b"2".to_vec(), None).await.unwrap(); + c.invalidate("a").await.unwrap(); + assert_eq!(c.get("a").await.unwrap(), None); + assert_eq!(c.get("b").await.unwrap(), Some(b"2".to_vec())); + c.clear().await.unwrap(); + assert_eq!(c.get("b").await.unwrap(), None); + } + + #[cfg(feature = "cache-aside")] + #[tokio::test] + async fn typed_cache_roundtrip() { + use typed::TypedCache; + #[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq)] + struct User { + id: u64, + name: String, + } + let typed = TypedCache::new(MemoryCache::new()); + typed + .set( + "u", + &User { + id: 1, + name: "alice".into(), + }, + None, + ) + .await + .unwrap(); + let got: User = typed.get("u").await.unwrap().unwrap(); + assert_eq!( + got, + User { + id: 1, + name: "alice".into() + } + ); + } +} diff --git a/crates/corex-cache/src/moka_cache.rs b/crates/corex-cache/src/moka_cache.rs new file mode 100644 index 0000000..9438233 --- /dev/null +++ b/crates/corex-cache/src/moka_cache.rs @@ -0,0 +1,96 @@ +//! A high-performance in-process cache backed by Moka (L1, `l1-moka`). +//! +//! Moka provides automatic max-capacity and (optionally) TTL-based eviction, +//! so this L1 is well-suited to workloads where memory bounds matter. + +use std::time::Duration; + +use async_trait::async_trait; +use moka::future::Cache as MokaCache; + +use crate::traits::{Cache, CacheError}; + +/// A Moka-backed [`Cache`] for L1 caching. +#[derive(Clone)] +pub struct MokaL1 { + inner: MokaCache>, +} + +impl MokaL1 { + /// Builds a Moka cache with `max_capacity` entries and an optional default + /// `ttl`. + pub fn new(max_capacity: u64, ttl: Option) -> Self { + let mut builder = MokaCache::builder().max_capacity(max_capacity); + if let Some(ttl) = ttl { + builder = builder.time_to_live(ttl); + } + Self { + inner: builder.build(), + } + } +} + +#[async_trait] +impl Cache for MokaL1 { + async fn get(&self, key: &str) -> Result>, CacheError> { + Ok(self.inner.get(key).await) + } + + async fn set( + &self, + key: &str, + value: Vec, + _ttl: Option, + ) -> Result<(), CacheError> { + // Per-entry TTL overrides are handled by the builder default in Moka; + // the passed `ttl` is intentionally ignored (single configured policy). + self.inner.insert(key.to_string(), value).await; + Ok(()) + } + + async fn invalidate(&self, key: &str) -> Result<(), CacheError> { + self.inner.invalidate(key).await; + Ok(()) + } + + async fn clear(&self) -> Result<(), CacheError> { + self.inner.invalidate_all(); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn set_get_roundtrip() { + let c = MokaL1::new(100, None); + c.set("k", b"v".to_vec(), None).await.unwrap(); + assert_eq!(c.get("k").await.unwrap(), Some(b"v".to_vec())); + assert_eq!(c.get("missing").await.unwrap(), None); + } + + #[tokio::test] + async fn invalidate_and_clear() { + let c = MokaL1::new(100, None); + c.set("a", b"1".to_vec(), None).await.unwrap(); + c.set("b", b"2".to_vec(), None).await.unwrap(); + c.invalidate("a").await.unwrap(); + assert_eq!(c.get("a").await.unwrap(), None); + assert_eq!(c.get("b").await.unwrap(), Some(b"2".to_vec())); + c.clear().await.unwrap(); + assert_eq!(c.get("b").await.unwrap(), None); + } + + #[tokio::test] + async fn ttl_does_expire() { + // Keep a firm TTL assertion; sleep well past the expiry window. + let c = MokaL1::new(100, Some(Duration::from_millis(40))); + c.set("k", b"v".to_vec(), None).await.unwrap(); + assert_eq!(c.get("k").await.unwrap(), Some(b"v".to_vec())); + tokio::time::sleep(Duration::from_millis(120)).await; + let v = c.get("k").await.unwrap(); + assert!(matches!(v, None)); + } +} diff --git a/crates/corex-cache/src/multilayer.rs b/crates/corex-cache/src/multilayer.rs new file mode 100644 index 0000000..ae24000 --- /dev/null +++ b/crates/corex-cache/src/multilayer.rs @@ -0,0 +1,139 @@ +//! Multi-layer (L1/L2) caching behind a single [`Cache`] face. +//! +//! [`MultiLayerCache`] layers a fast in-process L1 over a slower but larger +//! L2 (e.g. Redis). Reads are L1-first with an L2 fallback; a hit on L2 is +//! backfilled into L1. Writes and invalidations go to both layers. + +use std::time::Duration; + +use async_trait::async_trait; + +use crate::traits::{Cache, CacheError}; + +/// A read-through, write-through composition of an L1 and L2 cache. +/// +/// `L1` is typically [`crate::memory::MemoryCache`] or +/// [`crate::moka_cache::MokaL1`]; `L2` is typically a distributed cache such +/// as a Redis backend. Order of layers fixed: `L1` is consulted first. +#[derive(Clone)] +pub struct MultiLayerCache { + l1: L1, + l2: L2, + /// When `true`, an L2 hit is written back into L1 (default `true`). + populate_l1: bool, +} + +impl MultiLayerCache +where + L1: Cache, + L2: Cache, +{ + /// Builds a two-layer cache with L1-backfill enabled. + pub fn new(l1: L1, l2: L2) -> Self { + Self { + l1, + l2, + populate_l1: true, + } + } + + /// Disables L1 backfill-on-read. + pub fn without_l1_backfill(mut self) -> Self { + self.populate_l1 = false; + self + } + + /// Returns a reference to the L1 layer. + pub fn l1(&self) -> &L1 { + &self.l1 + } + + /// Returns a reference to the L2 layer. + pub fn l2(&self) -> &L2 { + &self.l2 + } +} + +#[async_trait] +impl Cache for MultiLayerCache +where + L1: Cache, + L2: Cache, +{ + async fn get(&self, key: &str) -> Result>, CacheError> { + // L1 first. + if let Some(value) = self.l1.get(key).await? { + return Ok(Some(value)); + } + // L2 fallback. + if let Some(value) = self.l2.get(key).await? { + if self.populate_l1 { + self.l1.set(key, value.clone(), None).await?; + } + return Ok(Some(value)); + } + Ok(None) + } + + async fn set( + &self, + key: &str, + value: Vec, + ttl: Option, + ) -> Result<(), CacheError> { + self.l1.set(key, value.clone(), ttl).await?; + self.l2.set(key, value, ttl).await + } + + async fn invalidate(&self, key: &str) -> Result<(), CacheError> { + self.l1.invalidate(key).await?; + self.l2.invalidate(key).await + } + + async fn clear(&self) -> Result<(), CacheError> { + self.l1.clear().await?; + self.l2.clear().await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::memory::MemoryCache; + + #[tokio::test] + async fn read_through_populates_l1() { + let l2 = MemoryCache::new(); + l2.set("k", b"l2-value".to_vec(), None).await.unwrap(); + + let layered = MultiLayerCache::new(MemoryCache::new(), l2); + assert_eq!(layered.l1().get("k").await.unwrap(), None); + assert_eq!(layered.get("k").await.unwrap(), Some(b"l2-value".to_vec())); + // L2 hit should have populated L1. + assert_eq!( + layered.l1().get("k").await.unwrap(), + Some(b"l2-value".to_vec()) + ); + } + + #[tokio::test] + async fn write_goes_to_both() { + let l1 = MemoryCache::new(); + let l2 = MemoryCache::new(); + let layered = MultiLayerCache::new(l1, l2.clone()); + layered.set("k", b"v".to_vec(), None).await.unwrap(); + assert_eq!(layered.l1().get("k").await.unwrap(), Some(b"v".to_vec())); + assert_eq!(l2.get("k").await.unwrap(), Some(b"v".to_vec())); + } + + #[tokio::test] + async fn invalidate_clears_both() { + let l1 = MemoryCache::new(); + let l2 = MemoryCache::new(); + let layered = MultiLayerCache::new(l1, l2); + layered.set("k", b"v".to_vec(), None).await.unwrap(); + layered.invalidate("k").await.unwrap(); + assert_eq!(layered.l1().get("k").await.unwrap(), None); + assert_eq!(layered.l2().get("k").await.unwrap(), None); + } +} diff --git a/crates/corex-cache/src/redis.rs b/crates/corex-cache/src/redis.rs new file mode 100644 index 0000000..f564f71 --- /dev/null +++ b/crates/corex-cache/src/redis.rs @@ -0,0 +1,124 @@ +//! A distributed (L2) cache backed by Redis / Valkey (feature `l2-redis`). +//! +//! Wraps a `redis` async connection (multiplexed). Values are stored as raw +//! Redis strings with an optional TTL (`SETEX` when a TTL is given). The +//! caller provides the connection; this type only issues cache commands. + +use std::time::Duration; + +use async_trait::async_trait; +use redis::aio::MultiplexedConnection; +use redis::{AsyncCommands, RedisError}; + +use crate::traits::{Cache, CacheError}; + +/// Map a Redis error onto a [`CacheError`]. +fn map_err(e: RedisError) -> CacheError { + CacheError::Io(e.to_string()) +} + +/// An L2 cache backed by a `redis` [`MultiplexedConnection`]. +/// +/// The connection is supplied by the caller; it is cheaply cloned (the +/// multiplexed connection is `Arc`-backed internally), so one pool can drive +/// both cache operations and other Redis usage. +#[derive(Clone)] +pub struct RedisCache { + conn: MultiplexedConnection, + /// Optional namespace prefix prepended to every key. + prefix: String, +} + +impl RedisCache { + /// Wraps an existing connection. + pub fn new(conn: MultiplexedConnection) -> Self { + Self::with_prefix(conn, String::new()) + } + + /// Wraps a connection and adds a namespace prefix to every key. + pub fn with_prefix(conn: MultiplexedConnection, prefix: String) -> Self { + Self { conn, prefix } + } + + fn key(&self, key: &str) -> String { + if self.prefix.is_empty() { + key.to_string() + } else { + format!("{}{}", self.prefix, key) + } + } +} + +#[async_trait] +impl Cache for RedisCache { + async fn get(&self, key: &str) -> Result>, CacheError> { + // `get::<_, Option>>` returns `None` for a missing key. + let mut c = self.conn.clone(); + let k = self.key(key); + let result: Result>, RedisError> = c.get(&k).await; + result.map_err(map_err) + } + + async fn set( + &self, + key: &str, + value: Vec, + ttl: Option, + ) -> Result<(), CacheError> { + let mut c = self.conn.clone(); + let k = self.key(key); + match ttl { + Some(ttl) => { + let secs = ttl.as_secs().max(1); + let result: Result<(), RedisError> = c.set_ex(&k, value, secs).await; + result.map_err(map_err) + } + None => { + let result: Result<(), RedisError> = c.set(&k, value).await; + result.map_err(map_err) + } + } + } + + async fn invalidate(&self, key: &str) -> Result<(), CacheError> { + let mut c = self.conn.clone(); + let k = self.key(key); + let result: Result = c.del(&k).await; + result.map(|_| ()).map_err(map_err) + } + + async fn clear(&self) -> Result<(), CacheError> { + // Deliberately does nothing: `FLUSHALL`/`FLUSHDB` are dangerous on a + // shared instance. Consumers should scope keys under a prefix and call + // `invalidate` for the keys they own. + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Integration test requiring a live Redis at `REDIS_URL` + /// (e.g. `redis://127.0.0.1:6379`). Run with: + /// `REDIS_URL=redis://127.0.0.1:6379 cargo test -p corex-cache --features l2-redis -- --ignored` . + #[tokio::test] + #[ignore = "requires a live Redis instance (REDIS_URL)"] + async fn set_get_roundtrip_live() { + let url = std::env::var("REDIS_URL").expect("set REDIS_URL"); + let client = redis::Client::open(url).expect("valid redis url"); + let conn = client + .get_multiplexed_tokio_connection() + .await + .expect("connect"); + let cache = RedisCache::with_prefix(conn, "corex_cache_test:".to_string()); + + cache + .set("k", b"v".to_vec(), Some(Duration::from_secs(3600))) + .await + .unwrap(); + assert_eq!(cache.get("k").await.unwrap(), Some(b"v".to_vec())); + cache.invalidate("k").await.unwrap(); + assert_eq!(cache.get("k").await.unwrap(), None); + } +} diff --git a/crates/corex-cache/src/traits.rs b/crates/corex-cache/src/traits.rs new file mode 100644 index 0000000..93df831 --- /dev/null +++ b/crates/corex-cache/src/traits.rs @@ -0,0 +1,77 @@ +//! The core [`Cache`] and [`KeyEncoder`] traits. + +use std::borrow::Cow; +use std::time::Duration; + +use async_trait::async_trait; + +/// Errors returned by cache operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CacheError { + /// The backend could not be reached (e.g. Redis connection lost). + Io(String), + /// A value could not be serialized / deserialized. + Serialization(String), + /// A key could not be encoded for the backend. + Key(String), +} + +impl std::fmt::Display for CacheError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(s) => write!(f, "cache io: {s}"), + Self::Serialization(s) => write!(f, "cache serialization: {s}"), + Self::Key(s) => write!(f, "cache key: {s}"), + } + } +} + +impl std::error::Error for CacheError {} + +/// A generic byte-oriented cache. +/// +/// Real caches operate on bytes or strings; typed convenience is layered on +/// top (see [`crate::memory::typed::TypedCache`], behind `cache-aside`). +/// Implementors control the value format. +#[async_trait] +pub trait Cache: Send + Sync { + /// Fetches a value by key. `None` indicates a miss. + async fn get(&self, key: &str) -> Result>, CacheError>; + /// Stores a value under `key`, optionally expiring after `ttl`. + async fn set(&self, key: &str, value: Vec, ttl: Option) + -> Result<(), CacheError>; + /// Removes a key. + async fn invalidate(&self, key: &str) -> Result<(), CacheError>; + /// Removes all entries. + async fn clear(&self) -> Result<(), CacheError>; +} + +/// Keys given to the byte-oriented [`Cache`] are `&str`, but concrete backends +/// may need richer keys. [`KeyEncoder`] turns typed keys into canonical strings. +pub trait KeyEncoder { + /// The "shape" of a key, e.g. `"user:{id}:profile"`. + fn encode>, R: std::fmt::Display>(parts: (C, R)) -> String; +} + +/// A blanket implementation that formats `{collection}:{id}`. +pub struct DefaultKeyEncoder; + +impl KeyEncoder for DefaultKeyEncoder { + fn encode>, R: std::fmt::Display>(parts: (C, R)) -> String { + format!("{}:{}", parts.0.into(), parts.1) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_key_encoder_formats() { + assert_eq!(DefaultKeyEncoder::encode(("user", 42)), "user:42"); + assert_eq!( + DefaultKeyEncoder::encode(("session", "abc-123")), + "session:abc-123" + ); + } +} diff --git a/crates/corex-config/Cargo.toml b/crates/corex-config/Cargo.toml new file mode 100644 index 0000000..6a731f8 --- /dev/null +++ b/crates/corex-config/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "corex-config" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +license = "MIT OR Apache-2.0" +repository = "https://github.com/asepharyana/mytheclipse" +homepage = "https://github.com/asepharyana/mytheclipse" +documentation = "https://docs.rs/corex-config" +authors = ["asepharyana "] +description = "Type-safe, dynamic configuration engine: load .env/YAML/JSON/TOML into typed structs with hot-reload and validation." +readme = "README.md" +keywords = ["config", "env", "yaml", "json", "hot-reload"] +categories = ["config", "development-tools"] + +[features] +default = ["env", "yaml", "toml", "hot-reload"] +# Load .env files + environment variables. +env = ["dep:dotenvy"] +# Parse structured files. JSON support (`.json`) is always available since +# `serde_json::Value` is also the loader's internal merge representation. +yaml = ["dep:serde_yaml"] +toml = ["dep:toml"] +# Watch config files and hot-reload. +hot-reload = ["dep:notify", "dep:tokio"] + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing = "0.1" + +# Optional loaders +dotenvy = { version = "0.15", optional = true } +serde_yaml = { version = "0.9", optional = true } +toml = { version = "0.8", optional = true } +# Hot-reload file watching +notify = { version = "6", optional = true } +tokio = { version = "1.53", features = ["sync", "rt", "time"], optional = true } + +[dev-dependencies] +tokio = { version = "1.53", features = ["full"] } +tempfile = "3" diff --git a/crates/corex-config/LICENSE-APACHE b/crates/corex-config/LICENSE-APACHE new file mode 100644 index 0000000..0da389e --- /dev/null +++ b/crates/corex-config/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 The corex Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/corex-config/LICENSE-MIT b/crates/corex-config/LICENSE-MIT new file mode 100644 index 0000000..687a34c --- /dev/null +++ b/crates/corex-config/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The corex Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/corex-config/README.md b/crates/corex-config/README.md new file mode 100644 index 0000000..abfa282 --- /dev/null +++ b/crates/corex-config/README.md @@ -0,0 +1,49 @@ +# corex-config + +Type-safe, dynamic configuration: load `.env`, YAML, JSON, or TOML directly +into a typed Rust struct, merge multiple sources with environment variables +taking priority, and optionally hot-reload when the source files change. + +## Features + +- `env` (default) — `.env` file loading via `dotenvy`. +- `yaml` / `toml` (default) — structured file parsing (`.json` is always available). +- `hot-reload` (default) — watch files and swap in a freshly reloaded value. + +## Usage + +```rust +use serde::Deserialize; +use corex_config::ConfigLoader; + +#[derive(Debug, Deserialize, Clone)] +struct AppConfig { + port: u16, + database_url: String, +} + +let config: AppConfig = ConfigLoader::new() + .merge_file("config.yaml".as_ref())? + .merge_env("APP") // APP_PORT, APP_DATABASE_URL override the file + .build()?; +``` + +### Hot-reload + +```rust +use corex_config::DynamicConfig; +# use serde::Deserialize; +# #[derive(Debug, Deserialize, Clone)] struct AppConfig { port: u16 } + +let cfg = DynamicConfig::::watch_files( + vec!["config.yaml".into()], + || corex_config::ConfigLoader::new().merge_file("config.yaml".as_ref())?.build(), +)?; + +let mut changes = cfg.subscribe(); +tokio::spawn(async move { + while changes.recv().await.is_ok() { + println!("config reloaded: {:?}", cfg.get()); + } +}); +``` diff --git a/crates/corex-config/src/dynamic.rs b/crates/corex-config/src/dynamic.rs new file mode 100644 index 0000000..cb5c59c --- /dev/null +++ b/crates/corex-config/src/dynamic.rs @@ -0,0 +1,191 @@ +//! Runtime configuration hot-reload (feature `hot-reload`). +//! +//! [`DynamicConfig`] holds a typed configuration value behind an `RwLock`, +//! optionally watching a set of source files with `notify` and re-running a +//! caller-supplied reload closure whenever they change. Subscribers can await +//! a [`tokio::sync::broadcast::Receiver`] to react to a successful reload. + +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, Instant}; + +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use tokio::sync::broadcast; + +use crate::{Config, ConfigError}; + +/// A hot-reloadable, thread-safe configuration handle. +pub struct DynamicConfig { + inner: Arc>, + tx: broadcast::Sender<()>, +} + +impl Clone for DynamicConfig { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + tx: self.tx.clone(), + } + } +} + +impl DynamicConfig { + /// Wraps an already-loaded value with no file watching. + pub fn new(initial: T) -> Self { + let (tx, _rx) = broadcast::channel(16); + Self { + inner: Arc::new(RwLock::new(initial)), + tx, + } + } + + /// Returns a clone of the current configuration snapshot. + pub fn get(&self) -> T { + self.inner + .read() + .expect("corex-config: RwLock poisoned") + .clone() + } + + /// Replaces the current value and notifies subscribers. + pub fn set(&self, new: T) { + *self.inner.write().expect("corex-config: RwLock poisoned") = new; + let _ = self.tx.send(()); + } + + /// Subscribes to change notifications (fired after every successful + /// [`DynamicConfig::set`] or file-triggered reload). + pub fn subscribe(&self) -> broadcast::Receiver<()> { + self.tx.subscribe() + } + + /// Loads the initial value via `reload`, then watches `paths` for + /// modifications and re-runs `reload` on each change (debounced), atomically + /// swapping in the result on success. A failed reload is logged via + /// `tracing::error!` and the previous value is retained. + /// + /// The underlying OS file watcher lives on a dedicated background thread + /// for the lifetime of the process; there is currently no explicit + /// "unwatch" — construct one `DynamicConfig` per watched file set. + pub fn watch_files(paths: Vec, reload: F) -> Result + where + F: Fn() -> Result + Send + Sync + 'static, + { + Self::watch_files_debounced(paths, reload, Duration::from_millis(50)) + } + + /// Same as [`DynamicConfig::watch_files`] with an explicit debounce + /// window (the minimum time between two applied reloads). + pub fn watch_files_debounced( + paths: Vec, + reload: F, + debounce: Duration, + ) -> Result + where + F: Fn() -> Result + Send + Sync + 'static, + { + let initial = reload()?; + let config = Self::new(initial); + let inner = Arc::clone(&config.inner); + let tx = config.tx.clone(); + + let (raw_tx, raw_rx) = std::sync::mpsc::channel(); + let mut watcher: RecommendedWatcher = notify::recommended_watcher(move |res| { + let _ = raw_tx.send(res); + }) + .map_err(|e| ConfigError::Watch(e.to_string()))?; + for path in &paths { + watcher + .watch(path, RecursiveMode::NonRecursive) + .map_err(|e| ConfigError::Watch(e.to_string()))?; + } + + std::thread::Builder::new() + .name("corex-config-watch".into()) + .spawn(move || { + // Keep the watcher alive for the life of this thread. + let _watcher = watcher; + let mut last_applied = Instant::now() - debounce; + for event in raw_rx { + if event.is_err() { + continue; + } + if last_applied.elapsed() < debounce { + continue; + } + match reload() { + Ok(new) => { + *inner.write().expect("corex-config: RwLock poisoned") = new; + let _ = tx.send(()); + last_applied = Instant::now(); + } + Err(e) => { + tracing::error!("corex-config: hot-reload failed: {e}"); + } + } + } + }) + .map_err(|e| ConfigError::Watch(e.to_string()))?; + + Ok(config) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ConfigLoader; + use serde::Deserialize; + use std::io::Write; + + #[derive(Debug, Deserialize, Clone, PartialEq)] + struct Cfg { + value: u32, + } + + #[test] + fn set_and_get_roundtrip() { + let cfg = DynamicConfig::new(Cfg { value: 1 }); + assert_eq!(cfg.get(), Cfg { value: 1 }); + cfg.set(Cfg { value: 2 }); + assert_eq!(cfg.get(), Cfg { value: 2 }); + } + + #[tokio::test] + async fn subscribe_receives_change_notification() { + let cfg = DynamicConfig::new(Cfg { value: 1 }); + let mut rx = cfg.subscribe(); + cfg.set(Cfg { value: 9 }); + rx.recv().await.expect("change notification"); + assert_eq!(cfg.get().value, 9); + } + + #[tokio::test] + async fn watch_files_reloads_on_change() { + let mut file = tempfile::Builder::new().suffix(".json").tempfile().unwrap(); + write!(file, r#"{{"value": 1}}"#).unwrap(); + let path = file.path().to_path_buf(); + + let reload_path = path.clone(); + let cfg = DynamicConfig::::watch_files_debounced( + vec![path.clone()], + move || { + ConfigLoader::new() + .merge_file(&reload_path) + .and_then(|l| l.build()) + }, + Duration::from_millis(10), + ) + .expect("watch setup"); + assert_eq!(cfg.get().value, 1); + + let mut rx = cfg.subscribe(); + // Rewrite the file to trigger a reload. + std::fs::write(&path, r#"{"value": 42}"#).unwrap(); + + // Wait (with a generous timeout) for the watcher thread to notice. + let result = tokio::time::timeout(Duration::from_secs(5), rx.recv()).await; + assert!(result.is_ok(), "expected a reload notification within 5s"); + assert_eq!(cfg.get().value, 42); + } +} diff --git a/crates/corex-config/src/error.rs b/crates/corex-config/src/error.rs new file mode 100644 index 0000000..74a57a8 --- /dev/null +++ b/crates/corex-config/src/error.rs @@ -0,0 +1,32 @@ +//! Shared error type for corex-config. + +/// Errors surfaced while loading or reloading configuration. +#[non_exhaustive] +#[derive(Debug)] +pub enum ConfigError { + /// An I/O error reading a config source (file not found, permissions, ...). + Io(String), + /// A source could not be parsed (malformed YAML/JSON/TOML). + Parse(String), + /// The merged configuration could not be deserialized into the target type. + Deserialize(String), + /// The requested file extension has no registered loader (feature not + /// enabled, or unsupported format). + UnsupportedFormat(String), + /// Hot-reload setup failed (e.g. the file watcher could not be installed). + Watch(String), +} + +impl std::fmt::Display for ConfigError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(s) => write!(f, "config io error: {s}"), + Self::Parse(s) => write!(f, "config parse error: {s}"), + Self::Deserialize(s) => write!(f, "config deserialize error: {s}"), + Self::UnsupportedFormat(s) => write!(f, "unsupported config format: {s}"), + Self::Watch(s) => write!(f, "config watch error: {s}"), + } + } +} + +impl std::error::Error for ConfigError {} diff --git a/crates/corex-config/src/lib.rs b/crates/corex-config/src/lib.rs new file mode 100644 index 0000000..3bdfb6d --- /dev/null +++ b/crates/corex-config/src/lib.rs @@ -0,0 +1,53 @@ +//! # corex-config +//! +//! A type-safe, dynamic configuration engine: load environment variables, +//! `.env` files, YAML, JSON, or TOML directly into a typed Rust struct, with +//! optional runtime hot-reload. +//! +//! - [`ConfigLoader`] merges any number of file/env sources into a single +//! typed value (files first, environment variables override). +//! - [`DynamicConfig`] wraps a loaded value behind an `RwLock`, optionally +//! watching its source files and swapping in a freshly reloaded value when +//! they change, broadcasting the change to subscribers. +//! +//! ## Example +//! +//! ```no_run +//! use serde::Deserialize; +//! use corex_config::ConfigLoader; +//! +//! #[derive(Debug, Deserialize, Clone)] +//! struct AppConfig { +//! port: u16, +//! database: DatabaseConfig, +//! } +//! #[derive(Debug, Deserialize, Clone)] +//! struct DatabaseConfig { +//! url: String, +//! } +//! +//! let config: AppConfig = ConfigLoader::new() +//! .merge_file("config.yaml".as_ref()) +//! .unwrap() +//! .merge_env("APP") +//! .build() +//! .unwrap(); +//! ``` + +pub mod error; +pub mod loader; + +#[cfg(feature = "hot-reload")] +pub mod dynamic; + +pub use error::ConfigError; +pub use loader::ConfigLoader; + +#[cfg(feature = "hot-reload")] +pub use dynamic::DynamicConfig; + +/// Marker trait for types loadable by [`ConfigLoader`]. +/// +/// Blanket-implemented for any `Deserialize + Send + Sync + 'static`. +pub trait Config: for<'de> serde::Deserialize<'de> + Send + Sync + 'static {} +impl Config for T where T: for<'de> serde::Deserialize<'de> + Send + Sync + 'static {} diff --git a/crates/corex-config/src/loader.rs b/crates/corex-config/src/loader.rs new file mode 100644 index 0000000..9ddd2ac --- /dev/null +++ b/crates/corex-config/src/loader.rs @@ -0,0 +1,348 @@ +//! Merges file and environment sources into a typed configuration value. + +use std::marker::PhantomData; +use std::path::Path; + +use serde_json::{Map, Value}; + +use crate::{Config, ConfigError}; + +/// Builds a typed configuration value from any number of sources. +/// +/// Sources are merged in the order they're added; later sources override +/// earlier ones at the leaf level (objects are merged recursively, not +/// replaced wholesale). The conventional order is: defaults, then files +/// (base -> environment-specific), then environment variables (highest +/// priority, for secrets/overrides). +pub struct ConfigLoader { + value: Value, + _marker: PhantomData T>, +} + +impl Default for ConfigLoader { + fn default() -> Self { + Self { + value: Value::Object(Map::new()), + _marker: PhantomData, + } + } +} + +impl ConfigLoader { + /// Builds an empty loader. + pub fn new() -> Self { + Self::default() + } + + /// Merges a JSON value directly (useful for defaults / tests). + pub fn merge_value(mut self, value: Value) -> Self { + deep_merge(&mut self.value, value); + self + } + + /// Reads `path`, parses it by extension (`.yaml`/`.yml`, `.json`, + /// `.toml`), and merges the result. + pub fn merge_file(mut self, path: &Path) -> Result { + let contents = std::fs::read_to_string(path) + .map_err(|e| ConfigError::Io(format!("{}: {e}", path.display())))?; + let parsed = parse_by_extension(path, &contents)?; + deep_merge(&mut self.value, parsed); + Ok(self) + } + + /// Loads a `.env`-style file into the process environment (does not merge + /// into the config value directly — call [`Self::merge_env`] afterward to + /// pick the variables up). + #[cfg(feature = "env")] + pub fn load_dotenv(self, path: &Path) -> Result { + dotenvy::from_path(path).map_err(|e| ConfigError::Io(e.to_string()))?; + Ok(self) + } + + /// Merges environment variables whose name starts with `prefix` (an + /// underscore is inserted between the prefix and the field name if not + /// already present). Nested fields use `__` as a separator, e.g. + /// `APP_DATABASE__URL` maps to `{ "database": { "url": ... } }`. + /// + /// Values are parsed as JSON scalars when possible (`true`/`false`, + /// integers, floats), otherwise kept as strings. + pub fn merge_env(mut self, prefix: &str) -> Self { + let collected = collect_env(prefix); + deep_merge(&mut self.value, Value::Object(collected)); + self + } + + /// Deserializes the merged value into `T`. + pub fn build(self) -> Result { + serde_json::from_value(self.value).map_err(|e| ConfigError::Deserialize(e.to_string())) + } + + /// Returns the current merged value without deserializing, for + /// inspection/debugging. + pub fn peek(&self) -> &Value { + &self.value + } +} + +/// Parses `contents` according to `path`'s extension. +fn parse_by_extension(path: &Path, contents: &str) -> Result { + let ext = path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default() + .to_ascii_lowercase(); + match ext.as_str() { + #[cfg(feature = "yaml")] + "yaml" | "yml" => serde_yaml::from_str(contents) + .map_err(|e| ConfigError::Parse(e.to_string())) + .map(yaml_to_json), + "json" => serde_json::from_str(contents).map_err(|e| ConfigError::Parse(e.to_string())), + #[cfg(feature = "toml")] + "toml" => { + let v: toml::Value = + toml::from_str(contents).map_err(|e| ConfigError::Parse(e.to_string()))?; + Ok(toml_to_json(v)) + } + other => Err(ConfigError::UnsupportedFormat(format!( + "no loader registered for `.{other}` (path: {})", + path.display() + ))), + } +} + +#[cfg(feature = "yaml")] +fn yaml_to_json(v: serde_yaml::Value) -> Value { + // Round-trip through serde_json for a uniform merge representation. + serde_json::to_value(v).unwrap_or(Value::Null) +} + +#[cfg(feature = "toml")] +fn toml_to_json(v: toml::Value) -> Value { + serde_json::to_value(v).unwrap_or(Value::Null) +} + +/// Deep-merges `overlay` into `base`; scalars and arrays in `overlay` replace +/// `base`, objects are merged key-by-key recursively. +fn deep_merge(base: &mut Value, overlay: Value) { + match (base, overlay) { + (Value::Object(base_map), Value::Object(overlay_map)) => { + for (k, v) in overlay_map { + match base_map.get_mut(&k) { + Some(existing) => deep_merge(existing, v), + None => { + base_map.insert(k, v); + } + } + } + } + (base_slot, overlay_value) => { + *base_slot = overlay_value; + } + } +} + +/// Collects environment variables with `prefix` into a nested JSON object. +/// +/// `PREFIX_FOO` -> `{"foo": ...}`; `PREFIX_FOO__BAR` -> `{"foo": {"bar": ...}}`. +fn collect_env(prefix: &str) -> Map { + let mut root = Map::new(); + let full_prefix = if prefix.ends_with('_') { + prefix.to_string() + } else { + format!("{prefix}_") + }; + for (key, raw_value) in std::env::vars() { + let Some(rest) = key.strip_prefix(&full_prefix) else { + continue; + }; + if rest.is_empty() { + continue; + } + let path: Vec = rest.split("__").map(|s| s.to_ascii_lowercase()).collect(); + insert_nested(&mut root, &path, coerce_scalar(&raw_value)); + } + root +} + +fn insert_nested(root: &mut Map, path: &[String], value: Value) { + if path.len() == 1 { + root.insert(path[0].clone(), value); + return; + } + let entry = root + .entry(path[0].clone()) + .or_insert_with(|| Value::Object(Map::new())); + if let Value::Object(nested) = entry { + insert_nested(nested, &path[1..], value); + } +} + +/// Parses `s` as a JSON scalar (`bool`/number) if possible, else keeps it as +/// a string. +fn coerce_scalar(s: &str) -> Value { + if let Ok(b) = s.parse::() { + return Value::Bool(b); + } + if let Ok(i) = s.parse::() { + return Value::Number(i.into()); + } + if let Ok(f) = s.parse::() { + if let Some(n) = serde_json::Number::from_f64(f) { + return Value::Number(n); + } + } + Value::String(s.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Deserialize; + + #[derive(Debug, Deserialize, PartialEq)] + struct Db { + url: String, + pool_size: u32, + } + + #[derive(Debug, Deserialize, PartialEq)] + struct AppConfig { + port: u16, + debug: bool, + database: Db, + } + + #[test] + fn merge_value_builds_typed_struct() { + let value = serde_json::json!({ + "port": 8080, + "debug": true, + "database": { "url": "postgres://x", "pool_size": 10 } + }); + let cfg: AppConfig = ConfigLoader::new().merge_value(value).build().unwrap(); + assert_eq!( + cfg, + AppConfig { + port: 8080, + debug: true, + database: Db { + url: "postgres://x".into(), + pool_size: 10 + } + } + ); + } + + #[test] + fn deep_merge_overrides_nested_leaf_only() { + let base = serde_json::json!({ "port": 8080, "debug": false, "database": { "url": "a", "pool_size": 5 } }); + let overlay = serde_json::json!({ "database": { "pool_size": 20 } }); + let cfg: AppConfig = ConfigLoader::new() + .merge_value(base) + .merge_value(overlay) + .build() + .unwrap(); + assert_eq!(cfg.database.pool_size, 20); + assert_eq!(cfg.database.url, "a"); // untouched sibling field preserved + assert_eq!(cfg.port, 8080); + } + + #[test] + fn env_merge_maps_nested_and_types() { + // Safe because this test owns unique env var names. + std::env::set_var("CFGTEST_PORT", "9090"); + std::env::set_var("CFGTEST_DEBUG", "true"); + std::env::set_var("CFGTEST_DATABASE__URL", "redis://y"); + std::env::set_var("CFGTEST_DATABASE__POOL_SIZE", "42"); + + let cfg: AppConfig = ConfigLoader::new().merge_env("CFGTEST").build().unwrap(); + assert_eq!(cfg.port, 9090); + assert!(cfg.debug); + assert_eq!(cfg.database.url, "redis://y"); + assert_eq!(cfg.database.pool_size, 42); + + std::env::remove_var("CFGTEST_PORT"); + std::env::remove_var("CFGTEST_DEBUG"); + std::env::remove_var("CFGTEST_DATABASE__URL"); + std::env::remove_var("CFGTEST_DATABASE__POOL_SIZE"); + } + + #[test] + fn env_overrides_file_value() { + std::env::set_var("CFGTEST2_PORT", "7000"); + let base = serde_json::json!({ "port": 1, "debug": false, "database": { "url": "a", "pool_size": 1 } }); + let cfg: AppConfig = ConfigLoader::new() + .merge_value(base) + .merge_env("CFGTEST2") + .build() + .unwrap(); + assert_eq!(cfg.port, 7000); + std::env::remove_var("CFGTEST2_PORT"); + } + + #[cfg(feature = "yaml")] + #[test] + fn merge_yaml_file() { + use std::io::Write; + let mut file = tempfile::Builder::new().suffix(".yaml").tempfile().unwrap(); + writeln!( + file, + "port: 3000\ndebug: false\ndatabase:\n url: sqlite://mem\n pool_size: 3\n" + ) + .unwrap(); + let cfg: AppConfig = ConfigLoader::new() + .merge_file(file.path()) + .unwrap() + .build() + .unwrap(); + assert_eq!(cfg.port, 3000); + assert_eq!(cfg.database.url, "sqlite://mem"); + } + + #[test] + fn merge_json_file() { + use std::io::Write; + let mut file = tempfile::Builder::new().suffix(".json").tempfile().unwrap(); + write!( + file, + r#"{{"port": 4000, "debug": true, "database": {{"url": "mysql://x", "pool_size": 7}}}}"# + ) + .unwrap(); + let cfg: AppConfig = ConfigLoader::new() + .merge_file(file.path()) + .unwrap() + .build() + .unwrap(); + assert_eq!(cfg.port, 4000); + assert_eq!(cfg.database.pool_size, 7); + } + + #[cfg(feature = "toml")] + #[test] + fn merge_toml_file() { + use std::io::Write; + let mut file = tempfile::Builder::new().suffix(".toml").tempfile().unwrap(); + writeln!( + file, + "port = 5000\ndebug = false\n\n[database]\nurl = \"file://local\"\npool_size = 2\n" + ) + .unwrap(); + let cfg: AppConfig = ConfigLoader::new() + .merge_file(file.path()) + .unwrap() + .build() + .unwrap(); + assert_eq!(cfg.port, 5000); + assert_eq!(cfg.database.pool_size, 2); + } + + #[test] + fn unsupported_extension_errors() { + let file = tempfile::Builder::new().suffix(".ini").tempfile().unwrap(); + std::fs::write(file.path(), "unused").unwrap(); + let result: Result = ConfigLoader::new() + .merge_file(file.path()) + .and_then(|l| l.build()); + assert!(matches!(result, Err(ConfigError::UnsupportedFormat(_)))); + } +} diff --git a/crates/corex-crypto/Cargo.toml b/crates/corex-crypto/Cargo.toml new file mode 100644 index 0000000..1f20aae --- /dev/null +++ b/crates/corex-crypto/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "corex-crypto" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +license = "MIT OR Apache-2.0" +repository = "https://github.com/asepharyana/mytheclipse" +homepage = "https://github.com/asepharyana/mytheclipse" +documentation = "https://docs.rs/corex-crypto" +authors = ["asepharyana "] +description = "Safe hashing, encryption, and token helpers (Argon2id, AES-256-GCM, JWT/Paseto) with key rotation support." +readme = "README.md" +keywords = ["crypto", "argon2", "aes-gcm", "jwt", "security"] +categories = ["cryptography", "authentication"] + +[features] +default = ["password", "encryption", "tokens"] +# Low-level primitives are always available (zero-cost). The feature flags +# pull in the backing SDK crates. +password = ["dep:password-hash", "dep:argon2"] +encryption = ["dep:aead", "dep:aes-gcm", "dep:rand_core", "dep:rand"] +tokens = ["encryption", "dep:serde", "dep:serde_json", "dep:base64", "dep:jsonwebtoken"] + +[dependencies] +tracing = "0.1" + +# Optional backends +argon2 = { version = "0.5", default-features = false, features = ["std"], optional = true } +password-hash = { version = "0.5", default-features = false, features = ["std"], optional = true } +aes-gcm = { version = "0.10", default-features = false, features = ["aes", "alloc"], optional = true } +aead = { version = "0.5", default-features = false, features = ["alloc"], optional = true } +jsonwebtoken = { version = "9", default-features = false, optional = true } +base64 = { version = "0.22", default-features = false, optional = true } +serde = { version = "1", optional = true, features = ["derive"] } +serde_json = { version = "1", optional = true } +rand = { version = "0.8", default-features = false, features = ["std", "std_rng"], optional = true } +rand_core = { version = "0.6", optional = true } diff --git a/crates/corex-crypto/LICENSE-APACHE b/crates/corex-crypto/LICENSE-APACHE new file mode 100644 index 0000000..0da389e --- /dev/null +++ b/crates/corex-crypto/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 The corex Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/corex-crypto/LICENSE-MIT b/crates/corex-crypto/LICENSE-MIT new file mode 100644 index 0000000..687a34c --- /dev/null +++ b/crates/corex-crypto/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The corex Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/corex-crypto/README.md b/crates/corex-crypto/README.md new file mode 100644 index 0000000..22c713a --- /dev/null +++ b/crates/corex-crypto/README.md @@ -0,0 +1,36 @@ +# corex-crypto + +Safe, one-line security helpers that are easy to get wrong when hand-rolled: + +- **Argon2id** password hashing & verification (PHC-encoded, RFC 9106-style). +- **AES-256-GCM** authenticated encryption with a fresh random nonce per op. +- **JWT** (HS256) token generation & validation. +- **Key rotation** via `KeyRing` — reads keep working with the previous key + during a rotation window. + +## Features + +- `password` (default) — Argon2id hashing. +- `encryption` (default) — AES-256-GCM. +- `tokens` (default) — JSON Web Tokens. + +Zero features enabled by default? No — all three are on, but each is cheap and +independent. + +## Usage + +```rust +use corex_crypto::{PasswordHasher, Encryptor, TokenSigner}; + +let hasher = PasswordHasher::new(); +let hash = hasher.hash("letmein").unwrap(); +assert!(hasher.verify(&hash, "letmein")); + +let enc = Encryptor::new(&[0u8; 32]); +let (nonce, ct) = enc.encrypt(b"secret"); +assert_eq!(enc.decrypt(&nonce, &ct).unwrap(), b"secret"); + +let signer = TokenSigner::new("my-secret"); +let token = signer.sign(&serde_json::json!({"sub":"u1"}), std::time::Duration::from_secs(3600)).unwrap(); +assert_eq!(signer.verify(&token).unwrap()["sub"], "u1"); +``` diff --git a/crates/corex-crypto/src/encryption.rs b/crates/corex-crypto/src/encryption.rs new file mode 100644 index 0000000..8b85394 --- /dev/null +++ b/crates/corex-crypto/src/encryption.rs @@ -0,0 +1,146 @@ +//! AES-256-GCM authenticated encryption with a random nonce per operation. +//! +//! The `nonce` is generated fresh for every [`Encryptor::encrypt`] call and +//! returned alongside the ciphertext so the caller can store/transmit it. The +//! caller must keep the plaintext length out of scope of concern; GCM provides +//! confidentiality and integrity. + +use aes_gcm::aead::{Aead, KeyInit}; +use aes_gcm::{Aes256Gcm, Nonce}; + +use crate::{KEY_LEN, NONCE_LEN}; + +/// A thin wrapper around AES-256-GCM providing a safe one-line encrypt/decrypt. +pub struct Encryptor { + cipher: Aes256Gcm, +} + +/// The failure mode of an AEAD operation. +#[derive(Debug, PartialEq, Eq)] +pub enum AeadError { + /// Decryption failed because the authentication tag did not match. + AuthenticationFailed, + /// Key or nonce material had an invalid length/format. + InvalidInput, +} + +impl std::fmt::Display for AeadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AuthenticationFailed => write!(f, "authentication failed"), + Self::InvalidInput => write!(f, "invalid input"), + } + } +} + +impl std::error::Error for AeadError {} + +impl Encryptor { + /// Builds a GCM encryptor from a 32-byte key. + /// + /// # Panics + /// + /// Panics if `key` is not exactly `KEY_LEN` (32) bytes. + pub fn new(key: &[u8]) -> Self { + assert_eq!(key.len(), KEY_LEN, "AES-256-GCM requires a 32-byte key"); + let mut kb = [0u8; KEY_LEN]; + kb.copy_from_slice(key); + Self { + cipher: Aes256Gcm::new((&kb).into()), + } + } + + /// Encrypts `plaintext`, returning `(nonce, ciphertext_with_tag)`. + /// + /// The nonce is 12 random bytes, unique per call. + pub fn encrypt(&self, plaintext: &[u8]) -> (Vec, Vec) { + let nonce_bytes = Self::random_nonce(); + let nonce = Nonce::from_slice(&nonce_bytes); + let ct = self + .cipher + .encrypt(nonce, plaintext) + .expect("AES-256-GCM encryption is infallible for valid input"); + (nonce_bytes.to_vec(), ct) + } + + /// Decrypts `nonce || ciphertext` produced by [`Encryptor::encrypt`]. + pub fn decrypt(&self, nonce: &[u8], ciphertext: &[u8]) -> Result, AeadError> { + if nonce.len() != NONCE_LEN { + return Err(AeadError::InvalidInput); + } + let nonce = Nonce::from_slice(nonce); + self.cipher + .decrypt(nonce, ciphertext) + .map_err(|_| AeadError::AuthenticationFailed) + } + + /// Deterministically encrypts with a caller-supplied nonce (for tests or + /// for deriving per-record nonces from a counter). Prefer [`encrypt`]. + /// + /// [`encrypt`]: Encryptor::encrypt + pub fn encrypt_with_nonce( + &self, + nonce: &[u8; NONCE_LEN], + plaintext: &[u8], + ) -> Result, AeadError> { + self.cipher + .encrypt(Nonce::from_slice(nonce), plaintext) + .map_err(|_| AeadError::InvalidInput) + } + + fn random_nonce() -> [u8; NONCE_LEN] { + let mut n = [0u8; NONCE_LEN]; + rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut n); + n + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn key() -> [u8; KEY_LEN] { + [0x42u8; KEY_LEN] + } + + #[test] + fn encrypt_decrypt_roundtrip() { + let e = Encryptor::new(&key()); + let (nonce, ct) = e.encrypt(b"classified briefcase"); + let plain = e.decrypt(&nonce, &ct).unwrap(); + assert_eq!(plain, b"classified briefcase"); + } + + #[test] + fn tampered_ciphertext_fails_auth() { + let e = Encryptor::new(&key()); + let (nonce, mut ct) = e.encrypt(b"tamper me"); + ct[0] ^= 0xff; + assert_eq!(e.decrypt(&nonce, &ct), Err(AeadError::AuthenticationFailed)); + } + + #[test] + fn wrong_key_fails_auth() { + let e = Encryptor::new(&key()); + let (nonce, ct) = e.encrypt(b"hi"); + let wrong = Encryptor::new(&[0x99u8; KEY_LEN]); + assert_eq!( + wrong.decrypt(&nonce, &ct), + Err(AeadError::AuthenticationFailed) + ); + } + + #[test] + fn nonce_is_random_per_call() { + let e = Encryptor::new(&key()); + let (n1, _) = e.encrypt(b"data"); + let (n2, _) = e.encrypt(b"data"); + assert_ne!(n1, n2); + } + + #[test] + fn wrong_key_length_panics() { + let result = std::panic::catch_unwind(|| Encryptor::new(&[0u8; 16])); + assert!(result.is_err()); + } +} diff --git a/crates/corex-crypto/src/key_ring.rs b/crates/corex-crypto/src/key_ring.rs new file mode 100644 index 0000000..e54d053 --- /dev/null +++ b/crates/corex-crypto/src/key_ring.rs @@ -0,0 +1,96 @@ +//! Key rotation support. +//! +//! [`KeyRing`] holds a "current" key plus a list of "previous" keys. Operations +//! that need to *decrypt* or *verify* (as opposed to sign/encrypt) try the +//! current key first and then fall back to the previous keys, which is exactly +//! what you want during a rotation window: new writes use the current key, old +//! data can still be read with the previous key. + +/// A generic ring of keys: one current plus any number of previous. +/// +/// `T` is typically `&[u8]` or a key handle. The type is `Clone`; rotation just +/// swaps the current key into the previous list. +#[derive(Debug, Clone)] +pub struct KeyRing { + current: T, + previous: Vec, +} + +impl KeyRing { + /// Builds a ring with a single current key. + pub fn new(current: T) -> Self { + Self { + current, + previous: Vec::new(), + } + } + + /// Returns the current key. + pub fn current(&self) -> &T { + &self.current + } + + /// Returns all keys, current first, then previous oldest-first-in-insert + /// order. + pub fn all_keys(&self) -> impl Iterator { + std::iter::once(&self.current).chain(self.previous.iter()) + } + + /// Rotates in a new key, demoting the old current key to `previous`. + /// + /// Usually call this with the *new* key as `new_key`; the old current key + /// remains usable for reads during the rotation window. + pub fn rotate(&mut self, new_key: T) { + let old = std::mem::replace(&mut self.current, new_key); + self.previous.push(old); + } + + /// The number of keys being tracked (current + previous). + pub fn size(&self) -> usize { + 1 + self.previous.len() + } +} + +impl KeyRing +where + T: PartialEq, +{ + /// Whether `key` is currently in the ring (current or previous). + pub fn contains(&self, key: &T) -> bool { + self.all_keys().any(|k| k == key) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rotate_preserves_previous() { + let mut ring = KeyRing::new([1u8; 32]); + assert_eq!(ring.current(), &[1u8; 32]); + assert_eq!(ring.size(), 1); + + ring.rotate([2u8; 32]); + assert_eq!(ring.current(), &[2u8; 32]); + assert_eq!(ring.size(), 2); + assert!(ring.contains(&[1u8; 32])); + assert!(ring.contains(&[2u8; 32])); + + ring.rotate([3u8; 32]); + assert_eq!(ring.size(), 3); + assert!(ring.contains(&[1u8; 32])); + assert!(ring.contains(&[2u8; 32])); + assert!(ring.contains(&[3u8; 32])); + } + + #[test] + fn all_keys_iterates_current_first() { + let mut ring = KeyRing::new("current"); + ring.rotate("prev1"); + ring.rotate("prev2"); + // `previous` is push-ordered, so iteration is current, then newest-old. + let keys: Vec<&str> = ring.all_keys().copied().collect(); + assert_eq!(keys, vec!["prev2", "current", "prev1"]); + } +} diff --git a/crates/corex-crypto/src/lib.rs b/crates/corex-crypto/src/lib.rs new file mode 100644 index 0000000..ea3ed44 --- /dev/null +++ b/crates/corex-crypto/src/lib.rs @@ -0,0 +1,96 @@ +//! # corex-crypto +//! +//! Low-level security helpers that are easy to get wrong when hand-rolled: +//! +//! - **Argon2id** password hashing & verification (`password` feature), with +//! RFC 9106-ish parameters. +//! - **AES-256-GCM** authenticated encryption with a fresh random nonce per +//! operation (`encryption` feature). +//! - **JWT** (HS256) / **Paseto** v4 local token generation & validation (`tokens` +//! feature). +//! - **Key rotation** via [`KeyRing`]: decryption/verification tries the current +//! key then a list of previous keys. +//! +//! Nothing in the crate owns long-lived key material; keys are passed in as +//! bytes/keys by the caller and the caller is responsible for storage. Each +//! primitive is small enough to reason about in one screen. +//! +//! ## Example +//! +//! ```no_run +//! use corex_crypto::{PasswordHasher, Encryptor, TokenSigner}; +//! +//! // Hash & verify a password. +//! let hasher = PasswordHasher::new(); +//! let hash = hasher.hash("hunter2").unwrap(); +//! assert!(hasher.verify(&hash, "hunter2")); +//! +//! // Encrypt & decrypt a blob. +//! let key = [0u8; 32]; +//! let enc = Encryptor::new(&key); +//! let (nonce, ct) = enc.encrypt(b"secret message"); +//! let plain = enc.decrypt(&nonce, &ct).unwrap(); +//! assert_eq!(plain, b"secret message"); +//! +//! // Sign & verify a JWT. +//! let signer = TokenSigner::new("super-secret-key"); +//! let token = signer +//! .sign(&serde_json::json!({ "sub": "u1" }), std::time::Duration::from_secs(3600)) +//! .unwrap(); +//! let claims = signer.verify(&token).unwrap(); +//! assert_eq!(claims["sub"], "u1"); +//! ``` + +pub mod key_ring; + +#[cfg(feature = "password")] +pub mod password; + +#[cfg(feature = "encryption")] +pub mod encryption; + +#[cfg(feature = "tokens")] +pub mod token; + +#[cfg(feature = "password")] +pub use password::PasswordHasher; + +#[cfg(feature = "encryption")] +pub use encryption::{AeadError, Encryptor}; + +#[cfg(feature = "tokens")] +pub use token::{Claims, TokenError, TokenSigner}; + +pub use key_ring::KeyRing; + +/// Errors returned across corex-crypto primitives. +#[non_exhaustive] +#[derive(Debug)] +pub enum CryptoError { + /// Password hashing or verification failed. + Password(String), + /// Authenticated encryption / decryption failed. + Encryption(String), + /// Token generation or validation failed. + Token(String), + /// Key material is invalid for the requested operation. + Key(String), +} + +impl std::fmt::Display for CryptoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Password(msg) => write!(f, "password error: {msg}"), + Self::Encryption(msg) => write!(f, "encryption error: {msg}"), + Self::Token(msg) => write!(f, "token error: {msg}"), + Self::Key(msg) => write!(f, "key error: {msg}"), + } + } +} + +impl std::error::Error for CryptoError {} + +/// The fixed AES-256-GCM nonce length (96 bits) in bytes. +pub const NONCE_LEN: usize = 12; +/// The fixed AES-256-GCM key length in bytes. +pub const KEY_LEN: usize = 32; diff --git a/crates/corex-crypto/src/password.rs b/crates/corex-crypto/src/password.rs new file mode 100644 index 0000000..951f449 --- /dev/null +++ b/crates/corex-crypto/src/password.rs @@ -0,0 +1,166 @@ +//! Argon2id password hashing (RFC 9106). +//! +//! Hashes are stored as PHC strings, so they carry their parameters inline and +//! can be verified even if the recommended parameters change over time. + +// Traits imported with `_` names so their methods resolve without colliding +// with the `PasswordHasher` struct defined below. +use argon2::password_hash::{PasswordHash, PasswordHasher as _, PasswordVerifier as _, SaltString}; +use argon2::Argon2; + +use crate::CryptoError; + +/// Default memory cost (KiB) — 64 MiB. +const DEFAULT_MEM: u32 = 64 * 1024; +/// Default time cost. +const DEFAULT_TIME: u32 = 3; +/// Default parallelism (lanes). +const DEFAULT_PAR: u32 = 1; + +/// An Argon2id password hasher with configurable parameters. +#[derive(Clone)] +pub struct PasswordHasher { + mem: u32, + time: u32, + parallelism: u32, +} + +impl Default for PasswordHasher { + fn default() -> Self { + Self { + mem: DEFAULT_MEM, + time: DEFAULT_TIME, + parallelism: DEFAULT_PAR, + } + } +} + +impl PasswordHasher { + /// Builds a hasher with RFC-9106-style defaults. + pub fn new() -> Self { + Self::default() + } + + /// Builds a hasher with custom Argon2 parameters. + pub fn with_params(mem: u32, time: u32, parallelism: u32) -> Self { + Self { + mem, + time, + parallelism, + } + } + + /// The configured memory cost in KiB. + pub fn memory_cost(&self) -> u32 { + self.mem + } + + /// Hashes `password` using Argon2id with a fresh random salt, returning a + /// PHC-encoded string (`$argon2id$v=19$m=...,t=...,p=...$salt$hash`). + pub fn hash(&self, password: &str) -> Result { + let salt = SaltString::generate(&mut rand::thread_rng()); + let argon2 = self.argon2(); + let hash = argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| CryptoError::Password(e.to_string()))?; + Ok(hash.to_string()) + } + + /// Verifies `password` against a previously computed PHC `hash`. + /// + /// Uses the parameters encoded in the hash (not our current defaults), so + /// older hashes with different parameters still verify. Returns `false` + /// on a mismatch or malformed hash. + pub fn verify(&self, hash: &str, password: &str) -> bool { + let parsed = match PasswordHash::new(hash) { + Ok(p) => p, + Err(_) => return false, + }; + // Reconstruct Argon2 parameters from the PHC-serialized params string. + let (mem, time, lanes) = match parse_params(parsed.params.as_str()) { + Some(v) => v, + None => (DEFAULT_MEM, DEFAULT_TIME, DEFAULT_PAR), + }; + let params = match argon2::Params::new(mem, time, lanes, None) { + Ok(p) => p, + Err(_) => return false, + }; + let version = parsed + .version + .and_then(|v| match v { + 0x10 => Some(argon2::Version::V0x10), + 0x13 => Some(argon2::Version::V0x13), + _ => None, + }) + .unwrap_or(argon2::Version::V0x13); + let algorithm = match &*parsed.algorithm { + "argon2d" => argon2::Algorithm::Argon2d, + "argon2i" => argon2::Algorithm::Argon2i, + _ => argon2::Algorithm::Argon2id, + }; + let verifier = Argon2::new(algorithm, version, params); + verifier + .verify_password(password.as_bytes(), &parsed) + .is_ok() + } + + fn argon2(&self) -> Argon2<'static> { + let params = argon2::Params::new(self.mem, self.time, self.parallelism, None) + .expect("valid argon2 parameters"); + Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params) + } +} + +/// Parses `m=65536,t=3,p=1` style params out of a PHC params string. +fn parse_params(params: &str) -> Option<(u32, u32, u32)> { + let mut m = None; + let mut t = None; + let mut p = None; + for part in params.split(',') { + let (k, v) = part.split_once('=')?; + let value = v.parse::().ok()?; + match k { + "m" => m = Some(value), + "t" => t = Some(value), + "p" => p = Some(value), + _ => {} + } + } + Some((m?, t?, p?)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hash_and_verify_roundtrip() { + let h = PasswordHasher::new(); + let encoded = h.hash("correct horse battery staple").unwrap(); + assert!(h.verify(&encoded, "correct horse battery staple")); + assert!(!h.verify(&encoded, "wrong password")); + } + + #[test] + fn different_salts_yield_different_hashes() { + let h = PasswordHasher::new(); + let a = h.hash("samepassword").unwrap(); + let b = h.hash("samepassword").unwrap(); + assert_ne!(a, b); + assert!(h.verify(&a, "samepassword")); + assert!(h.verify(&b, "samepassword")); + } + + #[test] + fn invalid_hash_verifies_false() { + let h = PasswordHasher::new(); + assert!(!h.verify("not-a-real-hash", "anything")); + } + + #[test] + fn hash_string_is_phc_formatted() { + let h = PasswordHasher::new(); + let encoded = h.hash("x").unwrap(); + assert!(encoded.starts_with("$argon2id$v=19$m=65536,t=3,p=1$")); + } +} diff --git a/crates/corex-crypto/src/token.rs b/crates/corex-crypto/src/token.rs new file mode 100644 index 0000000..dc06f06 --- /dev/null +++ b/crates/corex-crypto/src/token.rs @@ -0,0 +1,182 @@ +//! JWT (HS256) generation and validation. +//! +//! Uses `jsonwebtoken`. Claims are plain `serde_json::Value` so callers can +//! build arbitrary claim sets without a bespoke struct. + +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// The error produced by token sign/verify. +#[derive(Debug)] +pub enum TokenError { + /// The token or key was malformed. + Encoding(String), + /// The token failed validation (bad signature, expired, etc.). + Validation(String), +} + +impl std::fmt::Display for TokenError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Encoding(s) => write!(f, "token encoding: {s}"), + Self::Validation(s) => write!(f, "token validation: {s}"), + } + } +} + +impl std::error::Error for TokenError {} + +/// A JWT signing/verification helper using HS256. +#[derive(Clone)] +pub struct TokenSigner { + key: Vec, +} + +/// Convenience alias for a `serde_json::Value` claim set. +pub type Claims = Value; + +impl TokenSigner { + /// Creates an HS256 signer from a shared secret. + pub fn new(secret: &str) -> Self { + Self { + key: secret.as_bytes().to_vec(), + } + } + + /// Signs `claims` (plus an automated `exp` and `iat`) into a JWT string. + pub fn sign(&self, claims: &Value, ttl: std::time::Duration) -> Result { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + let header = jsonwebtoken::Header::new(jsonwebtoken::Algorithm::HS256); + let mut payload = claims.clone(); + if !payload.is_object() { + return Err(TokenError::Encoding("claims must be a JSON object".into())); + } + if payload.get("exp").is_none() { + payload["exp"] = Value::Number((now + ttl.as_secs()).into()); + } + if payload.get("iat").is_none() { + payload["iat"] = Value::Number(now.into()); + } + let token = jsonwebtoken::encode( + &header, + &payload, + &jsonwebtoken::EncodingKey::from_secret(&self.key), + ) + .map_err(|e| TokenError::Encoding(e.to_string()))?; + Ok(token) + } + + /// Verifies `token` and returns its decoded claims. + /// + /// The signature, `exp`, and `iat` are all validated. + pub fn verify(&self, token: &str) -> Result { + let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256); + validation.validate_exp = true; + // `iat` is validated implicitly (rejected if in the future beyond leeway). + let data = jsonwebtoken::decode::( + token, + &jsonwebtoken::DecodingKey::from_secret(&self.key), + &validation, + ) + .map_err(|e| TokenError::Validation(e.to_string()))?; + Ok(data.claims) + } + + /// Verifies a token and additionally checks the registered `sub` claim. + pub fn verify_subject(&self, token: &str, expected_subject: &str) -> Result<(), TokenError> { + let claims = self.verify(token)?; + match claims.get("sub").and_then(Value::as_str) { + Some(sub) if sub == expected_subject => Ok(()), + _ => Err(TokenError::Validation("subject mismatch".into())), + } + } +} + +/// The expiry claim helper used by [`TokenSigner`] (kept for symmetry). +#[derive(Debug, Serialize, Deserialize)] +pub struct RegisteredClaims { + pub iat: Option, + pub exp: Option, + pub sub: Option, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sign_verify_roundtrip() { + let s = TokenSigner::new("my secret"); + let token = s + .sign( + &serde_json::json!({ "sub": "user-1", "role": "admin" }), + std::time::Duration::from_secs(3600), + ) + .unwrap(); + let claims = s.verify(&token).unwrap(); + assert_eq!(claims["sub"], "user-1"); + assert_eq!(claims["role"], "admin"); + assert!(claims["exp"].is_number()); + assert!(claims["iat"].is_number()); + } + + #[test] + fn wrong_secret_fails() { + let a = TokenSigner::new("key-a"); + let b = TokenSigner::new("key-b"); + let token = a + .sign( + &serde_json::json!({ "sub": "x" }), + std::time::Duration::from_secs(100), + ) + .unwrap(); + assert!(b.verify(&token).is_err()); + } + + #[test] + fn tampered_token_fails() { + let a = TokenSigner::new("key-a"); + let token = a + .sign( + &serde_json::json!({ "sub": "x" }), + std::time::Duration::from_secs(100), + ) + .unwrap(); + let mut bytes = token.into_bytes(); + let last = bytes.len() - 1; + bytes[last] ^= 0x01; + let tampered = String::from_utf8(bytes).unwrap(); + assert!(a.verify(&tampered).is_err()); + } + + #[test] + fn expired_token_fails() { + let a = TokenSigner::new("key-a"); + // Explicitly past `exp` (1970); sign only fills it in if absent. + let token = a + .sign( + &serde_json::json!({ "sub": "x", "exp": 1000 }), + std::time::Duration::from_secs(3600), + ) + .unwrap(); + assert!(a.verify(&token).is_err()); + } + + #[test] + fn subject_check() { + let s = TokenSigner::new("s"); + let token = s + .sign( + &serde_json::json!({ "sub": "alice" }), + std::time::Duration::from_secs(100), + ) + .unwrap(); + assert!(s.verify_subject(&token, "alice").is_ok()); + assert!(s.verify_subject(&token, "bob").is_err()); + } +} diff --git a/crates/corex-event/Cargo.toml b/crates/corex-event/Cargo.toml new file mode 100644 index 0000000..cf9f9d3 --- /dev/null +++ b/crates/corex-event/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "corex-event" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +license = "MIT OR Apache-2.0" +repository = "https://github.com/asepharyana/mytheclipse" +homepage = "https://github.com/asepharyana/mytheclipse" +documentation = "https://docs.rs/corex-event" +authors = ["asepharyana "] +description = "Unified events & message bus abstraction: in-memory pub/sub dispatcher plus pluggable distributed broker backends (RabbitMQ, NATS)." +readme = "README.md" +keywords = ["events", "pubsub", "message-bus", "rabbitmq", "nats"] +categories = ["asynchronous", "network-programming"] + +[features] +default = ["mem"] +# In-memory dispatcher (tokio broadcast channel), always cheap. +mem = ["dep:tokio"] +# Distributed broker backends. +amqp = ["dep:lapin", "dep:futures-lite"] +nats = ["dep:async-nats", "dep:futures-util", "dep:bytes"] + +[dependencies] +tracing = "0.1" +# Required unconditionally: the core `EventBus`/`Subscription` traits (always +# compiled) use it. +async-trait = "0.1" +tokio = { version = "1.53", features = ["sync", "rt"], optional = true } +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# RabbitMQ (pure Rust AMQP 0.9.1 client). +lapin = { version = "2", optional = true } +futures-lite = { version = "2", optional = true } + +# NATS (pure Rust client). +async-nats = { version = "0.38", optional = true } +futures-util = { version = "0.3", optional = true } +bytes = { version = "1", optional = true } + +[dev-dependencies] +tokio = { version = "1.53", features = ["full"] } diff --git a/crates/corex-event/LICENSE-APACHE b/crates/corex-event/LICENSE-APACHE new file mode 100644 index 0000000..0da389e --- /dev/null +++ b/crates/corex-event/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 The corex Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/corex-event/LICENSE-MIT b/crates/corex-event/LICENSE-MIT new file mode 100644 index 0000000..687a34c --- /dev/null +++ b/crates/corex-event/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The corex Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/corex-event/README.md b/crates/corex-event/README.md new file mode 100644 index 0000000..dbf775e --- /dev/null +++ b/crates/corex-event/README.md @@ -0,0 +1,36 @@ +# corex-event + +A unified events & message bus abstraction so component-to-component (or +service-to-service) communication isn't locked to one transport. + +- **In-memory dispatcher** (default) — `tokio::sync::broadcast`-backed + pub/sub for communication inside a single process (monolith). +- **Distributed broker backends** — RabbitMQ (`amqp`) and NATS (`nats`) + behind the same `EventBus` trait, so swapping the backend doesn't change + handler code. +- **Typed convenience** — `TypedEventBus` layers JSON encoding on top. + +## Features + +- `mem` (default) — in-memory dispatcher. +- `amqp` — RabbitMQ via `lapin` (pure Rust AMQP 0.9.1 client). +- `nats` — NATS via `async-nats`. + +## Usage + +```rust +use corex_event::{InMemoryEventBus, TypedEventBus}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq)] +struct OrderCreated { id: u64 } + +let bus = TypedEventBus::new(InMemoryEventBus::default()); +let mut sub = bus.subscribe::("orders").await?; +bus.publish("orders", &OrderCreated { id: 42 }).await?; +let event = sub.recv().await?; +``` + +Swap `InMemoryEventBus::default()` for `AmqpEventBus::connect(url, "exchange").await?` +or `NatsEventBus::connect(url).await?` to move to a distributed broker without +touching handler code. diff --git a/crates/corex-event/src/amqp.rs b/crates/corex-event/src/amqp.rs new file mode 100644 index 0000000..62b1bc1 --- /dev/null +++ b/crates/corex-event/src/amqp.rs @@ -0,0 +1,163 @@ +//! A RabbitMQ-backed [`EventBus`] (feature `amqp`), via the pure-Rust `lapin` +//! AMQP 0.9.1 client. +//! +//! Topics map to routing keys on a single topic [`lapin::ExchangeKind::Topic`] +//! exchange (default name `corex.events`); each subscription declares its own +//! exclusive, auto-delete queue bound to that routing key, matching the +//! common "fanout via topic exchange" pattern. + +use async_trait::async_trait; +use futures_lite::StreamExt; +use lapin::options::{ + BasicAckOptions, BasicConsumeOptions, BasicPublishOptions, ExchangeDeclareOptions, + QueueBindOptions, QueueDeclareOptions, +}; +use lapin::types::FieldTable; +use lapin::{BasicProperties, Channel, Connection, ConnectionProperties, ExchangeKind}; + +use crate::traits::{EventBus, EventError, Subscription, SubscriptionInner}; + +fn map_err(e: lapin::Error) -> EventError { + EventError::Io(e.to_string()) +} + +/// An [`EventBus`] backed by a RabbitMQ topic exchange. +#[derive(Clone)] +pub struct AmqpEventBus { + channel: Channel, + exchange: String, +} + +impl AmqpEventBus { + /// Connects to `addr` (an AMQP URL, e.g. `amqp://guest:guest@localhost:5672/%2f`) + /// and declares the topic exchange `exchange`. + pub async fn connect(addr: &str, exchange: &str) -> Result { + let conn = Connection::connect(addr, ConnectionProperties::default()) + .await + .map_err(map_err)?; + let channel = conn.create_channel().await.map_err(map_err)?; + channel + .exchange_declare( + exchange, + ExchangeKind::Topic, + ExchangeDeclareOptions { + durable: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .map_err(map_err)?; + Ok(Self { + channel, + exchange: exchange.to_string(), + }) + } + + /// Wraps an already-open channel with a pre-declared exchange (advanced + /// use: sharing a channel/connection across multiple buses). + pub fn from_channel(channel: Channel, exchange: String) -> Self { + Self { channel, exchange } + } +} + +#[async_trait] +impl EventBus for AmqpEventBus { + async fn publish(&self, topic: &str, payload: Vec) -> Result<(), EventError> { + self.channel + .basic_publish( + &self.exchange, + topic, + BasicPublishOptions::default(), + &payload, + BasicProperties::default(), + ) + .await + .map_err(map_err)? + .await + .map_err(map_err)?; + Ok(()) + } + + async fn subscribe(&self, topic: &str) -> Result { + let queue = self + .channel + .queue_declare( + "", // server-generated name + QueueDeclareOptions { + exclusive: true, + auto_delete: true, + ..Default::default() + }, + FieldTable::default(), + ) + .await + .map_err(map_err)?; + self.channel + .queue_bind( + queue.name().as_str(), + &self.exchange, + topic, + QueueBindOptions::default(), + FieldTable::default(), + ) + .await + .map_err(map_err)?; + let consumer = self + .channel + .basic_consume( + queue.name().as_str(), + "corex-event-consumer", + BasicConsumeOptions::default(), + FieldTable::default(), + ) + .await + .map_err(map_err)?; + Ok(Subscription::new(Box::new(AmqpSubscription { consumer }))) + } +} + +struct AmqpSubscription { + consumer: lapin::Consumer, +} + +#[async_trait] +impl SubscriptionInner for AmqpSubscription { + async fn recv(&mut self) -> Result, EventError> { + match self.consumer.next().await { + Some(Ok(delivery)) => { + let data = delivery.data.clone(); + delivery + .ack(BasicAckOptions::default()) + .await + .map_err(map_err)?; + Ok(data) + } + Some(Err(e)) => Err(map_err(e)), + None => Err(EventError::Closed), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Requires a live RabbitMQ at `AMQP_URL` (e.g. + /// `amqp://guest:guest@127.0.0.1:5672/%2f`). Run with: + /// `AMQP_URL=... cargo test -p corex-event --features amqp -- --ignored`. + #[tokio::test] + #[ignore = "requires a live RabbitMQ instance (AMQP_URL)"] + async fn publish_subscribe_roundtrip_live() { + let url = std::env::var("AMQP_URL").expect("set AMQP_URL"); + let bus = AmqpEventBus::connect(&url, "corex_event_test") + .await + .unwrap(); + let mut sub = bus.subscribe("orders.created").await.unwrap(); + bus.publish("orders.created", b"hello".to_vec()) + .await + .unwrap(); + let msg = sub.recv().await.unwrap(); + assert_eq!(msg, b"hello"); + } +} diff --git a/crates/corex-event/src/lib.rs b/crates/corex-event/src/lib.rs new file mode 100644 index 0000000..0357079 --- /dev/null +++ b/crates/corex-event/src/lib.rs @@ -0,0 +1,60 @@ +//! # corex-event +//! +//! A unified events & message bus abstraction so component-to-component (or +//! service-to-service) communication doesn't get locked to one transport. +//! +//! - **In-memory dispatcher** ([`memory::InMemoryEventBus`], feature `mem`, +//! default): a `tokio::sync::broadcast`-backed pub/sub bus for +//! intra-process communication. +//! - **Distributed broker backends**: [`amqp::AmqpEventBus`] (RabbitMQ, +//! feature `amqp`) and [`nats::NatsEventBus`] (NATS, feature `nats`) behind +//! the same [`EventBus`] trait, so swapping the backend does not change +//! handler code. +//! - **Typed convenience**: [`typed::TypedEventBus`] layers JSON +//! (de)serialization on top of the byte-oriented [`EventBus`]. +//! +//! ## Example +//! +//! ``` +//! use corex_event::{EventBus, InMemoryEventBus, TypedEventBus}; +//! use serde::{Deserialize, Serialize}; +//! +//! #[derive(Debug, Serialize, Deserialize, PartialEq)] +//! struct OrderCreated { id: u64 } +//! +//! # #[tokio::main] async fn main() { +//! let bus = TypedEventBus::new(InMemoryEventBus::default()); +//! let mut sub = bus.subscribe::("orders").await.unwrap(); +//! bus.publish("orders", &OrderCreated { id: 42 }).await.unwrap(); +//! let event = sub.recv().await.unwrap(); +//! assert_eq!(event, OrderCreated { id: 42 }); +//! # } +//! ``` + +pub mod traits; + +#[cfg(feature = "mem")] +pub mod memory; + +#[cfg(feature = "mem")] +pub mod typed; + +#[cfg(feature = "amqp")] +pub mod amqp; + +#[cfg(feature = "nats")] +pub mod nats; + +pub use traits::{EventBus, EventError, Subscription}; + +#[cfg(feature = "mem")] +pub use memory::InMemoryEventBus; + +#[cfg(feature = "mem")] +pub use typed::{Event, TypedEventBus, TypedSubscription}; + +#[cfg(feature = "amqp")] +pub use amqp::AmqpEventBus; + +#[cfg(feature = "nats")] +pub use nats::NatsEventBus; diff --git a/crates/corex-event/src/memory.rs b/crates/corex-event/src/memory.rs new file mode 100644 index 0000000..1887def --- /dev/null +++ b/crates/corex-event/src/memory.rs @@ -0,0 +1,147 @@ +//! In-memory pub/sub dispatcher (feature `mem`), backed by +//! [`tokio::sync::broadcast`]. +//! +//! Suitable for communication between components inside a single process +//! (monolith). Each topic lazily gets its own broadcast channel on first use. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use tokio::sync::broadcast; + +use crate::traits::{EventBus, EventError, Subscription, SubscriptionInner}; + +const DEFAULT_CAPACITY: usize = 256; + +/// An in-process [`EventBus`] backed by a `tokio::sync::broadcast` channel per +/// topic. +#[derive(Clone)] +pub struct InMemoryEventBus { + channels: Arc>>>>, + capacity: usize, +} + +impl Default for InMemoryEventBus { + fn default() -> Self { + Self::new(DEFAULT_CAPACITY) + } +} + +impl InMemoryEventBus { + /// Builds a bus whose per-topic channels buffer up to `capacity` messages + /// for slow subscribers before older ones are dropped (lagged). + pub fn new(capacity: usize) -> Self { + Self { + channels: Arc::new(Mutex::new(HashMap::new())), + capacity, + } + } + + fn sender_for(&self, topic: &str) -> broadcast::Sender> { + let mut channels = self.channels.lock().unwrap(); + channels + .entry(topic.to_string()) + .or_insert_with(|| broadcast::channel(self.capacity).0) + .clone() + } + + /// The number of currently subscribed receivers for `topic` (0 if the + /// topic has never been touched or has no active subscribers). + pub fn subscriber_count(&self, topic: &str) -> usize { + self.channels + .lock() + .unwrap() + .get(topic) + .map(|tx| tx.receiver_count()) + .unwrap_or(0) + } +} + +#[async_trait] +impl EventBus for InMemoryEventBus { + async fn publish(&self, topic: &str, payload: Vec) -> Result<(), EventError> { + let tx = self.sender_for(topic); + // A `SendError` here just means there are currently no subscribers, + // which is not an error condition for a pub/sub bus. + let _ = tx.send(payload); + Ok(()) + } + + async fn subscribe(&self, topic: &str) -> Result { + let rx = self.sender_for(topic).subscribe(); + Ok(Subscription::new(Box::new(BroadcastSubscription(rx)))) + } +} + +struct BroadcastSubscription(broadcast::Receiver>); + +#[async_trait] +impl SubscriptionInner for BroadcastSubscription { + async fn recv(&mut self) -> Result, EventError> { + loop { + match self.0.recv().await { + Ok(payload) => return Ok(payload), + // A slow subscriber missed some messages; skip ahead rather + // than erroring, matching typical broadcast semantics. + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => return Err(EventError::Closed), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn publish_subscribe_roundtrip() { + let bus = InMemoryEventBus::default(); + let mut sub = bus.subscribe("orders").await.unwrap(); + bus.publish("orders", b"order-created".to_vec()) + .await + .unwrap(); + let msg = sub.recv().await.unwrap(); + assert_eq!(msg, b"order-created"); + } + + #[tokio::test] + async fn multiple_subscribers_all_receive() { + let bus = InMemoryEventBus::default(); + let mut sub_a = bus.subscribe("t").await.unwrap(); + let mut sub_b = bus.subscribe("t").await.unwrap(); + bus.publish("t", b"hello".to_vec()).await.unwrap(); + assert_eq!(sub_a.recv().await.unwrap(), b"hello"); + assert_eq!(sub_b.recv().await.unwrap(), b"hello"); + } + + #[tokio::test] + async fn publish_with_no_subscribers_is_ok() { + let bus = InMemoryEventBus::default(); + assert!(bus.publish("nobody-listening", b"x".to_vec()).await.is_ok()); + } + + #[tokio::test] + async fn different_topics_are_isolated() { + let bus = InMemoryEventBus::default(); + let mut sub = bus.subscribe("a").await.unwrap(); + bus.publish("b", b"for-b".to_vec()).await.unwrap(); + // Nothing arrives on `a`'s subscription; use a short timeout to prove it. + let result = tokio::time::timeout(std::time::Duration::from_millis(30), sub.recv()).await; + assert!( + result.is_err(), + "subscription on `a` should not receive `b`'s event" + ); + } + + #[tokio::test] + async fn subscriber_count_tracks_active_subscriptions() { + let bus = InMemoryEventBus::default(); + assert_eq!(bus.subscriber_count("t"), 0); + let sub = bus.subscribe("t").await.unwrap(); + assert_eq!(bus.subscriber_count("t"), 1); + drop(sub); + assert_eq!(bus.subscriber_count("t"), 0); + } +} diff --git a/crates/corex-event/src/nats.rs b/crates/corex-event/src/nats.rs new file mode 100644 index 0000000..dffe2ff --- /dev/null +++ b/crates/corex-event/src/nats.rs @@ -0,0 +1,95 @@ +//! A NATS-backed [`EventBus`] (feature `nats`), via the pure-Rust +//! `async-nats` client. +//! +//! Topics map directly onto NATS subjects. + +use async_trait::async_trait; +use futures_util::StreamExt; + +use crate::traits::{EventBus, EventError, Subscription, SubscriptionInner}; + +fn map_connect_err(e: async_nats::ConnectError) -> EventError { + EventError::Io(e.to_string()) +} + +fn map_publish_err(e: async_nats::PublishError) -> EventError { + EventError::Io(e.to_string()) +} + +fn map_subscribe_err(e: async_nats::SubscribeError) -> EventError { + EventError::Io(e.to_string()) +} + +/// An [`EventBus`] backed by a NATS client. +#[derive(Clone)] +pub struct NatsEventBus { + client: async_nats::Client, +} + +impl NatsEventBus { + /// Connects to a NATS server at `addr` (e.g. `nats://127.0.0.1:4222`). + pub async fn connect(addr: &str) -> Result { + let client = async_nats::connect(addr).await.map_err(map_connect_err)?; + Ok(Self { client }) + } + + /// Wraps an already-connected client. + pub fn from_client(client: async_nats::Client) -> Self { + Self { client } + } +} + +#[async_trait] +impl EventBus for NatsEventBus { + async fn publish(&self, topic: &str, payload: Vec) -> Result<(), EventError> { + self.client + .publish(topic.to_string(), payload.into()) + .await + .map_err(map_publish_err) + } + + async fn subscribe(&self, topic: &str) -> Result { + let subscriber = self + .client + .subscribe(topic.to_string()) + .await + .map_err(map_subscribe_err)?; + Ok(Subscription::new(Box::new(NatsSubscription { subscriber }))) + } +} + +struct NatsSubscription { + subscriber: async_nats::Subscriber, +} + +#[async_trait] +impl SubscriptionInner for NatsSubscription { + async fn recv(&mut self) -> Result, EventError> { + match self.subscriber.next().await { + Some(msg) => Ok(msg.payload.to_vec()), + None => Err(EventError::Closed), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Requires a live NATS server at `NATS_URL` (e.g. `nats://127.0.0.1:4222`). + /// Run with: `NATS_URL=... cargo test -p corex-event --features nats -- --ignored`. + #[tokio::test] + #[ignore = "requires a live NATS instance (NATS_URL)"] + async fn publish_subscribe_roundtrip_live() { + let url = std::env::var("NATS_URL").expect("set NATS_URL"); + let bus = NatsEventBus::connect(&url).await.unwrap(); + let mut sub = bus.subscribe("orders.created").await.unwrap(); + // Give the subscription a moment to register server-side. + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + bus.publish("orders.created", b"hello".to_vec()) + .await + .unwrap(); + let msg = sub.recv().await.unwrap(); + assert_eq!(msg, b"hello"); + } +} diff --git a/crates/corex-event/src/traits.rs b/crates/corex-event/src/traits.rs new file mode 100644 index 0000000..32d3bc3 --- /dev/null +++ b/crates/corex-event/src/traits.rs @@ -0,0 +1,71 @@ +//! The core [`EventBus`] trait and byte-oriented [`Subscription`] handle. + +use async_trait::async_trait; + +/// Errors returned by event bus operations. +#[derive(Debug)] +pub enum EventError { + /// The backend connection failed or was lost. + Io(String), + /// A value could not be serialized / deserialized. + Serialization(String), + /// The subscription's channel was closed (no more events will arrive). + Closed, +} + +impl std::fmt::Display for EventError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(s) => write!(f, "event bus io: {s}"), + Self::Serialization(s) => write!(f, "event serialization: {s}"), + Self::Closed => write!(f, "subscription closed"), + } + } +} + +impl std::error::Error for EventError {} + +/// A byte-oriented publish/subscribe bus. +/// +/// Implementations back this with an in-memory broadcast channel or a +/// distributed broker (RabbitMQ, NATS, ...). Topics are opaque strings; +/// backends may map them onto exchanges/subjects as appropriate. +#[async_trait] +pub trait EventBus: Send + Sync { + /// Publishes `payload` to `topic`. Whether publishing with zero + /// subscribers is an error is backend-defined (the in-memory backend + /// treats it as a no-op success). + async fn publish(&self, topic: &str, payload: Vec) -> Result<(), EventError>; + + /// Subscribes to `topic`, returning a handle to receive future messages. + async fn subscribe(&self, topic: &str) -> Result; +} + +/// A handle to receive messages from a subscribed topic. +/// +/// Wraps the backend-specific receiver behind one `recv` method so callers +/// don't need to know which [`EventBus`] implementation produced it. +pub struct Subscription { + inner: Box, +} + +impl Subscription { + /// Wraps a backend-specific receiver. + /// + /// Unused when no backend feature (`mem`/`amqp`/`nats`) is enabled. + #[allow(dead_code)] + pub(crate) fn new(inner: Box) -> Self { + Self { inner } + } + + /// Awaits the next message on this subscription. + pub async fn recv(&mut self) -> Result, EventError> { + self.inner.recv().await + } +} + +/// Object-safe receiver behind [`Subscription`]. +#[async_trait] +pub(crate) trait SubscriptionInner: Send { + async fn recv(&mut self) -> Result, EventError>; +} diff --git a/crates/corex-event/src/typed.rs b/crates/corex-event/src/typed.rs new file mode 100644 index 0000000..8b403ac --- /dev/null +++ b/crates/corex-event/src/typed.rs @@ -0,0 +1,103 @@ +//! JSON-typed convenience over the byte-oriented [`EventBus`]. + +use std::marker::PhantomData; +use std::sync::Arc; + +use serde::de::DeserializeOwned; +use serde::Serialize; + +use crate::traits::{EventBus, EventError, Subscription}; + +/// A marker trait for event payload types: anything JSON-(de)serializable. +pub trait Event: Serialize + DeserializeOwned + Send + Sync + 'static {} +impl Event for T where T: Serialize + DeserializeOwned + Send + Sync + 'static {} + +/// Wraps a byte-oriented [`EventBus`] with typed publish/subscribe using JSON +/// encoding. +#[derive(Clone)] +pub struct TypedEventBus { + inner: Arc, +} + +impl TypedEventBus { + /// Wraps `inner`. + pub fn new(inner: B) -> Self { + Self { + inner: Arc::new(inner), + } + } + + /// Serializes and publishes `event` to `topic`. + pub async fn publish(&self, topic: &str, event: &E) -> Result<(), EventError> { + let bytes = + serde_json::to_vec(event).map_err(|e| EventError::Serialization(e.to_string()))?; + self.inner.publish(topic, bytes).await + } + + /// Subscribes to `topic`, returning a [`TypedSubscription`]. + pub async fn subscribe( + &self, + topic: &str, + ) -> Result, EventError> { + let inner = self.inner.subscribe(topic).await?; + Ok(TypedSubscription { + inner, + _marker: PhantomData, + }) + } + + /// Returns the underlying byte-oriented bus. + pub fn inner(&self) -> &B { + &self.inner + } +} + +/// A typed view over a byte [`Subscription`], deserializing each message. +pub struct TypedSubscription { + inner: Subscription, + _marker: PhantomData E>, +} + +impl TypedSubscription { + /// Awaits and deserializes the next event. + pub async fn recv(&mut self) -> Result { + let bytes = self.inner.recv().await?; + serde_json::from_slice(&bytes).map_err(|e| EventError::Serialization(e.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::memory::InMemoryEventBus; + use serde::Deserialize; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct OrderCreated { + id: u64, + total_cents: u64, + } + + #[tokio::test] + async fn typed_publish_subscribe_roundtrip() { + let bus = TypedEventBus::new(InMemoryEventBus::default()); + let mut sub = bus.subscribe::("orders").await.unwrap(); + bus.publish( + "orders", + &OrderCreated { + id: 1, + total_cents: 4999, + }, + ) + .await + .unwrap(); + let event = sub.recv().await.unwrap(); + assert_eq!( + event, + OrderCreated { + id: 1, + total_cents: 4999 + } + ); + } +} diff --git a/crates/corex-storage/Cargo.toml b/crates/corex-storage/Cargo.toml new file mode 100644 index 0000000..cfd9e3b --- /dev/null +++ b/crates/corex-storage/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "corex-storage" +version = "0.1.0" +edition = "2021" +rust-version = "1.75" +license = "MIT OR Apache-2.0" +repository = "https://github.com/asepharyana/mytheclipse" +homepage = "https://github.com/asepharyana/mytheclipse" +documentation = "https://docs.rs/corex-storage" +authors = ["asepharyana "] +description = "Unified storage & file system abstraction: one driver interface over Local Disk, S3/MinIO, and Google Cloud Storage, with stream-based upload/download." +readme = "README.md" +keywords = ["storage", "s3", "gcs", "minio", "filesystem"] +categories = ["filesystem", "asynchronous"] + +[features] +default = ["local"] +# The core `StorageDriver` trait (always compiled) needs `tokio`'s io-util for +# `AsyncRead`/`ReadBuf`; `local` additionally needs `fs`/`rt`. +local = ["tokio/fs", "tokio/rt"] +# S3-compatible object storage (also covers MinIO via a custom endpoint). +s3 = ["dep:aws-sdk-s3", "dep:aws-config", "dep:aws-credential-types"] +# Google Cloud Storage. +gcs = ["dep:google-cloud-storage", "dep:google-cloud-auth"] + +[dependencies] +tracing = "0.1" +# Required unconditionally: the core `StorageDriver` trait uses both. +async-trait = "0.1" +tokio = { version = "1.53", features = ["io-util"] } +bytes = "1" + +# S3 / MinIO +aws-sdk-s3 = { version = "1", optional = true } +aws-config = { version = "1", optional = true, features = ["behavior-version-latest"] } +aws-credential-types = { version = "1", optional = true } + +# Google Cloud Storage +google-cloud-storage = { version = "0.24", optional = true } +google-cloud-auth = { version = "0.17", optional = true } + +[dev-dependencies] +tokio = { version = "1.53", features = ["full"] } +tempfile = "3" diff --git a/crates/corex-storage/LICENSE-APACHE b/crates/corex-storage/LICENSE-APACHE new file mode 100644 index 0000000..0da389e --- /dev/null +++ b/crates/corex-storage/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 The corex Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/corex-storage/LICENSE-MIT b/crates/corex-storage/LICENSE-MIT new file mode 100644 index 0000000..687a34c --- /dev/null +++ b/crates/corex-storage/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The corex Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/corex-storage/README.md b/crates/corex-storage/README.md new file mode 100644 index 0000000..45e7912 --- /dev/null +++ b/crates/corex-storage/README.md @@ -0,0 +1,26 @@ +# corex-storage + +A unified storage & file system abstraction so file handling isn't locked to +one physical location. + +- **Local disk** (default) — plain files under a root directory. +- **S3-compatible object storage** (`s3`) — Amazon S3, and any S3-compatible + endpoint (MinIO included) via a custom endpoint URL. +- **Google Cloud Storage** (`gcs`). + +All operations are stream-based, so uploading/downloading a huge object never +requires holding it entirely in memory. + +## Usage + +```rust +use corex_storage::{LocalFileStorage, StorageDriver, bytes_stream, read_to_vec}; + +let storage = LocalFileStorage::new("/var/data"); +storage.put("uploads/report.csv", bytes_stream(data)).await?; +let bytes = read_to_vec(storage.get("uploads/report.csv").await?).await?; +``` + +Swap `LocalFileStorage::new(path)` for `S3Storage::connect("bucket").await?` or +`GcsStorage::connect("bucket").await?` to move to a distributed backend +without touching call sites. diff --git a/crates/corex-storage/src/gcs.rs b/crates/corex-storage/src/gcs.rs new file mode 100644 index 0000000..d8fc57f --- /dev/null +++ b/crates/corex-storage/src/gcs.rs @@ -0,0 +1,165 @@ +//! A Google Cloud Storage [`StorageDriver`] (feature `gcs`), via +//! `google-cloud-storage`. +//! +//! [`get`](GcsStorage::get) streams the downloaded object; [`put`](GcsStorage::put) +//! currently buffers the input before uploading in a single request (the +//! `google-cloud-storage` crate's simple upload API takes an owned body). For +//! very large uploads prefer chunked application-level batching until this +//! crate grows resumable-upload support. + +use async_trait::async_trait; +use google_cloud_storage::client::{Client, ClientConfig}; +use google_cloud_storage::http::objects::delete::DeleteObjectRequest; +use google_cloud_storage::http::objects::download::Range; +use google_cloud_storage::http::objects::get::GetObjectRequest; +use google_cloud_storage::http::objects::upload::{Media, UploadObjectRequest, UploadType}; +use tokio::io::AsyncReadExt; + +use crate::traits::{ObjectMeta, ObjectStream, StorageDriver, StorageError}; + +fn map_err(e: E) -> StorageError { + StorageError::Io(e.to_string()) +} + +/// A [`StorageDriver`] backed by a Google Cloud Storage bucket. +#[derive(Clone)] +pub struct GcsStorage { + client: Client, + bucket: String, +} + +impl GcsStorage { + /// Connects using Application Default Credentials (`GOOGLE_APPLICATION_CREDENTIALS`, + /// workload identity, etc.). + pub async fn connect(bucket: impl Into) -> Result { + let config = ClientConfig::default().with_auth().await.map_err(map_err)?; + Ok(Self { + client: Client::new(config), + bucket: bucket.into(), + }) + } + + /// Wraps an already-configured client. + pub fn from_client(client: Client, bucket: impl Into) -> Self { + Self { + client, + bucket: bucket.into(), + } + } +} + +#[async_trait] +impl StorageDriver for GcsStorage { + async fn get(&self, path: &str) -> Result { + let bytes = self + .client + .download_object( + &GetObjectRequest { + bucket: self.bucket.clone(), + object: path.to_string(), + ..Default::default() + }, + &Range::default(), + ) + .await + .map_err(|e| { + if e.to_string().contains("404") { + StorageError::NotFound(path.to_string()) + } else { + map_err(e) + } + })?; + Ok(crate::traits::bytes_stream(bytes)) + } + + async fn put(&self, path: &str, mut data: ObjectStream) -> Result { + let mut buf = Vec::new(); + data.read_to_end(&mut buf) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + let len = buf.len() as u64; + let upload_type = UploadType::Simple(Media::new(path.to_string())); + self.client + .upload_object( + &UploadObjectRequest { + bucket: self.bucket.clone(), + ..Default::default() + }, + buf, + &upload_type, + ) + .await + .map_err(map_err)?; + Ok(len) + } + + async fn delete(&self, path: &str) -> Result<(), StorageError> { + self.client + .delete_object(&DeleteObjectRequest { + bucket: self.bucket.clone(), + object: path.to_string(), + ..Default::default() + }) + .await + .map_err(map_err) + } + + async fn exists(&self, path: &str) -> Result { + match self.stat(path).await { + Ok(_) => Ok(true), + Err(StorageError::NotFound(_)) => Ok(false), + Err(e) => Err(e), + } + } + + async fn stat(&self, path: &str) -> Result { + let obj = self + .client + .get_object(&GetObjectRequest { + bucket: self.bucket.clone(), + object: path.to_string(), + ..Default::default() + }) + .await + .map_err(|e| { + if e.to_string().contains("404") { + StorageError::NotFound(path.to_string()) + } else { + map_err(e) + } + })?; + Ok(ObjectMeta { + size: obj.size.max(0) as u64, + etag: Some(obj.etag), + last_modified: obj.updated.map(|t| t.into()), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::{bytes_stream, read_to_vec}; + + /// Requires a live GCS bucket with Application Default Credentials + /// configured, and `GCS_BUCKET` set. Run with: + /// `GCS_BUCKET=my-bucket cargo test -p corex-storage --features gcs -- --ignored`. + #[tokio::test] + #[ignore = "requires a live GCS bucket + Application Default Credentials"] + async fn put_get_roundtrip_live() { + let bucket = std::env::var("GCS_BUCKET").expect("set GCS_BUCKET"); + let storage = GcsStorage::connect(bucket).await.unwrap(); + storage + .put( + "corex-storage-test.txt", + bytes_stream(b"hello gcs".to_vec()), + ) + .await + .unwrap(); + let data = read_to_vec(storage.get("corex-storage-test.txt").await.unwrap()) + .await + .unwrap(); + assert_eq!(data, b"hello gcs"); + storage.delete("corex-storage-test.txt").await.unwrap(); + } +} diff --git a/crates/corex-storage/src/lib.rs b/crates/corex-storage/src/lib.rs new file mode 100644 index 0000000..502fc80 --- /dev/null +++ b/crates/corex-storage/src/lib.rs @@ -0,0 +1,51 @@ +//! # corex-storage +//! +//! A unified storage & file system abstraction so file handling doesn't get +//! locked to one physical storage location. +//! +//! - **Local disk** ([`local::LocalFileStorage`], feature `local`, default). +//! - **S3-compatible object storage** ([`s3::S3Storage`], feature `s3`) — +//! works with Amazon S3 and any S3-compatible endpoint, including MinIO +//! (pass a custom endpoint via [`s3::S3Storage::connect_with_endpoint`]). +//! - **Google Cloud Storage** ([`gcs::GcsStorage`], feature `gcs`). +//! +//! All operations are stream-based (an [`traits::ObjectStream`] is a boxed +//! [`tokio::io::AsyncRead`]), so uploading/downloading a huge object never +//! requires holding it entirely in memory. +//! +//! ## Example +//! +//! ``` +//! use corex_storage::{LocalFileStorage, StorageDriver, bytes_stream, read_to_vec}; +//! # #[tokio::main] async fn main() { +//! # let dir = tempfile::tempdir().unwrap(); +//! let storage = LocalFileStorage::new(dir.path()); +//! storage.put("hello.txt", bytes_stream(b"hi".to_vec())).await.unwrap(); +//! let data = read_to_vec(storage.get("hello.txt").await.unwrap()).await.unwrap(); +//! assert_eq!(data, b"hi"); +//! # } +//! ``` + +pub mod traits; + +#[cfg(feature = "local")] +pub mod local; + +#[cfg(feature = "s3")] +pub mod s3; + +#[cfg(feature = "gcs")] +pub mod gcs; + +pub use traits::{ + bytes_stream, read_to_vec, ObjectMeta, ObjectStream, StorageDriver, StorageError, +}; + +#[cfg(feature = "local")] +pub use local::LocalFileStorage; + +#[cfg(feature = "s3")] +pub use s3::S3Storage; + +#[cfg(feature = "gcs")] +pub use gcs::GcsStorage; diff --git a/crates/corex-storage/src/local.rs b/crates/corex-storage/src/local.rs new file mode 100644 index 0000000..5a4eb1f --- /dev/null +++ b/crates/corex-storage/src/local.rs @@ -0,0 +1,165 @@ +//! A local-disk [`StorageDriver`] (feature `local`, default). + +use std::path::{Component, Path, PathBuf}; + +use async_trait::async_trait; + +use crate::traits::{ObjectMeta, ObjectStream, StorageDriver, StorageError}; + +/// Stores objects as files under a root directory. +/// +/// Paths are always resolved relative to the configured root; `..` path +/// components are rejected to prevent escaping the root. +#[derive(Clone)] +pub struct LocalFileStorage { + root: PathBuf, +} + +impl LocalFileStorage { + /// Builds a driver rooted at `root`. The directory is not required to + /// exist yet; it's created lazily on first write. + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } + + fn resolve(&self, path: &str) -> Result { + let rel = Path::new(path.trim_start_matches('/')); + for component in rel.components() { + if matches!(component, Component::ParentDir | Component::Prefix(_)) { + return Err(StorageError::InvalidPath(path.to_string())); + } + } + Ok(self.root.join(rel)) + } +} + +#[async_trait] +impl StorageDriver for LocalFileStorage { + async fn get(&self, path: &str) -> Result { + let full = self.resolve(path)?; + let file = tokio::fs::File::open(&full).await.map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + StorageError::NotFound(path.to_string()) + } else { + StorageError::Io(e.to_string()) + } + })?; + Ok(Box::pin(file)) + } + + async fn put(&self, path: &str, mut data: ObjectStream) -> Result { + let full = self.resolve(path)?; + if let Some(parent) = full.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + } + let mut file = tokio::fs::File::create(&full) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + let written = tokio::io::copy(&mut data, &mut file) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + Ok(written) + } + + async fn delete(&self, path: &str) -> Result<(), StorageError> { + let full = self.resolve(path)?; + match tokio::fs::remove_file(&full).await { + Ok(()) => Ok(()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => { + Err(StorageError::NotFound(path.to_string())) + } + Err(e) => Err(StorageError::Io(e.to_string())), + } + } + + async fn exists(&self, path: &str) -> Result { + let full = self.resolve(path)?; + Ok(tokio::fs::metadata(&full).await.is_ok()) + } + + async fn stat(&self, path: &str) -> Result { + let full = self.resolve(path)?; + let meta = tokio::fs::metadata(&full).await.map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + StorageError::NotFound(path.to_string()) + } else { + StorageError::Io(e.to_string()) + } + })?; + Ok(ObjectMeta { + size: meta.len(), + etag: None, + last_modified: meta.modified().ok(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::{bytes_stream, read_to_vec}; + + fn driver() -> (LocalFileStorage, tempfile::TempDir) { + let dir = tempfile::tempdir().unwrap(); + (LocalFileStorage::new(dir.path()), dir) + } + + #[tokio::test] + async fn put_get_roundtrip() { + let (storage, _dir) = driver(); + let written = storage + .put("a/b/file.txt", bytes_stream(b"hello world".to_vec())) + .await + .unwrap(); + assert_eq!(written, 11); + let data = read_to_vec(storage.get("a/b/file.txt").await.unwrap()) + .await + .unwrap(); + assert_eq!(data, b"hello world"); + } + + #[tokio::test] + async fn get_missing_returns_not_found() { + let (storage, _dir) = driver(); + // `ObjectStream` isn't `Debug`, so match rather than `unwrap_err()`. + match storage.get("missing.txt").await { + Err(StorageError::NotFound(_)) => {} + other => panic!("expected NotFound, got {:?}", other.is_ok()), + } + } + + #[tokio::test] + async fn delete_removes_object() { + let (storage, _dir) = driver(); + storage + .put("x.txt", bytes_stream(b"x".to_vec())) + .await + .unwrap(); + assert!(storage.exists("x.txt").await.unwrap()); + storage.delete("x.txt").await.unwrap(); + assert!(!storage.exists("x.txt").await.unwrap()); + } + + #[tokio::test] + async fn stat_reports_size() { + let (storage, _dir) = driver(); + storage + .put("s.bin", bytes_stream(vec![0u8; 1024])) + .await + .unwrap(); + let meta = storage.stat("s.bin").await.unwrap(); + assert_eq!(meta.size, 1024); + } + + #[tokio::test] + async fn path_traversal_is_rejected() { + let (storage, _dir) = driver(); + let err = storage + .put("../escape.txt", bytes_stream(b"x".to_vec())) + .await + .unwrap_err(); + assert!(matches!(err, StorageError::InvalidPath(_))); + } +} diff --git a/crates/corex-storage/src/s3.rs b/crates/corex-storage/src/s3.rs new file mode 100644 index 0000000..3793456 --- /dev/null +++ b/crates/corex-storage/src/s3.rs @@ -0,0 +1,334 @@ +//! An S3-compatible [`StorageDriver`] (feature `s3`), via `aws-sdk-s3`. +//! +//! Works against Amazon S3 or any S3-compatible endpoint — pass a custom +//! endpoint via [`S3Storage::connect_with_endpoint`] to target MinIO or +//! another compatible service. +//! +//! [`put`](S3Storage::put) streams the input in fixed-size chunks through +//! S3's multipart upload API rather than buffering the whole object in +//! memory, so large uploads stay within a bounded memory footprint. + +use async_trait::async_trait; +use aws_sdk_s3::config::{Credentials, Region}; +use aws_sdk_s3::primitives::ByteStream as AwsByteStream; +use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart}; +use aws_sdk_s3::Client; +use tokio::io::AsyncReadExt; + +use crate::traits::{ObjectMeta, ObjectStream, StorageDriver, StorageError}; + +/// S3's minimum multipart part size (5 MiB), except for the final part. +const MULTIPART_CHUNK_SIZE: usize = 8 * 1024 * 1024; + +fn map_sdk_err(e: E) -> StorageError { + StorageError::Io(format!("{e:?}")) +} + +/// A [`StorageDriver`] backed by an S3 (or S3-compatible) bucket. +#[derive(Clone)] +pub struct S3Storage { + client: Client, + bucket: String, +} + +impl S3Storage { + /// Connects using the ambient AWS environment/credential chain + /// (`AWS_ACCESS_KEY_ID`, IAM role, profile, etc.) against real S3. + pub async fn connect(bucket: impl Into) -> Self { + let config = aws_config::load_from_env().await; + let client = Client::new(&config); + Self { + client, + bucket: bucket.into(), + } + } + + /// Connects to a custom S3-compatible endpoint (e.g. MinIO) with static + /// credentials and path-style addressing. + pub async fn connect_with_endpoint( + bucket: impl Into, + endpoint: &str, + region: &str, + access_key: &str, + secret_key: &str, + ) -> Self { + let credentials = Credentials::new(access_key, secret_key, None, None, "corex-storage"); + let config = aws_sdk_s3::Config::builder() + .region(Region::new(region.to_string())) + .endpoint_url(endpoint) + .credentials_provider(credentials) + .force_path_style(true) + .behavior_version(aws_sdk_s3::config::BehaviorVersion::latest()) + .build(); + Self { + client: Client::from_conf(config), + bucket: bucket.into(), + } + } + + /// Wraps an already-configured client (advanced use: sharing a client + /// across multiple `S3Storage` instances pointed at different buckets). + pub fn from_client(client: Client, bucket: impl Into) -> Self { + Self { + client, + bucket: bucket.into(), + } + } +} + +#[async_trait] +impl StorageDriver for S3Storage { + async fn get(&self, path: &str) -> Result { + let resp = self + .client + .get_object() + .bucket(&self.bucket) + .key(path) + .send() + .await + .map_err(|e| { + if is_not_found(&e) { + StorageError::NotFound(path.to_string()) + } else { + map_sdk_err(e) + } + })?; + Ok(Box::pin(resp.body.into_async_read())) + } + + async fn put(&self, path: &str, mut data: ObjectStream) -> Result { + // Read the first chunk to decide between a simple `PutObject` (small + // objects) and a streamed multipart upload (anything larger). + let mut first_chunk = vec![0u8; MULTIPART_CHUNK_SIZE]; + let mut filled = 0usize; + while filled < first_chunk.len() { + let n = data + .read(&mut first_chunk[filled..]) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + if n == 0 { + break; + } + filled += n; + } + first_chunk.truncate(filled); + + if filled < MULTIPART_CHUNK_SIZE { + // The whole object fit in one chunk: a single PutObject suffices. + let len = first_chunk.len() as u64; + self.client + .put_object() + .bucket(&self.bucket) + .key(path) + .body(AwsByteStream::from(first_chunk)) + .send() + .await + .map_err(map_sdk_err)?; + return Ok(len); + } + + // Larger objects: stream the rest through multipart upload. + let create = self + .client + .create_multipart_upload() + .bucket(&self.bucket) + .key(path) + .send() + .await + .map_err(map_sdk_err)?; + let upload_id = create + .upload_id() + .ok_or_else(|| StorageError::Io("S3 did not return an upload id".to_string()))?; + + let result = upload_parts( + &self.client, + &self.bucket, + path, + upload_id, + first_chunk, + &mut data, + ) + .await; + + match result { + Ok((total, parts)) => { + self.client + .complete_multipart_upload() + .bucket(&self.bucket) + .key(path) + .upload_id(upload_id) + .multipart_upload( + CompletedMultipartUpload::builder() + .set_parts(Some(parts)) + .build(), + ) + .send() + .await + .map_err(map_sdk_err)?; + Ok(total) + } + Err(e) => { + let _ = self + .client + .abort_multipart_upload() + .bucket(&self.bucket) + .key(path) + .upload_id(upload_id) + .send() + .await; + Err(e) + } + } + } + + async fn delete(&self, path: &str) -> Result<(), StorageError> { + self.client + .delete_object() + .bucket(&self.bucket) + .key(path) + .send() + .await + .map_err(map_sdk_err)?; + Ok(()) + } + + async fn exists(&self, path: &str) -> Result { + match self + .client + .head_object() + .bucket(&self.bucket) + .key(path) + .send() + .await + { + Ok(_) => Ok(true), + Err(e) if is_not_found(&e) => Ok(false), + Err(e) => Err(map_sdk_err(e)), + } + } + + async fn stat(&self, path: &str) -> Result { + let resp = self + .client + .head_object() + .bucket(&self.bucket) + .key(path) + .send() + .await + .map_err(|e| { + if is_not_found(&e) { + StorageError::NotFound(path.to_string()) + } else { + map_sdk_err(e) + } + })?; + Ok(ObjectMeta { + size: resp.content_length().unwrap_or(0).max(0) as u64, + etag: resp.e_tag().map(|s| s.to_string()), + last_modified: resp + .last_modified() + .and_then(|d| d.to_owned().try_into().ok()), + }) + } +} + +/// Uploads `first_chunk` as part 1, then continues reading `data` in +/// [`MULTIPART_CHUNK_SIZE`] chunks until exhausted, uploading each as a part. +async fn upload_parts( + client: &Client, + bucket: &str, + key: &str, + upload_id: &str, + first_chunk: Vec, + data: &mut ObjectStream, +) -> Result<(u64, Vec), StorageError> { + let mut total = 0u64; + let mut parts = Vec::new(); + let mut part_number = 1i32; + let mut chunk = first_chunk; + + loop { + total += chunk.len() as u64; + let resp = client + .upload_part() + .bucket(bucket) + .key(key) + .upload_id(upload_id) + .part_number(part_number) + .body(AwsByteStream::from(chunk)) + .send() + .await + .map_err(map_sdk_err)?; + parts.push( + CompletedPart::builder() + .e_tag(resp.e_tag().unwrap_or_default()) + .part_number(part_number) + .build(), + ); + part_number += 1; + + let mut next = vec![0u8; MULTIPART_CHUNK_SIZE]; + let mut filled = 0usize; + while filled < next.len() { + let n = data + .read(&mut next[filled..]) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + if n == 0 { + break; + } + filled += n; + } + next.truncate(filled); + if next.is_empty() { + break; + } + chunk = next; + } + + Ok((total, parts)) +} + +/// Best-effort detection of a "not found" S3 error across the SDK's error +/// variants (`GetObject`/`HeadObject` surface this differently). +fn is_not_found(e: &E) -> bool { + format!("{e:?}").contains("NotFound") || format!("{e:?}").contains("404") +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::traits::{bytes_stream, read_to_vec}; + + /// Requires a live S3-compatible endpoint (e.g. MinIO) configured via + /// `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`. Run with: + /// `S3_ENDPOINT=http://127.0.0.1:9000 S3_BUCKET=test S3_ACCESS_KEY=... S3_SECRET_KEY=... \ + /// cargo test -p corex-storage --features s3 -- --ignored`. + #[tokio::test] + #[ignore = "requires a live S3-compatible endpoint (S3_ENDPOINT, S3_BUCKET, ...)"] + async fn put_get_roundtrip_live() { + let endpoint = std::env::var("S3_ENDPOINT").expect("set S3_ENDPOINT"); + let bucket = std::env::var("S3_BUCKET").expect("set S3_BUCKET"); + let access_key = std::env::var("S3_ACCESS_KEY").expect("set S3_ACCESS_KEY"); + let secret_key = std::env::var("S3_SECRET_KEY").expect("set S3_SECRET_KEY"); + + let storage = S3Storage::connect_with_endpoint( + bucket, + &endpoint, + "us-east-1", + &access_key, + &secret_key, + ) + .await; + + storage + .put("corex-storage-test.txt", bytes_stream(b"hello s3".to_vec())) + .await + .unwrap(); + let data = read_to_vec(storage.get("corex-storage-test.txt").await.unwrap()) + .await + .unwrap(); + assert_eq!(data, b"hello s3"); + storage.delete("corex-storage-test.txt").await.unwrap(); + } +} diff --git a/crates/corex-storage/src/traits.rs b/crates/corex-storage/src/traits.rs new file mode 100644 index 0000000..33d9c4a --- /dev/null +++ b/crates/corex-storage/src/traits.rs @@ -0,0 +1,109 @@ +//! The core [`StorageDriver`] trait and supporting types. + +use std::pin::Pin; +use std::task::{Context, Poll}; +use std::time::SystemTime; + +use async_trait::async_trait; +use tokio::io::{AsyncRead, ReadBuf}; + +/// Errors returned by storage operations. +#[derive(Debug)] +pub enum StorageError { + /// The object does not exist. + NotFound(String), + /// The path was rejected (e.g. attempted directory traversal). + InvalidPath(String), + /// An I/O or backend transport error occurred. + Io(String), +} + +impl std::fmt::Display for StorageError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound(p) => write!(f, "object not found: {p}"), + Self::InvalidPath(p) => write!(f, "invalid path: {p}"), + Self::Io(s) => write!(f, "storage io error: {s}"), + } + } +} + +impl std::error::Error for StorageError {} + +/// A boxed, unpin, streaming byte source used for both `get` results and +/// `put` input, so large objects never need to be buffered in memory whole. +pub type ObjectStream = Pin>; + +/// Metadata about a stored object. +#[derive(Debug, Clone)] +pub struct ObjectMeta { + /// Size in bytes. + pub size: u64, + /// A backend-provided content hash/version tag, if available. + pub etag: Option, + /// Last-modified timestamp, if available. + pub last_modified: Option, +} + +/// A unified interface for storing, reading, and deleting files regardless of +/// the physical backend (local disk, S3/MinIO, Google Cloud Storage, ...). +/// +/// All operations are stream-based: [`StorageDriver::put`] takes an +/// [`ObjectStream`] and [`StorageDriver::get`] returns one, so a caller +/// forwarding a large upload/download never has to hold the whole object in +/// memory. +#[async_trait] +pub trait StorageDriver: Send + Sync { + /// Opens `path` for streaming read. + async fn get(&self, path: &str) -> Result; + + /// Writes `data` to `path`, returning the number of bytes written. + async fn put(&self, path: &str, data: ObjectStream) -> Result; + + /// Removes the object at `path`. + async fn delete(&self, path: &str) -> Result<(), StorageError>; + + /// Whether an object exists at `path`. + async fn exists(&self, path: &str) -> Result; + + /// Fetches metadata about the object at `path` without downloading it. + async fn stat(&self, path: &str) -> Result; +} + +/// A minimal, dependency-free in-memory [`AsyncRead`] over an owned buffer. +struct VecCursor { + data: Vec, + pos: usize, +} + +impl AsyncRead for VecCursor { + fn poll_read( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let remaining = &self.data[self.pos..]; + let n = remaining.len().min(buf.remaining()); + buf.put_slice(&remaining[..n]); + self.pos += n; + Poll::Ready(Ok(())) + } +} + +/// Convenience: wraps an owned `Vec` as an [`ObjectStream`] for +/// [`StorageDriver::put`]. +pub fn bytes_stream(data: Vec) -> ObjectStream { + Box::pin(VecCursor { data, pos: 0 }) +} + +/// Convenience: drains an [`ObjectStream`] into an owned `Vec` (defeats +/// the point of streaming for huge objects — intended for tests/small files). +pub async fn read_to_vec(mut stream: ObjectStream) -> Result, StorageError> { + use tokio::io::AsyncReadExt; + let mut buf = Vec::new(); + stream + .read_to_end(&mut buf) + .await + .map_err(|e| StorageError::Io(e.to_string()))?; + Ok(buf) +} diff --git a/CHANGELOG.md b/crates/mytheclipse/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to crates/mytheclipse/CHANGELOG.md diff --git a/crates/mytheclipse/Cargo.lock b/crates/mytheclipse/Cargo.lock new file mode 100644 index 0000000..6cf068d --- /dev/null +++ b/crates/mytheclipse/Cargo.lock @@ -0,0 +1,479 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crossbeam-deque" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "mytheclipse" +version = "1.0.0" +dependencies = [ + "num_cpus", + "rand", + "rayon", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/crates/mytheclipse/Cargo.toml b/crates/mytheclipse/Cargo.toml new file mode 100644 index 0000000..c4b5913 --- /dev/null +++ b/crates/mytheclipse/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "mytheclipse" +version = "1.0.0" +edition = "2021" +rust-version = "1.75" +license = "MIT OR Apache-2.0" +repository = "https://github.com/asepharyana/mytheclipse" +homepage = "https://github.com/asepharyana/mytheclipse" +documentation = "https://docs.rs/mytheclipse" +authors = ["asepharyana "] +description = "Resource-aware abstractions for async I/O, heavy compute, background queue management, resiliency, traffic control, lifecycle, and observability." +readme = "README.md" +keywords = ["async", "concurrency", "rayon", "tokio", "resource-management", "resiliency", "retry", "circuit-breaker", "rate-limit", "observability", "cron", "shutdown"] +categories = ["asynchronous", "concurrency", "rust-patterns"] + +[dependencies] +tokio = { version = "1.53", features = ["full"], optional = true } +rayon = { version = "1.12", optional = true } +rand = { version = "0.8", optional = true } +num_cpus = "1.17" +tracing = "0.1" + +[dev-dependencies] +tokio = { version = "1.53", features = ["full"] } +tracing-subscriber = "0.3" + +[features] +default = [] +io = ["dep:tokio"] +compute = ["dep:rayon"] +bg = ["dep:tokio"] +resiliency = ["dep:tokio", "dep:rand"] +traffic = ["dep:tokio"] +lifecycle = ["dep:tokio"] +observability = ["dep:tokio"] +full = ["io", "compute", "bg", "resiliency", "traffic", "lifecycle", "observability"] + +[[example]] +name = "main" +path = "examples/main.rs" +required-features = ["full"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file diff --git a/crates/mytheclipse/LICENSE-APACHE b/crates/mytheclipse/LICENSE-APACHE new file mode 100644 index 0000000..0da389e --- /dev/null +++ b/crates/mytheclipse/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 The corex Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crates/mytheclipse/LICENSE-MIT b/crates/mytheclipse/LICENSE-MIT new file mode 100644 index 0000000..687a34c --- /dev/null +++ b/crates/mytheclipse/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 The corex Authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/mytheclipse/README.md b/crates/mytheclipse/README.md new file mode 100644 index 0000000..912a0da --- /dev/null +++ b/crates/mytheclipse/README.md @@ -0,0 +1,163 @@ +# mytheclipse + +[![Crates.io](https://img.shields.io/crates/v/mytheclipse.svg)](https://crates.io/crates/mytheclipse) +[![Documentation](https://docs.rs/mytheclipse/badge.svg)](https://docs.rs/mytheclipse) +[![License](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](LICENSE-MIT) + +Resource-aware execution primitives and reliability abstractions for Rust: async I/O, heavy compute, background queue management, resiliency, traffic control, lifecycle management, and observability. Sized automatically from the host's logical core count and exposed through a single, lazily-initialized engine context alongside self-contained, constructible utilities. + +## Resource Sizing + +Given $N$ logical cores (via `num_cpus::get()`): + +| Subsystem | Sizing Formula | Default on 8 cores | Backing Primitive | +| :--- | :--- | :--- | :--- | +| **Async I/O** | $N$ | 8 | Ambient `tokio::spawn` + `tracing` span | +| **Compute** | $\max(1, N - 1)$ | 7 | Sized `rayon::ThreadPool` + `catch_unwind` | +| **Background Queue** | $\max(2, \lfloor N / 2 \rfloor)$ | 4 | `tokio::sync::Semaphore` + `tokio::spawn` | + +The three execution primitives (`io`, `compute`, `bg`) are sized from the host's CPU core count through `mytheclipse::context()`. Resiliency, traffic control, lifecycle, and observability utilities are constructible per-instance without global state. + +## Features + +- **`io`**: enables `mytheclipse::spawn_io`, instrumented async task spawning. +- **`compute`**: enables `mytheclipse::compute`, panic-isolated execution on a sized Rayon pool. +- **`bg`**: enables `mytheclipse::spawn_bg`, semaphore-bounded background tasks. +- **`resiliency`**: fault tolerance abstractions: + - `retry` — auto-retry with exponential backoff and full/equal jitter. + - `CircuitBreaker` — failure threshold and cooldown state machine (Closed/Open/HalfOpen). + - `with_timeout` / `timeout` / `Timeout` — hard execution deadlines. +- **`traffic`**: traffic & load control: + - `RateLimiter` — lazy token-bucket rate limiter with burst capacity. + - `BackpressureQueue` — bounded queue with `DropOldest`, `Reject`, and `Block` overflow policies. + - `ConcurrencyLimiter` — synchronous RAII semaphore wrapper for capping concurrent operations. +- **`lifecycle`**: system lifecycle coordination: + - `ShutdownManager` / `ShutdownSignal` — OS signal catching (SIGINT/SIGTERM/Ctrl-C) and graceful task draining. + - `CronSchedule` / `schedule` — self-contained 5-field cron parser and async timer scheduler. +- **`observability`**: runtime visibility: + - `MetricsCollector` — thread-safe statistics collector with Prometheus text exposition format export. + - `PanicTracker` — non-fatal panic logging with tracing context and boundary isolation. +- **`full`**: enables all subsystems: `io`, `compute`, `bg`, `resiliency`, `traffic`, `lifecycle`, `observability`. + +Zero features enabled by default (`default = []`), so you only pull in the dependencies your application actually uses. + +## Quick Start + +Add to your `Cargo.toml`: + +```toml +[dependencies] +mytheclipse = { version = "0.2", features = ["full"] } +``` + +### 1. Execution Primitives + +```rust +#[tokio::main] +async fn main() { + let ctx = mytheclipse::init(); + + // Async I/O (instrumented with tracing) + let io = mytheclipse::spawn_io(async { 42 }); + + // Heavy Compute (isolated from worker panics) + let sum = mytheclipse::compute(|| (1..=1_000_000u64).sum::())?; + + // Background Queue (concurrency-bounded) + let bg = mytheclipse::spawn_bg(async { /* task */ }).await; + + let _ = (io.await, bg.await); +} +``` + +### 2. Resiliency & Fault Tolerance + +```rust +use std::time::Duration; + +// Auto-Retry with Exponential Backoff + Jitter +let result = mytheclipse::retry( + mytheclipse::RetryConfig::default(), + |err| err.is_transient(), + || async { make_network_request().await }, +).await?; + +// Circuit Breaker +let breaker = mytheclipse::CircuitBreaker::new(mytheclipse::CircuitBreakerConfig::default()); +let value = breaker.call(|| fetch_remote_resource())?; + +// Timeout & Deadlines +let value = mytheclipse::with_timeout(Duration::from_secs(5), async { + long_running_task().await +}).await?; +``` + +### 3. Traffic & Resource Control + +```rust +// Rate Limiter (Token Bucket) +let limiter = mytheclipse::RateLimiter::new(100.0, 10); +limiter.acquire().await?; + +// Concurrency Limiter (Sync Semaphore) +let limiter = mytheclipse::ConcurrencyLimiter::new(10); +let _permit = limiter.acquire(); // released on drop + +// Backpressure Queue (Graceful Degradation) +let queue = mytheclipse::BackpressureQueue::new(100, mytheclipse::OverflowPolicy::DropOldest); +queue.push(job).await?; +let next_job = queue.pop().await; +``` + +### 4. Lifecycle & State Management + +```rust +// Graceful Shutdown Manager +let shutdown = mytheclipse::ShutdownManager::new(); +let sig = shutdown.handle(); +tokio::spawn(async move { + let mut sig = sig; + tokio::select! { + _ = sig.wait() => { /* clean up */ } + _ = worker_loop() => {} + } +}); +shutdown.drain(Duration::from_secs(10)).await; + +// Cron Periodic Job Scheduler (Self-contained, no external crates) +let cron = mytheclipse::CronSchedule::parse("0 1 * * *")?; // 1 AM daily +let job = mytheclipse::schedule("0 1 * * *", || async { + clean_cache().await; +})?; +``` + +### 5. Telemetry & Observability + +```rust +// Centralized Metrics Collector (Prometheus text exposition format) +let metrics = mytheclipse::MetricsCollector::new(); +metrics.record_task(Duration::from_millis(15)); +metrics.inc_counter("http_requests_total", 1); +let prometheus_output = metrics.export_prometheus(); + +// Panic Isolation Tracker +let guard = mytheclipse::PanicTracker::install(); // logs panics with tracing span +let result = mytheclipse::PanicTracker::catch(|| { + risky_operation() +}); +``` + +## Running the Example + +```bash +cargo run --example main --features full +``` + +## License + +Licensed under either of: + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or ) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. diff --git a/examples/main.rs b/crates/mytheclipse/examples/main.rs similarity index 100% rename from examples/main.rs rename to crates/mytheclipse/examples/main.rs diff --git a/src/backpressure.rs b/crates/mytheclipse/src/backpressure.rs similarity index 100% rename from src/backpressure.rs rename to crates/mytheclipse/src/backpressure.rs diff --git a/src/bg.rs b/crates/mytheclipse/src/bg.rs similarity index 100% rename from src/bg.rs rename to crates/mytheclipse/src/bg.rs diff --git a/src/circuit_breaker.rs b/crates/mytheclipse/src/circuit_breaker.rs similarity index 100% rename from src/circuit_breaker.rs rename to crates/mytheclipse/src/circuit_breaker.rs diff --git a/src/compute.rs b/crates/mytheclipse/src/compute.rs similarity index 100% rename from src/compute.rs rename to crates/mytheclipse/src/compute.rs diff --git a/src/concurrency.rs b/crates/mytheclipse/src/concurrency.rs similarity index 100% rename from src/concurrency.rs rename to crates/mytheclipse/src/concurrency.rs diff --git a/src/context.rs b/crates/mytheclipse/src/context.rs similarity index 100% rename from src/context.rs rename to crates/mytheclipse/src/context.rs diff --git a/src/cron.rs b/crates/mytheclipse/src/cron.rs similarity index 100% rename from src/cron.rs rename to crates/mytheclipse/src/cron.rs diff --git a/src/error.rs b/crates/mytheclipse/src/error.rs similarity index 100% rename from src/error.rs rename to crates/mytheclipse/src/error.rs diff --git a/src/io.rs b/crates/mytheclipse/src/io.rs similarity index 100% rename from src/io.rs rename to crates/mytheclipse/src/io.rs diff --git a/src/lib.rs b/crates/mytheclipse/src/lib.rs similarity index 100% rename from src/lib.rs rename to crates/mytheclipse/src/lib.rs diff --git a/src/metrics.rs b/crates/mytheclipse/src/metrics.rs similarity index 100% rename from src/metrics.rs rename to crates/mytheclipse/src/metrics.rs diff --git a/src/panic_tracker.rs b/crates/mytheclipse/src/panic_tracker.rs similarity index 100% rename from src/panic_tracker.rs rename to crates/mytheclipse/src/panic_tracker.rs diff --git a/src/ratelimit.rs b/crates/mytheclipse/src/ratelimit.rs similarity index 100% rename from src/ratelimit.rs rename to crates/mytheclipse/src/ratelimit.rs diff --git a/src/retry.rs b/crates/mytheclipse/src/retry.rs similarity index 100% rename from src/retry.rs rename to crates/mytheclipse/src/retry.rs diff --git a/src/shutdown.rs b/crates/mytheclipse/src/shutdown.rs similarity index 100% rename from src/shutdown.rs rename to crates/mytheclipse/src/shutdown.rs diff --git a/src/timeout.rs b/crates/mytheclipse/src/timeout.rs similarity index 100% rename from src/timeout.rs rename to crates/mytheclipse/src/timeout.rs