diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0120f29 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + push: + branches: + - main + - dev + pull_request: + +# Only keep the latest run per branch/PR to save CI minutes. +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + rust-all: + name: Rust (fmt, clippy, test, doc, shear) + runs-on: ubuntu-latest + env: + # Cache compilation output between runs via sccache. + SCCACHE_GHA_ENABLED: "true" + RUSTC_WRAPPER: "sccache" + CARGO_INCREMENTAL: "0" + steps: + - uses: actions/checkout@v4 + + # Starts the sccache server used by RUSTC_WRAPPER above. + - name: Start sccache + uses: mozilla-actions/sccache-action@v0.0.9 + + # Runs every default workflow in a single step + - name: Run Rust workflows + uses: aderinom/rust-all-action@v1 + with: + run: fmt,clippy,test,doc,shear,deny + flow-fmt-toolchain: nightly diff --git a/.gitignore b/.gitignore index f2f9e58..83d1b8d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ target -Cargo.lock \ No newline at end of file +Cargo.lock + +# Vendored rust-all-action bundle for local CI (scripts/ci-local.sh) +/.tools/ \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..ae91028 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,21 @@ +# Requires nightly rustfmt to be installed. + +# Comments Formatting +comment_width = 100 +wrap_comments = true +format_code_in_doc_comments = true +normalize_comments = true # replace `/*comment*/ with `// comment` where possible + +# Imports Formatting +imports_granularity = "Crate" # Groups imports by their source crate +group_imports = "StdExternalCrate" # Orders imports by STD, External, Internal + +# Enum Formatting +enum_discrim_align_threshold = 4 # Vertical Align enum assignments by up to N characters + +# Block and Closure Formatting +force_multiline_blocks = true # Force multiline closures to be wrapped in a block + +# Patterns Formatting +condense_wildcard_suffixes = true # Replace (a, _, _) = (1,2,3) with (a, ..) = (1,2,3) +use_field_init_shorthand = true # A{ x: x } becomes A { x } diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..efe6630 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "cSpell.words": [ + "oneshot", + "wrapp" + ], + "rust-analyzer.rustfmt.extraArgs": [ + "+nightly" + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1e12bce --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,27 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "clippy-watch", + "type": "shell", + "command": "cargo watch -x clippy", + "isBackground": true, + "problemMatcher": { + "base": "$rustc", + "background": { + "activeOnStart": true, + "beginsPattern": "^\\[Running 'cargo clippy'\\]", + "endsPattern": "^\\[Finished running\\. Exit status: [0-9]+\\]" + } + }, + "presentation": { + "reveal": "never", + "panel": "dedicated", + "clear": true + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index fc9bd48..a6111cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,471 +2,12 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "actix-codec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-sink", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "actix-http" -version = "3.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e2faa3e7418ed780cca54829d32782a4008a077230f67457caa063415e99c2" -dependencies = [ - "actix-codec", - "actix-rt", - "actix-service", - "actix-utils", - "base64", - "bitflags", - "brotli", - "bytes", - "bytestring", - "derive_more", - "encoding_rs", - "flate2", - "foldhash", - "futures-core", - "h2", - "http", - "httparse", - "httpdate", - "itoa", - "language-tags", - "local-channel", - "mime", - "percent-encoding", - "pin-project-lite", - "rand", - "sha1", - "smallvec", - "tokio", - "tokio-util", - "tracing", - "zstd", -] - -[[package]] -name = "actix-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" -dependencies = [ - "quote", - "syn", -] - -[[package]] -name = "actix-router" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7" -dependencies = [ - "bytestring", - "cfg-if", - "http", - "regex", - "regex-lite", - "serde", - "tracing", -] - -[[package]] -name = "actix-rt" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63" -dependencies = [ - "futures-core", - "tokio", -] - -[[package]] -name = "actix-server" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502" -dependencies = [ - "actix-rt", - "actix-service", - "actix-utils", - "futures-core", - "futures-util", - "mio", - "socket2 0.5.10", - "tokio", - "tracing", -] - -[[package]] -name = "actix-service" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "actix-utils" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" -dependencies = [ - "local-waker", - "pin-project-lite", -] - -[[package]] -name = "actix-web" -version = "4.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df09e2d9239703dd64056359c920c7f3fba6535ec61a0059e0f44e095ffe02b4" -dependencies = [ - "actix-codec", - "actix-http", - "actix-macros", - "actix-router", - "actix-rt", - "actix-server", - "actix-service", - "actix-utils", - "actix-web-codegen", - "bytes", - "bytestring", - "cfg-if", - "cookie", - "derive_more", - "encoding_rs", - "foldhash", - "futures-core", - "futures-util", - "impl-more", - "itoa", - "language-tags", - "log", - "mime", - "once_cell", - "pin-project-lite", - "regex", - "regex-lite", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "socket2 0.6.4", - "time", - "tracing", - "url", -] - -[[package]] -name = "actix-web-codegen" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8" -dependencies = [ - "actix-router", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" - -[[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 = "brotli" -version = "8.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - -[[package]] -name = "bytes" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" - -[[package]] -name = "bytestring" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86566c496f2f47d9b8147a4c8b02ffdb69c919fe0c2b2e7195d22cbba0e635c9" -dependencies = [ - "bytes", -] - -[[package]] -name = "cc" -version = "1.2.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chacha20" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" -dependencies = [ - "cfg-if", - "cpufeatures", - "rand_core", -] - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "cookie" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" -dependencies = [ - "percent-encoding", - "time", - "version_check", -] - -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[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 = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[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" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[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 = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - [[package]] name = "futures" version = "0.3.32" @@ -534,608 +75,93 @@ checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[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", - "rand_core", -] - -[[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", - "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" - -[[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 = "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.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" -dependencies = [ - "typenum", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -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 = "impl-more" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35a84fd5aa25fae5c0f4a33d9cac2ca017fc622cbd089be2229993514990f870" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[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.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "language-tags" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "local-channel" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8" -dependencies = [ - "futures-core", - "futures-sink", - "local-waker", -] - -[[package]] -name = "local-waker" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487" - -[[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 = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "num-conv" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" - -[[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 = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prototype" -version = "0.1.0" -dependencies = [ - "actix-web", - "tokio", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -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.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.3", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -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" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] -name = "rustc_version" -version = "0.4.1" +name = "futures-util" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ - "semver", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", ] [[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "scopeguard" -version = "1.2.0" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "semver" -version = "1.0.28" +name = "log" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] -name = "serde" -version = "1.0.228" +name = "memchr" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", -] +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] -name = "serde_core" -version = "1.0.228" +name = "nu-ansi-term" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "serde_derive", + "windows-sys", ] [[package]] -name = "serde_derive" -version = "1.0.228" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "serde_json" -version = "1.0.150" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "unicode-ident", ] [[package]] -name = "sha1" -version = "0.11.0" +name = "quote" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "proc-macro2", ] [[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" +name = "sharded-slab" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ - "errno", - "libc", + "lazy_static", ] -[[package]] -name = "simd-adler32" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - [[package]] name = "slab" version = "0.4.12" @@ -1148,32 +174,6 @@ version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - [[package]] name = "syn" version = "2.0.118" @@ -1185,17 +185,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -1217,84 +206,12 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.51" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" -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.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.4", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" +name = "thread_local" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", + "cfg-if", ] [[package]] @@ -1303,7 +220,6 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ - "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1327,70 +243,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", ] [[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "url" -version = "2.5.8" +name = "tracing-log" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", - "serde", + "log", + "once_cell", + "tracing-core", ] [[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "version_check" -version = "0.9.5" +name = "tracing-subscriber" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] [[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" +name = "valuable" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "windows-link" @@ -1398,15 +289,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -1416,220 +298,38 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - [[package]] name = "wrapp" -version = "0.1.0" +version = "0.0.0" [[package]] name = "wrapp-actix" -version = "0.1.0" +version = "0.0.0" [[package]] name = "wrapp-config" -version = "0.1.0" +version = "0.0.0" +dependencies = [ + "thiserror", + "tracing", + "wrapp-di", +] [[package]] name = "wrapp-di" -version = "0.1.0" +version = "0.0.0" dependencies = [ "futures", "futures-channel", - "pin-project-lite", "thiserror", "tracing", + "tracing-subscriber", ] [[package]] name = "wrapp-router" -version = "0.1.0" +version = "0.0.0" [[package]] name = "wrapp-strategies" -version = "0.1.0" - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[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", - "synstructure", -] - -[[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", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] +version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index c36dc3a..3b2c9e1 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,30 @@ [workspace] resolver = "2" members = ["examples/*", "features/*", "strategies/*", "wrapp"] +exclude = ["examples/prototype"] + +package.license = "MIT OR Apache-2.0" +package.edition = "2024" +package.version = "0.0.0" [workspace.dependencies] # Project Modules -wrapp-di = { path = "./features/wrapp-di" } +wrapp-di = { version = "0.0.0", path = "./features/wrapp-di" } # External Crates futures = "0.3" futures-channel = "0.3" tracing = "0.1" -pin-project-lite = "0.2" +tracing-subscriber = "0.3.23" thiserror = "2.0" + + +[workspace.lints.clippy] +pedantic = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +unwrap_used = { level = "warn" } + +option_if_let_else = { level = "allow" } # More readable +items_after_statements = { level = "allow" } # More readable +match_bool = { level = "allow" } # Personal preference diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..7b8a625 --- /dev/null +++ b/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 Aaron Kelbsch + +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 + + + +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/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..562b605 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2026 Aaron Kelbsch + +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/check.sh b/check.sh new file mode 100755 index 0000000..19b0ec7 --- /dev/null +++ b/check.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# Executes multiple cargo commands in a workspace + +push_script_dir() { + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + pushd "$SCRIPT_DIR" > /dev/null || exit 1 +} +push_script_dir + + +# print help if -h or --help or help is passed at any position +for arg in "$@"; do + if [[ "$arg" == "-h" || "$arg" == "--help" || "$arg" == "help" ]]; then + echo -e "\e[36mBatch Commands:\e[0m + - \e[33mcheck\e[0m (runs fmt, lint, doc, shear, tests in check mode) + - \e[33mfix\e[0m (runs fmt, lint, shear in fix mode) + " + echo -e "\e[36mAliases:\e[0m + - \e[33mfmt\e[0m (for +nightly fmt) + - \e[33mdoc\e[0m (for doc with CI settings) + - \e[33mlint\e[0m (for clippy with CI settings) + - \e[33mtest\e[0m (for a workspace-wide test run) + " + exit 0 + fi +done + +# if no arguments are passed, fallback to check +if [ $# -eq 0 ]; then + set -- "check" +fi + +######################## +# Batch Commands: +declare -a RESULTS +FAILED=0 + +# resolve aliases (fmt/doc/lint/test) into the full cargo arg list in the REPLY array; +cargo_alias() { + case "$1" in + # fmt should always use +nightly + fmt) REPLY=(+nightly fmt "${@:2}") ;; + # doc gets the full documentation argument set + doc) REPLY=(doc --workspace --all-features --no-deps --document-private-items "${@:2}") ;; + # lint is clippy with the workspace-wide settings + lint) REPLY=(clippy --all-targets "${@:2}" -- -D warnings) ;; + # test is a workspace-wide test run + test) REPLY=(test --all "${@:2}") ;; + *) REPLY=("$@") ;; + esac +} + +# run_cargo resolves an alias, echoes the "▶ cargo …" line, then runs it live. +run_cargo() { + cargo_alias "$@" + printf '\e[36m▶ cargo %s\e[0m\n' "${REPLY[*]}" + cargo "${REPLY[@]}" +} + +# run_step runs one step live (echo + output) and records pass/fail for the +# summary. Used by the sequential `fix` path. +run_step() { + local name="$1"; shift + local start=$SECONDS + if run_cargo "$@"; then + printf '\e[32m✓ %s\e[0m (%ds)\n\n' "$name" "$((SECONDS - start))" + RESULTS+=(" \e[32m✓\e[0m $name") + else + printf '\e[31m✗ %s\e[0m (%ds)\n\n' "$name" "$((SECONDS - start))" + RESULTS+=(" \e[31m✗\e[0m $name") + FAILED=1 + fi +} + +# launch_step echoes the resolved "▶ cargo …" line live, then starts the step in +# the background, buffering the command's output to a per-step log and recording +# its own wall-clock duration. +launch_step() { + local name="$1"; shift + local idx=${#PIDS[@]} + cargo_alias "$@" + printf '\e[36m▶ cargo %s\e[0m\n' "${REPLY[*]}" + NAMES[idx]="$name" + LOGS[idx]="$STEP_TMPDIR/step_$idx.log" + DURS[idx]="$STEP_TMPDIR/step_$idx.dur" + local cmd=("${REPLY[@]}") + ( + s=$(date +%s) + + RUSTDOCFLAGS="-D warnings" cargo "${cmd[@]}" > "${LOGS[idx]}" 2>&1 + rc=$? + printf '%s' "$(( $(date +%s) - s ))" > "${DURS[idx]}" + exit "$rc" + ) & + PIDS[idx]=$! +} + +# wait_steps waits for every launched step (in launch order) and reports each +# result: quiet on success, full buffered log on failure. +wait_steps() { + local i status dur + for i in "${!PIDS[@]}"; do + if wait "${PIDS[i]}"; then status=0; else status=1; fi + dur="$(cat "${DURS[i]}" 2>/dev/null)" + if [ "$status" -eq 0 ]; then + printf '\e[32m✓ %s\e[0m (%ss)\n' "${NAMES[i]}" "$dur" + RESULTS+=(" \e[32m✓\e[0m ${NAMES[i]}") + else + printf '\e[31m✗ %s\e[0m (%ss)\n' "${NAMES[i]}" "$dur" + cat "${LOGS[i]}" + RESULTS+=(" \e[31m✗\e[0m ${NAMES[i]}") + FAILED=1 + fi + done +} + +# All Checks +if [ "$1" = "check" ]; then + # Per-run scratch space for buffered step output; only the parallel `check` + # path needs it, so it's created here rather than at the top of the script. + STEP_TMPDIR="$(mktemp -d)" + trap 'rm -rf "$STEP_TMPDIR"' EXIT + declare -a PIDS=() NAMES=() LOGS=() DURS=() + + launch_step "format" fmt --all -- --check + launch_step "doc" doc --quiet --frozen + launch_step "shear" shear + launch_step "lint" lint --quiet + launch_step "test" test --quiet --no-fail-fast + launch_step "deny" deny check + echo -e "\e[36m▶ running ${#PIDS[@]} checks in parallel…\e[0m" + wait_steps + + echo "──────── Summary ────────" + printf '%b\n' "${RESULTS[@]}" + if [ "$FAILED" -eq 0 ]; then + echo -e "\e[32mAll checks passed ✅\e[0m" + else + echo -e "\e[31mSome checks failed ❌\e[0m" + fi + + exit "$FAILED" +fi + + +# All Fixes +if [ "$1" = "fix" ]; then + # Sequentially run all fixable steps, recording pass/fail for the summary. + run_step "lint --fix" lint --fix --allow-dirty + run_step "format" fmt --all + run_step "shear --fix" shear --fix + + echo "──────── Changed files ────────" + git --no-pager diff --stat + + echo "──────── Summary ────────" + printf '%b\n' "${RESULTS[@]}" + [ "$FAILED" -eq 0 ] \ + && echo -e "\e[32mAll fixes applied ✅\e[0m" \ + || echo -e "\e[31mSome fixes failed ❌\e[0m" + + exit "$FAILED" +fi + +################## +# Direct invocation: expand any alias (see run_cargo) and run it. +run_cargo "$@" +if [ $? -ne 0 ]; then + echo -e "\e[31mCommand\e[0m $1 \e[31mfailed\e[0m" + exit 1 +fi diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..40a51dc --- /dev/null +++ b/deny.toml @@ -0,0 +1,57 @@ +# cargo-deny configuration — https://embarkstudios.github.io/cargo-deny/ + +# We are licensed under MIT/Apache-2.0. Explicitly deny any copyleft licenses + +[graph] +all-features = true + +[output] +feature-depth = 1 + +[licenses] +version = 2 + +# Permissive, MIT-compatible licenses. Anything NOT listed here fails the check. +allow = [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Zlib", + "Unlicense", + "0BSD", + "BSL-1.0", + "Unicode-3.0", +] +unused-allowed-license = "allow" + +# Licenses intentionally NOT allowed +# Strong copyleft : GPL-2.0, GPL-3.0, AGPL-3.0 +# Weak copyleft : LGPL-2.1, LGPL-3.0, MPL-2.0, EPL-1.0/2.0, CDDL-1.0 + +confidence-threshold = 0.8 + +# Per-crate license exceptions. +exceptions = [] + +# Security advisories (RustSec) +[advisories] +version = 2 +ignore = [] + +# Dependency bans / hygiene +[bans] +# Reject crates with multiple versions in the dependency graph +multiple-versions = "warn" +# Reject `version = "*"` requirements +wildcards = "deny" +allow-wildcard-paths = true +allow-workspace = true + + +# Crate sources +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] diff --git a/docs/idea.md b/docs/idea.md new file mode 100644 index 0000000..beb4847 --- /dev/null +++ b/docs/idea.md @@ -0,0 +1,104 @@ +# Why? + +Building systems which are more than one thing at once (e.g. A WebAPI + Kafka Consumer) often forces +developers to either build more bootstrapping glue or to accept their applications to look chaotic. + +E.g., a typical multi-functional application might require: + +1. Setting up a web server for API endpoints +2. Configuring Kafka consumers for message processing +3. Initializing database connections +4. Setting up background job schedulers +5. Implementing logging and monitoring systems + +Each of these components often requires its own initialization logic, configuration management, and integration with the rest of the application. This can lead to: + +- Complex and hard-to-maintain bootstrapping code +- Tight coupling between different parts of the application +- Difficulty in adding or removing features +- Challenges in testing individual components in isolation +- Inconsistent patterns across different parts of the system + +These issues can significantly increase the cognitive load on developers and make the codebase harder to evolve over time. +The wrapp ecosystem aims to address these challenges by providing a modular and extensible framework for building multi-functional applications. + +# How? + +1. Modular Architecture: + Wrapp encourages breaking down the application into smaller, self-contained modules. Each module encapsulates a specific Domain (e.g. User, Todos, Metrics). + Each domain on its own can then use one or multiple Strategies to define how it has to interact with the outside world. + E.g. User could be an Actix and a Kafka module, thus getting input from these sources. + + This approach offers several advantages: + - Improved maintainability: Each module can be developed, tested, and updated independently. + - Flexibility: Domains can easily adopt new strategies or remove existing ones without affecting other parts of the system. + - Scalability: As the application grows, new domains can be added as separate modules without disrupting existing functionality. + - Reusability: Well-defined modules can be reused across different projects or shared within a larger ecosystem. + + The separation of domains and strategies allows for a clean architecture where business logic (domains) is decoupled from the technical implementation details (strategies), promoting better code organization and easier long-term maintenance. + +# Goal of wrapp + +Create an ecosystem which allows applications to compose themselves of different logical modules. +All modules share a DI framework, and strategies which determine how the outside world can interact with them. + +The DI Framework will allow a Module to declare other Modules it depends on - being optional, required or lazily. +The DI Framework will orchestrate the construction of all Modules, injecting dependencies as needed. + +The strategy system will allow modules to bootstrap themselves to e.g. the Webserver started by the application, or the Kafka Cluster connected to the app. +With the strategy system a module can declare itself as a WebModule (or probably more specific as an ActixModule, even though a general WebModule would be really cool). +This WebModule could then declare it's own routes, handlers, middlewares etc. which if the Module is registerd will attach themselves to the Server. +Then the Module could also declare itself as a KafkaModule, or a PostgresNotifyModule or a GRPC module, or a GraphQLModule etc. + +This way reusability components like a MetricsModule, a TaskQueueModule, and many more could be created which then +can simply be registered allowing their functionality work. + +To simplify the creation of Strategies Wrapp should also provide some useful tools. E.g. a simple Router which can be used to quickly build a new strategy. +In general it is a goal to make Strategies and Modules: + + 1. As easy to implement as possible + 2. Backwards compatible for as long as possible + +```mermaid +graph LR; + +wrapp --> wrapp-di +wrapp --> wrapp-strategy +wrapp --> wrapp-router + +wrapp-strategy --> wrapp-kafka +wrapp-strategy --> wrapp-actix +wrapp-strategy --> wrapp-sql-notify + + + +wrapp-di --> Module1 +wrapp-actix --> ActixWebModule --> Module1 --> UserApp +wrapp-kafka --> KafkaModule --> Module1 --> UserApp +wrapp-tauri --> TauriModule --> Module1 --> UserApp + +``` + +wrapp - overarching framework +wrapp-di - DI framework +wrapp-strategy - Trait for implementing new Strategies - e.g. a WebServer would be a strategy, or Listening to Kafka is a strategy +wrapp-router - Optional component to use for strategies - could e.g. allow routing through proc macros, and extracting app state + +E.g. + +wrapp-actixs implements wrapp-strategy +wrapp-actixs thus has hooks to: collect all the routes and config it needs and start the actix server +wrapp-actixs exports a trait wrapp-actix-module, which if implemented by a module, has the functions needed to collect all routes and config +since actix has it's own router, it will not use wrapp-router, but the wrapp-axtic strategy will add the finished DI container to actix's app state, and will provide an etractor like WrappState, to access data from the Wrapp-DI + +or + +wrapp-kafka implements wrapp-strategy +wrapp-kafka has a hook to collect all routes for messages, and has an implementation to start clients to listen to kafka +it exports a trait wrapp-kafka-module, which if implemented... +since this does not have it's own router, it can opt in to use the wrapp router, automatically allowing it to use e.g. middlewares and injection on it's endpoints. + +Possible Issues: + +1. Version conflicts +If module has serverely different versions of e.g. wrapp-actixs, this could lead to annoyances diff --git a/docs/todo b/docs/todo new file mode 100644 index 0000000..af1e179 --- /dev/null +++ b/docs/todo @@ -0,0 +1,18 @@ +[~] Wrapp + [~] DI + [x] Implementation + [ ] Rework: Error handling and Lazy + Possibly change it so that instead of returning the error to the requestor, DI just fails, this simplifies error handling + Furthermore Lazy should be looked at again, it's awkward to use. + [] Proc Macro + + [x] Config + [x] Implement Config Provider + [x] Implement Injector + + [] Strategies + [] Figure out how to pass strategies + [] Axum-Utoipa or Actix-Utoipa + + [] Routing + [] See if we could reuse something existing - maybe Tower? \ No newline at end of file diff --git a/examples/prototype/Cargo.toml b/examples/prototype/Cargo.toml index eb1b223..f655fb2 100644 --- a/examples/prototype/Cargo.toml +++ b/examples/prototype/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "prototype" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -12,3 +13,6 @@ tokio = { version = "1.43.0", features = ["full"] } [[bin]] name = "prototype" path = "main.rs" + +[lints] +workspace = true diff --git a/features/wrapp-config/Cargo.toml b/features/wrapp-config/Cargo.toml index 257aacf..8e0a169 100644 --- a/features/wrapp-config/Cargo.toml +++ b/features/wrapp-config/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "wrapp-config" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,3 +11,7 @@ wrapp-di.workspace = true thiserror.workspace = true tracing.workspace = true + + +[lints] +workspace = true diff --git a/features/wrapp-config/examples/using-config-provider.rs b/features/wrapp-config/examples/using-config-provider.rs index 1aabab1..5feab3e 100644 --- a/features/wrapp-config/examples/using-config-provider.rs +++ b/features/wrapp-config/examples/using-config-provider.rs @@ -23,12 +23,9 @@ fn main() { } }; - let retrieved_config = match config_provider.config::() { - Some(c) => c, - None => { - eprintln!("Could not find config type"); - return; - } + let Some(retrieved_config) = config_provider.config::() else { + eprintln!("Could not find config type"); + return; }; assert_eq!(app_config.host, retrieved_config.host); diff --git a/features/wrapp-config/src/errors.rs b/features/wrapp-config/src/errors.rs index a31086f..8279606 100644 --- a/features/wrapp-config/src/errors.rs +++ b/features/wrapp-config/src/errors.rs @@ -2,7 +2,6 @@ use wrapp_di::types::TypeInfo; - /// Errors when trying to register a config #[derive(thiserror::Error, Debug, Clone)] pub enum RegisterConfigError { diff --git a/features/wrapp-config/src/lib.rs b/features/wrapp-config/src/lib.rs index 80a03fb..fae05c8 100755 --- a/features/wrapp-config/src/lib.rs +++ b/features/wrapp-config/src/lib.rs @@ -1,16 +1,18 @@ //! Wrapp Config provides a simple config injection mechanism for Wrapp DI. //! //! ### Overview -//! -//! - [`ConfigProvider`](crate::provider::ConfigProvider) - Registry of configs which can be injected into modules. -//! - [`Config`](crate::resolver::Config) - [`Resolver`](wrapp_di::resolver::Resolver) type which allows for config injections in factories. -//! -//! +//! +//! - [`ConfigProvider`](crate::provider::ConfigProvider) - Registry of configs which can be +//! injected into modules. +//! - [`Config`](crate::resolver::Config) - [`Resolver`](wrapp_di::resolver::Resolver) +//! type which allows for config injections in factories. +//! +//! //! # Examples //! ```rust #![doc = include_str!("../examples/using-config-provider.rs")] //! ``` -pub mod resolver; pub mod errors; pub mod provider; +pub mod resolver; diff --git a/features/wrapp-config/src/provider.rs b/features/wrapp-config/src/provider.rs index 88c4d97..89e9e7f 100644 --- a/features/wrapp-config/src/provider.rs +++ b/features/wrapp-config/src/provider.rs @@ -8,7 +8,7 @@ use std::{ use wrapp_di::types::TypeInfo; -use crate::errors::{RegisterConfigError}; +use crate::errors::RegisterConfigError; /// A provider to register all configs. /// @@ -20,6 +20,7 @@ pub struct ConfigProvider { impl ConfigProvider { /// Initializes an empty Config Provider + #[must_use] pub fn new() -> Self { Self { configs: HashMap::new(), @@ -30,21 +31,28 @@ impl ConfigProvider { pub fn config(&self) -> Option> { let type_id = TypeId::of::(); - let config = self.configs - .get(&type_id)?; + let config = self.configs.get(&type_id)?; - match config.clone().downcast::() { - Ok(config) => Some(config), - Err(_) => { - debug_assert!(false, "Config Provider contained invalid type in slot for type: {:?}", TypeInfo::of::()); - tracing::error!("Config Provider contained invalid type in slot for type: {:?}", TypeInfo::of::()); - None - }, + if let Ok(config) = config.clone().downcast::() { + Some(config) + } else { + debug_assert!( + false, + "Config Provider contained invalid type in slot for type: {:?}", + TypeInfo::of::() + ); + tracing::error!( + "Config Provider contained invalid type in slot for type: {:?}", + TypeInfo::of::() + ); + None } - } /// Add a config to the registry. + /// + /// # Errors + /// - Fails if a config of the same type has already been registered. pub fn add_config( &mut self, config: T, diff --git a/features/wrapp-config/src/resolver.rs b/features/wrapp-config/src/resolver.rs index b5a1b72..90f5af7 100644 --- a/features/wrapp-config/src/resolver.rs +++ b/features/wrapp-config/src/resolver.rs @@ -45,7 +45,6 @@ use crate::provider::ConfigProvider; /// config.enabled /// } /// } -/// /// ``` pub struct Config { inner: Arc, @@ -58,10 +57,12 @@ impl Deref for Config { } } impl Config { + #[must_use] pub fn inner(&self) -> Arc { self.inner.clone() } + #[must_use] pub fn into_inner(self) -> Arc { self.inner } @@ -79,12 +80,12 @@ impl Resolver for Config { .config() .ok_or_else(|| InjectError::RequireError(RequireError::TypeMissing(config_name)))?; - Ok(Config { inner: config }) + Ok(Self { inner: config }) } fn dependency_info() -> DependencyInfo { DependencyInfo { - type_info: TypeInfo::of::>(), + type_info: TypeInfo::of::(), optional: false, lazy: false, } diff --git a/features/wrapp-di/Cargo.toml b/features/wrapp-di/Cargo.toml index b370ee6..40cfd0a 100755 --- a/features/wrapp-di/Cargo.toml +++ b/features/wrapp-di/Cargo.toml @@ -1,7 +1,8 @@ [package] name = "wrapp-di" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,5 +10,10 @@ edition = "2021" futures.workspace = true futures-channel.workspace = true tracing.workspace = true -pin-project-lite.workspace = true thiserror.workspace = true + +[dev-dependencies] +tracing-subscriber.workspace = true + +[lints] +workspace = true diff --git a/features/wrapp-di/examples/manual-di-impl.rs b/features/wrapp-di/examples/manual-di-impl.rs new file mode 100644 index 0000000..fbae515 --- /dev/null +++ b/features/wrapp-di/examples/manual-di-impl.rs @@ -0,0 +1,105 @@ +//! An example of how a manual DI implementation can be done using the Wrapp DI framework. +//! +//! Usually you should use the wrapp-di procmacros to generate the factory for you, +//! but this example shows how to do it manually to help understanding what happens under the hood. +#![allow(dead_code)] + +use std::{fmt::Debug, sync::Arc}; + +use wrapp_di::{ + builder::DiBuilder, + errors::InjectError, + factories::InstanceFactory, + initiator::DiHandle, + resolver::{ + Resolver, + lazy::{Lazy, LazyOption}, + }, + types::DependencyInfo, +}; + +struct TestConfig { + enable: bool, +} + +fn main() { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + + // Create a new DI builder and register instances and factories + let app = DiBuilder::new() + .add_instance(TestConfig { enable: true }) + .add_instance(124_i128) + .add_instance("test".to_string()) + .add_factory(TestFactory); + + // Build the DI container and handle any errors that may occur + let app = futures::executor::block_on(app.build()); + let app = match app { + Ok(app) => app, + Err(e) => { + println!("{e}"); + return; + } + }; + + println!("{app:?}"); + let test_instance = app.require::(); + println!("{test_instance:?}"); +} + +/// Test struct requiring an Arc, a Lazy and a `LazyOption` as dependencies. +#[derive(Debug)] +struct Test { + a: Arc, + b: Lazy, + c: LazyOption, +} +struct TestFactory; +impl InstanceFactory for TestFactory { + type Provides = Test; + + /// Defines the dependencies required by the `TestFactory`. + fn dependencies() -> Vec { + vec![ + Arc::::dependency_info(), + Lazy::::dependency_info(), + LazyOption::::dependency_info(), + ] + } + + /// Determines if the `TestFactory` is enabled based on the `TestConfig` instance in the DI + /// container. + /// + /// This is run before the `construct` method and allows for conditional instantiation of the + /// `Test` struct. + async fn is_enabled( + &mut self, + mut di: DiHandle, + ) -> Result> { + let enabled = Option::>::resolve(&mut di) + .await? + .is_some_and(|config| config.enable); + Ok::<_, InjectError>(enabled) + } + + /// Constructs a new instance of the Test struct with dependencies injected from the DI + /// container. + async fn construct( + &mut self, + mut di: DiHandle, + ) -> Result> { + // Resolve the dependencies from the DI container + let str = Arc::::resolve(&mut di).await?; + let str2 = Lazy::::resolve(&mut di).await?; + let str3 = LazyOption::::resolve(&mut di).await?; + + // Return a new instance of Test with the resolved dependencies + Ok::<_, InjectError>(Test { + a: str, + b: str2, + c: str3, + }) + } +} diff --git a/features/wrapp-di/src/builder.rs b/features/wrapp-di/src/builder.rs index 9801144..d888fff 100644 --- a/features/wrapp-di/src/builder.rs +++ b/features/wrapp-di/src/builder.rs @@ -8,14 +8,6 @@ use crate::{ types::{Injectable, Instance, TypeInfo}, }; -////////////////////////////////////////////////////////////////////// -/// -/// The DI Consist of three Parts. -/// 1. The AppBuilder where one registers all factories and instances -/// 2. Then for initialization - - - pub struct DiBuilder { /// Registered factories which can provide instances pub(crate) registered_factories: Vec>, @@ -29,29 +21,59 @@ impl Default for DiBuilder { } impl DiBuilder { + #[must_use] pub fn new() -> Self { - DiBuilder { + Self { registered_factories: Vec::new(), registered_instances: HashMap::new(), } } } impl DiBuilder { + /// Adds an instance of a type to the DI container. + /// + /// An instance is a concrete object that has already been created and is ready to be used. + /// These instances are stored in the DI container and can be retrieved by their type during + /// container initialization. + #[must_use] pub fn add_instance(mut self, instance: T) -> Self { self.registered_instances .insert(TypeInfo::of::(), Instance::new(instance)); self } + /// Adds a factory to the DI container. + /// + /// [Factories](InstanceFactory) are responsible for creating instances of a specific type + /// during initialization of the DI container. They can have dependencies on other types, + /// which will be resolved and injected by the DI container during [building](DiBuilder::build). + #[must_use] pub fn add_factory(mut self, factory: Factory) -> Self { self.registered_factories.push(Box::new(factory)); self } + /// Builds the DI container, resolving all dependencies and creating instances as needed. + /// + /// ### Errors + /// - Fails if any factory fails to create an instance. + /// - Fails if there are conflicts in the dependency graph, e.g. circular dependencies or + /// missing required types. + /// - Fails if two factories produce the same type, which would create ambiguity in the + /// dependency graph. pub async fn build(self) -> Result { DiInitiator::new().initiate(self, None).await } + /// Builds the DI container, resolving all dependencies and creating instances as needed. + /// + /// ### Errors + /// - Fails if any factory fails to create an instance. + /// - Fails if there are conflicts in the dependency graph, e.g. circular dependencies or + /// missing required types. + /// - Fails if two factories produce the same type, which would create ambiguity in the + /// dependency graph. + /// - Fails if the initialization takes longer than the specified timeout duration. pub async fn build_timeout(self, timeout: Duration) -> Result { DiInitiator::new().initiate(self, Some(timeout)).await } diff --git a/features/wrapp-di/src/container.rs b/features/wrapp-di/src/container.rs index 75bbdbe..9b757cd 100644 --- a/features/wrapp-di/src/container.rs +++ b/features/wrapp-di/src/container.rs @@ -1,5 +1,5 @@ use std::{ - any::{type_name, TypeId}, + any::{TypeId, type_name}, collections::HashMap, fmt::Debug, sync::Arc, @@ -41,22 +41,26 @@ impl DiContainer { Self(Arc::new(DiContainerInner { instances, graph })) } - /// Attempts to get the requested type + /// Attempts to get an instance of type `T` from the DI container. + /// + /// # Errors + /// See [`RequireError`] for possible errors during resolution. pub fn require(&self) -> Result, RequireError> { match self.0.instances.get(&TypeId::of::()) { Some((_, Some(instance))) => { - instance - .downcast() - .map_err(|actual_type| RequireError::DowncastFailed { + instance.downcast().map_err(|actual_type| { + RequireError::DowncastFailed { required_type: type_name::(), actual_type, - }) + } + }) } Some((_, None)) => Err(RequireError::TypeDisabled(type_name::())), None => Err(RequireError::TypeMissing(type_name::())), } } + #[must_use] pub fn graph(&self) -> &DependencyGraph { &self.0.graph } diff --git a/features/wrapp-di/src/dependency_graph.rs b/features/wrapp-di/src/dependency_graph.rs index 0d779e9..d410cc5 100644 --- a/features/wrapp-di/src/dependency_graph.rs +++ b/features/wrapp-di/src/dependency_graph.rs @@ -16,9 +16,13 @@ pub struct DependencyGraph { map: BTreeMap, } impl DependencyGraph { + /// Creates a new `DependencyGraph` from the given `DiBuilder` + /// + /// # Errors + /// See [`DependencyGraphError`] for possible errors during graph creation. pub fn new(builder: &DiBuilder) -> Result { let mut graph = Self { - map: Default::default(), + map: BTreeMap::default(), }; for instance in builder.registered_instances.values() { @@ -32,6 +36,10 @@ impl DependencyGraph { Ok(graph) } + /// Adds a new entry to the graph + /// + /// # Errors + /// See [`DependencyGraphError`] for possible errors during graph creation. pub fn add( &mut self, info: TypeInfo, @@ -47,9 +55,10 @@ impl DependencyGraph { Ok(()) } - /// Validate the graph + /// Validates the dependency graph and returns a list of errors if any are found. /// - /// Returns a list of all issues + /// # Errors + /// See [`DependencyGraphErrors`] for possible errors during graph validation. pub fn check(&self) -> Result<(), DependencyGraphErrors> { let mut checked = HashSet::new(); let mut errors = Vec::new(); @@ -94,7 +103,7 @@ impl DependencyGraph { // Skip other checks if already checked if !checked.insert(entry.info.type_id) { return; - }; + } dependency_chain.push(entry.info); @@ -137,25 +146,28 @@ pub enum DependencyGraphError { dependency: TypeInfo, required_by: TypeInfo, }, - #[error("A Circular Dependency exists between '{from}' and '{to}' through {chain:?} - Consider using `Lazy`")] + #[error( + "A Circular Dependency exists between '{from}' and '{to}' through {chain:?} - Consider using `Lazy`" + )] CircularDependency { from: TypeInfo, to: TypeInfo, chain: Vec, }, } + +/// One or more errors in the dependency graph +#[derive(Error, Debug, Clone)] +pub struct DependencyGraphErrors { + pub errors: Vec, +} impl std::fmt::Display for DependencyGraphErrors { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut display = Vec::new(); display.push("The dependency graph had one or more errors:".to_string()); for error in &self.errors { - display.push(format!("- {}", error)); + display.push(format!("- {error}")); } f.write_str(&display.join("\n")) } } - -#[derive(Error, Debug, Clone)] -pub struct DependencyGraphErrors { - pub errors: Vec, -} diff --git a/features/wrapp-di/src/errors.rs b/features/wrapp-di/src/errors.rs index f2eecc1..70013c7 100644 --- a/features/wrapp-di/src/errors.rs +++ b/features/wrapp-di/src/errors.rs @@ -13,9 +13,6 @@ pub enum InjectError { /// Injection Handle channel was closed #[error("Handle was closed, did you try using it after Initialization?")] HandleClosed, - /// Generic error during Injection - #[error("Error during injection: {0}")] - Other(DynError), } impl From for InjectError { fn from(_: mpsc::SendError) -> Self { diff --git a/features/wrapp-di/src/factories.rs b/features/wrapp-di/src/factories.rs index c9a965f..c485b16 100644 --- a/features/wrapp-di/src/factories.rs +++ b/features/wrapp-di/src/factories.rs @@ -6,20 +6,22 @@ use crate::{ }; /// A Factory providing instances of a given type -pub trait InstanceFactory: Send + Sync { +pub trait InstanceFactory: Send { type Provides: Injectable; /// Returns the typeinfo about the factory's provided type + #[must_use] fn supplies() -> TypeInfo { TypeInfo::of::() } /// Returns a list of dependencies the factory requires to supply it's type - fn get_dependencies() -> Vec; + fn dependencies() -> Vec; /// Constructs a new instance of the factory's provided type /// - /// Returns the constructed instance, or an error if either Dependencies are not satisfied or the Instantiation failed + /// Returns the constructed instance, or an error if either Dependencies are not satisfied or + /// the Instantiation failed fn construct( &mut self, di: DiHandle, @@ -36,7 +38,7 @@ pub trait InstanceFactory: Send + Sync { } /// Wrapper Trait for factories, providing instances of Any -pub trait DynFactory { +pub trait DynFactory: Send { fn supplies(&self) -> TypeInfo; /// Returns a list of dependencies for the factory @@ -61,7 +63,7 @@ impl> DynFactory f } fn dependencies(&self) -> Vec { - SpecificFactory::get_dependencies() + SpecificFactory::dependencies() } fn construct( @@ -73,7 +75,7 @@ impl> DynFactory f SpecificFactory::construct(self, di) .await .map(Instance::new) - .map_err(|e| e.into()) + .map_err(std::convert::Into::into) }; Box::new(construction_fut) @@ -87,7 +89,7 @@ impl> DynFactory f // Forward the call to the specific implementation SpecificFactory::is_enabled(self, di) .await - .map_err(|e| e.into()) + .map_err(std::convert::Into::into) }; Box::new(future) diff --git a/features/wrapp-di/src/initiator.rs b/features/wrapp-di/src/initiator.rs index 6b355b0..7d899fc 100644 --- a/features/wrapp-di/src/initiator.rs +++ b/features/wrapp-di/src/initiator.rs @@ -1,12 +1,13 @@ use std::{ any::TypeId, collections::{HashMap, HashSet}, + fmt::Display, sync::Arc, thread::{self, sleep}, time::Duration, }; -use futures::{stream::FuturesUnordered, StreamExt}; +use futures::{StreamExt, stream::FuturesUnordered}; use futures_channel::{mpsc, oneshot}; use crate::{ @@ -18,7 +19,7 @@ use crate::{ types::{DynError, Instance, TypeInfo}, }; -/// Initiates the DiContainer +/// Initiates the `DiContainer` pub(crate) struct DiInitiator { request_rx: mpsc::Receiver, request_tx: mpsc::Sender, @@ -27,16 +28,16 @@ pub(crate) struct DiInitiator { /// Waiters for instance results instance_waiters: HashMap>>, - /// Waiters for the final DiContainer + /// Waiters for the final `DiContainer` container_waiters: Vec>, /// All produced instances - None = Type is known but disabled instances: HashMap)>, } impl DiInitiator { - pub(crate) fn new() -> DiInitiator { + pub(crate) fn new() -> Self { let (injection_request_sender, injection_request_receiver) = mpsc::channel(10); - DiInitiator { + Self { request_rx: injection_request_receiver, request_tx: injection_request_sender, all_registered_type_ids: HashSet::new(), @@ -59,11 +60,13 @@ impl DiInitiator { sleep(timeout); let _ = timeout_tx.send(()); }); - }; + } // Build and check Graph - let graph = DependencyGraph::new(&blueprint).map_err(|error| DependencyGraphErrors { - errors: vec![error], + let graph = DependencyGraph::new(&blueprint).map_err(|error| { + DependencyGraphErrors { + errors: vec![error], + } })?; graph.check()?; @@ -73,7 +76,7 @@ impl DiInitiator { // On fail - inform all waiters let msg = Err(e.clone().into()); for (_, waiters) in self.instance_waiters { - for waiter in waiters.into_iter() { + for waiter in waiters { let _ = waiter.send(msg.clone()); } } @@ -86,7 +89,30 @@ impl DiInitiator { return Err(e); } - tracing::debug!("All"); + tracing::debug!( + "All factories finished, all injection requests handled, final instance count: {}", + self.instances.len() + ); + #[cfg(debug_assertions)] + { + for (info, instance) in self.instances.values() { + let status = if instance.is_some() { + "" + } else { + "[disabled] " + }; + tracing::debug!(" - {}{}", status, info.type_name,); + } + } + + debug_assert!( + self.instance_waiters.is_empty(), + "Not all instance waiters were satisfied" + ); + debug_assert!( + self.container_waiters.is_empty(), + "Not all container waiters were satisfied" + ); let container = DiContainer::new(self.instances, graph); for waiter in self.container_waiters { @@ -115,7 +141,7 @@ impl DiInitiator { // Add all pre build instances to the results - for (info, instance) in registered_instances.into_iter() { + for (info, instance) in registered_instances { self.all_registered_type_ids.insert(info.type_id); self.instances.insert(info.type_id, (info, Some(instance))); } @@ -134,20 +160,16 @@ impl DiInitiator { let supply_info = factory.supplies(); let result = async { // Check if the factory is enabled - match Box::into_pin(factory.is_enabled(handle.clone())).await? { - true => { - tracing::debug!("Factory for {} is enabled", supply_info.type_name) - } - false => { - tracing::debug!("Factory for {} is disabled", supply_info.type_name); - return Ok(None); - } + if Box::into_pin(factory.is_enabled(handle.clone())).await? { + tracing::debug!("Factory for {} is enabled", supply_info.type_name); + } else { + tracing::debug!("Factory for {} is disabled", supply_info.type_name); + return Ok(None); } // Construct factory let instance = Box::into_pin(factory.construct(handle.clone())).await?; - tracing::debug!("Constructed instance of {}", instance.info.type_name); Ok::<_, DynError>(Some(instance)) } .await; @@ -159,13 +181,14 @@ impl DiInitiator { } // Start handling injection requests and wait for all factories to finish - // let pin_set = pin!(set); let factory_count = factory_futures.len(); loop { let factories_left = factory_futures.len(); - tracing::debug!( - "Waiting for factories to finish [{factories_left} of {factory_count} complete]" + let waiters = self.instance_waiters.len(); + let container_waiters = self.container_waiters.len(); + tracing::trace!( + "Polling initiation [{factories_left} of {factory_count} in progress, waiters: {waiters}, container waiters: {container_waiters}]" ); futures::select! { @@ -183,6 +206,32 @@ impl DiInitiator { } } + // Close the request channel and handle any remaining requests + self.request_rx.close(); + loop { + tracing::trace!( + "Polling remaining injection requests [{} waiters, {} container waiters]", + self.instance_waiters.len(), + self.container_waiters.len(), + ); + futures::select! { + request = self.request_rx.next() => { + let Some(request) = request else { + break; + }; + self.handle_injection_request(request); + + assert!(self.instance_waiters.is_empty(), + "Not all instance waiters were satisfied after all factories finished: {} waiters remain - this is a bug in the DI system", + self.instance_waiters.len() + ); + } + _ = timeout => { + return Err(InitError::Timeout) + } + } + } + Ok(()) } @@ -193,19 +242,14 @@ impl DiInitiator { &mut self, result: Option<(TypeInfo, Result, DynError>)>, ) -> Result { - let (info, result) = match result { - Some(result) => result, - None => { - // If no more tasks are left, exit the loop - // all injection requests must now also be handled as nothing is left to be build - debug_assert!( - self.instance_waiters.is_empty(), - "Not all waiters were satisfied" - ); - return Ok(true); - } + let Some((info, result)) = result else { + // A none result means all tasks are complete, we exit the loop + // all injection requests must now also be handled as nothing is left to be build + return Ok(true); }; + tracing::debug!("Factory for {} finished", info.type_name); + match result { Ok(instance) => { handle_created_instance(self, info, instance); @@ -217,7 +261,7 @@ impl DiInitiator { error: Arc::new(err), }); } - }; + } return Ok(false); @@ -243,12 +287,13 @@ impl DiInitiator { .into_iter() .flat_map(Vec::into_iter) { + tracing::trace!("Informing waiter for {} with result", type_name); let _ = waiter.send(message.clone()); } } } - /// Get a handle to the DiInitiator + /// Get a handle to the `DiInitiator` /// /// The handle is only valid before and during Initiation. pub fn get_handle(&self) -> DiHandle { @@ -260,6 +305,8 @@ impl DiInitiator { // Injection Request handlers impl DiInitiator { fn handle_injection_request(&mut self, request: DiRequest) { + tracing::trace!("Got injection request: {}", request); + match request { DiRequest::Require { type_info, @@ -284,17 +331,22 @@ impl DiInitiator { } // Check if we already have a result - match self.instances.get(&info.type_id) { - Some((_, result)) => { - let _ = match result { - Some(instance) => response_channel.send(Ok(instance.clone())), - None => response_channel.send(Err(RequireError::TypeDisabled(info.type_name))), - }; - return; - } - None => {} // No result yet + if let Some((_, result)) = self.instances.get(&info.type_id) { + tracing::trace!("Found result for {} - sending to waiter", info.type_name); + let response = match result { + Some(instance) => Ok(instance.clone()), + None => Err(RequireError::TypeDisabled(info.type_name)), + }; + + // Ignore error, as the receiver may have been dropped + let _ = response_channel.send(response); + return; } + tracing::trace!( + "No result found for {} - adding to waiters list", + info.type_name + ); // Otherwise add the request to the waiters list self.instance_waiters .entry(info.type_id) @@ -308,6 +360,7 @@ impl DiInitiator { } /// DI Handle for resolving dependencies and getting instances from the registry. +/// /// The DI Handle is only valid during instantiation of the Application. /// Afterwards the DI Container can be used directly for dependency injection. #[derive(Clone)] @@ -315,6 +368,10 @@ pub struct DiHandle { pub request_sender: mpsc::Sender, } impl DiHandle { + /// Resolves a dependency of type `T` using the DI Handle. + /// + /// # Errors + /// See [`InjectError`] for possible errors during resolution. pub async fn resolve(&mut self) -> Result { T::resolve(self).await } @@ -323,15 +380,27 @@ impl DiHandle { pub type DiResponseSender = oneshot::Sender>; pub type DiResponseReceiver = oneshot::Receiver>; -/// Requests between [DiHandle] and [DiInitiator] +/// Requests between [`DiHandle`] and `DiInitiator` pub enum DiRequest { /// Requires an instance of a specific type Require { type_info: TypeInfo, response_channel: DiResponseSender, }, - /// Requires a reference to the [DiContainer] once it has been build + /// Requires a reference to the [`DiContainer`] once it has been built RequireApp { response_channel: DiResponseSender, }, } +impl Display for DiRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Require { type_info, .. } => { + write!(f, "Require({})", type_info.type_name) + } + Self::RequireApp { .. } => { + write!(f, "RequireApp") + } + } + } +} diff --git a/features/wrapp-di/src/lib.rs b/features/wrapp-di/src/lib.rs index 7fa6065..2c05c96 100644 --- a/features/wrapp-di/src/lib.rs +++ b/features/wrapp-di/src/lib.rs @@ -1,28 +1,41 @@ -//! Wrapp DI consists of following components: +//! # Wrapp DI //! -//! 1. DiBuilder - for registering all factories and instances. -//! 2. DiInitiator - which executes all factories, and handles injection requests during their initialization. -//! 2.1 Injection Handles - which are valid during the AppInit Phase, and allow a component to request its dependencies. -//! 3. DIContainer - which is the final container that holds all created instances. +//! Opinionated dependency injection framework for Wrapp. //! -//! General logic: -//! AppBuilder calls BUild +//! ### Overview +//! - [`DiBuilder`]: Main registration and building interface for the DI container. +//! - [`DiContainer`]: The DI container that holds instances and resolves dependencies. +//! - [`InstanceFactory`]: Trait for factories that can create instances of specific types. Usually +//! generated by the `todo!()` proc macro. +//! - [`Resolver`]: Trait allowing implementation of custom resolution strategies. //! -//! AppGraph queries all factories for their dependencies and creates a graph. -//! AppGraph checks the graph for circular dependencies, and aborts DI if: -//! - Any required Dependencies are missing -//! - Any Circular Dependencies were found +//! ### Example //! -//! AppInitiator creates tasks for all factories. -//! Factories start asking for their dependencies. -//! Some Factory will not require a dependency (or just requires already existing instances), and as such will complete -//! After Completion, AppInitiator gets the result of the completed task. -//! AppInitiator then informs all factories which were waiting for this dependency. -//! Step by step all factories will complete -//! After All factories have completed, AppInitiator creates the final DIContainer. -//! AppInitiator publishes the DIcontainer through a channel to all dependencies. -//! DiContainer wil also be given to the caller of AppBuilder.build() +//! ``` +#![doc = include_str!("../examples/manual-di-impl.rs")] +//! ``` +//! +//! ### Internal Flow //! +//! - All dependencies are registered in the [`DiBuilder`] either as instances or factories. +//! - When [`DiBuilder::build`] is called, the `DiInitiator` is created. +//! - The `DiInitiator` creates a [`DependencyGraph`] from the registered dependencies, checking +//! for circular dependencies and other issues. +//! - Afterwards all [`InstanceFactory`] are called and executed in a [`FuturesUnordered`], first to +//! check if they are enabled, and then to create their instances. +//! - Results from the [`InstanceFactory`] are stored and become available through a handle to the +//! `DiInitiator`. +//! - After all factories have been called, all results are collected and stored in the +//! [`DiContainer`], which is then returned to the application. +//! +//! +//! [`InstanceFactory`]: factories::InstanceFactory +//! [`DiBuilder`]: builder::DiBuilder +//! [`DiBuilder::build`]: builder::DiBuilder::build +//! [`DiContainer`]: container::DiContainer +//! [`DependencyGraph`]: dependency_graph::DependencyGraph +//! [`Resolver`]: resolver::Resolver +//! [`FuturesUnordered`]: futures::stream::FuturesUnordered pub mod builder; pub mod container; diff --git a/features/wrapp-di/src/main.rs b/features/wrapp-di/src/main.rs deleted file mode 100644 index d6994b6..0000000 --- a/features/wrapp-di/src/main.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::{error::Error, fmt::Debug, sync::Arc}; - -use wrapp_di::{ - builder::DiBuilder, - factories::InstanceFactory, - initiator::DiHandle, - resolver::{ - lazy::{Lazy, LazyOption}, - Resolver, - }, - types::DependencyInfo, -}; - -fn main() { - let app = DiBuilder::new() - .add_instance(124_i128) - .add_instance("test".to_string()) - .add_factory(TestFactory); - - let app = futures::executor::block_on(app.build()); - let app = match app { - Ok(app) => app, - Err(e) => { - println!("{}", e); - return; - } - }; - - println!("{:?}", app); - let t = app.require::(); - println!("{:?}", t); - - let a = Arc::new("String".to_owned()); - let b: &dyn Debug = &a; -} - -#[derive(Debug)] -struct Test { - a: Arc, - b: Lazy, - c: LazyOption, -} -struct TestFactory; -impl InstanceFactory for TestFactory { - type Provides = Test; - fn get_dependencies() -> Vec { - vec![ - Arc::::dependency_info(), - Lazy::::dependency_info(), - LazyOption::::dependency_info(), - ] - } - - async fn construct( - &mut self, - mut di: DiHandle, - ) -> Result> { - let str = Arc::::resolve(&mut di).await?; - let str2 = Lazy::::resolve(&mut di).await?; - let str3 = LazyOption::::resolve(&mut di).await?; - Ok(Test { - a: str, - b: str2, - c: str3, - }) - } -} diff --git a/features/wrapp-di/src/resolver/arc.rs b/features/wrapp-di/src/resolver/arc.rs index 9b95b70..7fa8c5d 100644 --- a/features/wrapp-di/src/resolver/arc.rs +++ b/features/wrapp-di/src/resolver/arc.rs @@ -22,12 +22,12 @@ impl Resolver for Arc { .await?; let resolved = rx.await??; - let downcasted = resolved - .downcast::() - .map_err(|e| RequireError::DowncastFailed { + let downcasted = resolved.downcast::().map_err(|e| { + RequireError::DowncastFailed { required_type: type_name::(), actual_type: e, - })?; + } + })?; Ok(downcasted) } @@ -48,12 +48,15 @@ impl Resolver for Option { { match Resolvable::resolve(handle).await { Ok(resolved) => Ok(Some(resolved)), - Err(e) => match e { - // If the required type is disabled, or not registered Option does not fail - InjectError::RequireError(RequireError::TypeDisabled(_)) - | InjectError::RequireError(RequireError::TypeMissing(_)) => Ok(None), - _ => Err(e), - }, + Err(e) => { + match e { + // If the required type is disabled, or not registered Option does not fail + InjectError::RequireError( + RequireError::TypeDisabled(_) | RequireError::TypeMissing(_), + ) => Ok(None), + _ => Err(e), + } + } } } diff --git a/features/wrapp-di/src/resolver/lazy.rs b/features/wrapp-di/src/resolver/lazy.rs index d7bc601..090933e 100644 --- a/features/wrapp-di/src/resolver/lazy.rs +++ b/features/wrapp-di/src/resolver/lazy.rs @@ -1,15 +1,12 @@ use std::{ any::type_name, fmt::Debug, - future::Future, ops::Deref, - sync::{Arc, Mutex, OnceLock}, - task::{Context, Poll}, + sync::{Arc, OnceLock}, }; -use futures::{FutureExt, SinkExt}; +use futures::{FutureExt, SinkExt, future::Shared}; use futures_channel::oneshot; -use pin_project_lite::pin_project; use crate::{ errors::{InjectError, RequireError}, @@ -18,6 +15,12 @@ use crate::{ types::{DependencyInfo, Injectable, Instance, TypeInfo}, }; +/// Shared receiver for a lazily resolved instance. +/// +/// [`Shared`] lets the single backing oneshot be observed from every clone, handling +/// the "resolve once, read many" synchronization so we don't have to. +type SharedInstance = Shared>; + /// Lazily resolved dependency /// /// Should only be accessed after the DI process has completed. @@ -28,20 +31,22 @@ use crate::{ /// /// Note: /// -/// This Type by itself has many panic conditions - However if used in the DI context, no panics should happen unless: +/// This Type by itself has many panic conditions - However if used in the DI context, no panics +/// should happen unless: /// - It is accessed during the Injection Phase /// - It is accessed after DI has already Failed -/// -pub struct Lazy(Arc>); +pub struct Lazy { + /// Shared source future, resolved once the instance becomes available. + source: SharedInstance, + /// Caches the downcast result so [`get`](Self::get) can hand out a stable reference. + resolved: OnceLock, InjectError>>, +} impl Debug for Lazy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_tuple("Lazy").field(&self.get()).finish() + // Use `try_get` so debugging an unresolved `Lazy` does not panic. + f.debug_tuple("Lazy").field(&self.try_get()).finish() } } -struct LazyInner { - once: OnceLock, InjectError>>, - rx: Mutex>, -} impl Deref for Lazy { type Target = Arc; @@ -55,6 +60,12 @@ impl Resolver for Lazy { Self: Sized, { let (tx, rx) = oneshot::channel(); + + tracing::trace!( + "Lazy dependency {} requested - will be resolved when available", + type_name::() + ); + handle .request_sender .send(DiRequest::Require { @@ -64,12 +75,11 @@ impl Resolver for Lazy { .await .map_err(|_| InjectError::HandleClosed)?; - // Check if we got an immediate error - - Ok(Lazy(Arc::new(LazyInner { - once: OnceLock::new(), - rx: Mutex::new(rx), - }))) + // We queue the request, but don't wait for the result here. + Ok(Self { + source: rx.shared(), + resolved: OnceLock::new(), + }) } fn dependency_info() -> DependencyInfo { @@ -80,113 +90,67 @@ impl Resolver for Lazy { } } } + impl Lazy { - /// Accesses the Lazy Dependency + /// Accesses the lazy dependency. /// /// # Panics - /// - When accessed before the DI Container Init has completed + /// - When accessed before the DI container init has completed. + /// - When accessed after DI has already failed. + #[must_use] pub fn get(&self) -> &Arc { self.try_get() .expect("Lazy inject accessed before initialized") - .as_ref() - .expect("Lazy inject accessed after DI failed") + .expect("Lazy inject result contained an error - this should not happen if accessed after DI has completed") } - /// Try to access the lazy dependency + /// Tries to access the lazy dependency without blocking. + /// + /// Returns `None` while the dependency has not been resolved yet. pub fn try_get(&self) -> Option, &InjectError>> { - if let Some(result) = self.0.once.get() { - return Some(result.as_ref()); - } - - // Lock receiver, so result is not taken out while we check - let mut recv = self.0.rx.lock().unwrap(); - - // Double check once - it might have been set while we waited for the lock - if let Some(result) = self.0.once.get() { - return Some(result.as_ref()); + if self.resolved.get().is_none() { + // Drive the shared future once with a no-op waker. This yields `None` + // while the backing oneshot has not produced a value yet. + let output = self.source.clone().now_or_never()?; + // First writer wins; concurrent callers just observe the same result. + let _ = self.resolved.set(Self::downcast(output)); } - // Otherwise, try to receive result, and set once - match recv.try_recv() { - Ok(Some(rx)) => { - let res = Lazy::::downcast_recv(rx); - self.0 - .once - .set(res) - .map_err(|_| ()) - .expect("holding lock on rx - this can't be set twice"); - - self.0.once.get().map(Result::as_ref) - } - Ok(None) => { - None - } - Err(_) => { - Some(Err(&InjectError::HandleClosed)) - } - } + self.resolved.get().map(Result::as_ref) } - /// Resolves as soon as the lazy is available - /// - /// Must not be waited on during module construction - // Note: Maybe Add a second DI stage (Injection, Pre Start) - where this is allowed - pub fn wait_result(&self) -> LazyFuture { - LazyFuture { lazy: &self.0 } - } -} -impl Lazy { - fn downcast_recv(recv: Result) -> Result, InjectError> { - match recv { - Ok(instance) => instance.downcast().map_err(|e| { - RequireError::DowncastFailed { - required_type: type_name::(), - actual_type: e, - } - .into() - }), - Err(e) => Err(e.into()), - } - } -} - -pin_project! { - struct LazyFuture<'a, T:Injectable> { - #[pin] - lazy: &'a LazyInner, + /// Resolves as soon as the lazy dependency is available. + /// + /// Must not be awaited during module construction - the dependency may not be + /// constructed yet at that point. + /// + /// # Errors + /// + /// See [`InjectError`] for possible errors during resolution. + /// + /// # Panics + /// - When accessed after DI has already failed. + /// - When accessed before the DI container init has completed. + // Note: Maybe add a second DI stage (Injection, Pre-Start) - where this is allowed. + pub async fn wait(&self) -> Result<&Arc, &InjectError> { + // Await the shared future so a real waker is registered. + let _ = self.source.clone().await; + self.try_get() + .expect("source resolved - try_get must return a value") } -} -impl<'a, T: Injectable> Future for LazyFuture<'a, T> { - type Output = &'a Result, InjectError>; - fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> std::task::Poll { - // Lock receiver, so result is not taken out while we check - let mut rx = self.lazy.rx.lock().unwrap(); - - // Check if result is ready - if let Some(result) = self.lazy.once.get() { - return Poll::Ready(result); - } - - // Poll recevier - match rx.poll_unpin(cx) { - Poll::Ready(recv) => { - // We have a result, handle it and set once lock - let res = match recv { - Ok(instance) => Lazy::::downcast_recv(instance), - Err(e) => Err(e.into()), - }; - - self.lazy - .once - .set(res) - .map_err(|_| ()) - .expect("holding lock on rx - this can't be set"); - - Poll::Ready(self.lazy.once.get().expect("just set once - must be set")) + /// Downcasts the resolved instance into the requested type. + fn downcast( + output: Result, oneshot::Canceled>, + ) -> Result, InjectError> { + let instance = output.map_err(|_| InjectError::HandleClosed)??; + instance.downcast::().map_err(|actual_type| { + RequireError::DowncastFailed { + required_type: type_name::(), + actual_type, } - Poll::Pending => Poll::Pending, - } + .into() + }) } } @@ -206,7 +170,7 @@ impl Resolver for LazyOption { where Self: Sized, { - Ok(LazyOption { + Ok(Self { lazy: Lazy::::resolve(handle).await?, }) } @@ -220,28 +184,25 @@ impl Resolver for LazyOption { } } impl LazyOption { - /// Accesses the Lazy Dependency - returning an error on access - pub fn try_get(&self) -> Option, &InjectError>> { - self.lazy.try_get() - } - /// Accesses the Lazy Dependency /// /// # Panics /// - If accessed after DI has failed + #[must_use] pub fn get(&self) -> Option<&Arc> { match self.lazy.try_get() { None => None, Some(Ok(result)) => Some(result), - Some(Err(err)) => match err { - InjectError::RequireError(RequireError::TypeDisabled(_)) - | InjectError::RequireError(RequireError::TypeMissing(_)) => { - None + Some(Err(err)) => { + match err { + InjectError::RequireError( + RequireError::TypeDisabled(_) | RequireError::TypeMissing(_), + ) => None, + err => { + panic!("Accessed LazyOption after DI failure: {err:?}"); + } } - err => { - panic!("Accessed LazyOption after DI failure: {:?}", err); - } - }, + } } } } diff --git a/features/wrapp-di/src/types.rs b/features/wrapp-di/src/types.rs index 4ad4d04..b10cdbc 100644 --- a/features/wrapp-di/src/types.rs +++ b/features/wrapp-di/src/types.rs @@ -20,12 +20,16 @@ pub struct Instance { impl Instance { pub(crate) fn new(instance: ExistingInstance) -> Self { - Instance { + Self { info: TypeInfo::of::(), instance: Arc::new(instance), } } + /// Attempts to downcast the instance to the requested type + /// + /// ### Errors + /// - Returns an error if the instance cannot be downcasted to the requested type pub fn downcast(&self) -> Result, &'static str> { match Arc::downcast::(self.instance.clone()) { Ok(downcasted) => Ok(downcasted), @@ -56,8 +60,9 @@ impl std::fmt::Display for TypeInfo { } } impl TypeInfo { - pub fn of() -> TypeInfo { - TypeInfo { + #[must_use] + pub fn of() -> Self { + Self { type_name: std::any::type_name::(), type_id: TypeId::of::(), } diff --git a/features/wrapp-router/Cargo.toml b/features/wrapp-router/Cargo.toml index 3194615..dc76f03 100644 --- a/features/wrapp-router/Cargo.toml +++ b/features/wrapp-router/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "wrapp-router" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + + +[lints] +workspace = true diff --git a/features/wrapp-router/src/lib.rs b/features/wrapp-router/src/lib.rs index 7d12d9a..56df860 100644 --- a/features/wrapp-router/src/lib.rs +++ b/features/wrapp-router/src/lib.rs @@ -1,4 +1,5 @@ -pub fn add(left: usize, right: usize) -> usize { +#[must_use] +pub const fn add(left: usize, right: usize) -> usize { left + right } diff --git a/features/wrapp-strategies/Cargo.toml b/features/wrapp-strategies/Cargo.toml index 9b1d373..6980ff5 100644 --- a/features/wrapp-strategies/Cargo.toml +++ b/features/wrapp-strategies/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "wrapp-strategies" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + + +[lints] +workspace = true diff --git a/features/wrapp-strategies/src/lib.rs b/features/wrapp-strategies/src/lib.rs index 7d12d9a..56df860 100644 --- a/features/wrapp-strategies/src/lib.rs +++ b/features/wrapp-strategies/src/lib.rs @@ -1,4 +1,5 @@ -pub fn add(left: usize, right: usize) -> usize { +#[must_use] +pub const fn add(left: usize, right: usize) -> usize { left + right } diff --git a/ideas.md b/ideas.md deleted file mode 100644 index 55cc5e7..0000000 --- a/ideas.md +++ /dev/null @@ -1,6 +0,0 @@ -# Strategies - -Instead of making modules mark themselves as X Module -Skip the magic, make a Strategy injector, which then has all relevant functions for adding endpoints. - -More flexibility on both the strategy side and on the user side. Strategy handle can have any kind of function. diff --git a/readme.md b/readme.md index e2aa14c..eaee5af 100644 --- a/readme.md +++ b/readme.md @@ -1,105 +1,103 @@ -# Why? -Building systems which are more than one thing at once (e.g. A WebAPI + Kafka Consumer) often forces -developers to either build more bootstrapping glue or to accept their applications to look chaotic. +# Wrapp -E.g., a typical multi-functional application might require: +**Create composable Rust Applications, cleanly separating business logic from infrastructure concerns.** -1. Setting up a web server for API endpoints -2. Configuring Kafka consumers for message processing -3. Initializing database connections -4. Setting up background job schedulers -5. Implementing logging and monitoring systems +> ⚠️ **Work in progress / experimental.** Wrapp is in early development (`v0.0.0`). The API is incomplete, unstable, and changes frequently. It is **not** ready for production use. Expect things to break. -Each of these components often requires its own initialization logic, configuration management, and integration with the rest of the application. This can lead to: +--- -- Complex and hard-to-maintain bootstrapping code -- Tight coupling between different parts of the application -- Difficulty in adding or removing features -- Challenges in testing individual components in isolation -- Inconsistent patterns across different parts of the system +## Why? -These issues can significantly increase the cognitive load on developers and make the codebase harder to evolve over time. -The wrapp ecosystem aims to address these challenges by providing a modular and extensible framework for building multi-functional applications. +Once an application exposes more than one interface, e.g. a web API, Kafka consumer, CLI, you usually end up writing bootstrapping code to wire everything together. -# How? +Each part needs its own setup, config, and wiring, often this leads to chaotic code and thus hard to maintain codebases. -1. Modular Architecture: - Wrapp encourages breaking down the application into smaller, self-contained modules. Each module encapsulates a specific Domain (e.g. User, Todos, Metrics). - Each domain on its own can then use one or multiple Strategies to define how it has to interact with the outside world. - E.g. User could be an Actix and a Kafka module, thus getting input from these sources. - - This approach offers several advantages: - - Improved maintainability: Each module can be developed, tested, and updated independently. - - Flexibility: Domains can easily adopt new strategies or remove existing ones without affecting other parts of the system. - - Scalability: As the application grows, new domains can be added as separate modules without disrupting existing functionality. - - Reusability: Well-defined modules can be reused across different projects or shared within a larger ecosystem. +Wrapp moves that wiring into reusable infrastructure so modules can focus on business logic. - The separation of domains and strategies allows for a clean architecture where business logic (domains) is decoupled from the technical implementation details (strategies), promoting better code organization and easier long-term maintenance. +Wrapp aims to make multi-functional applications **modular** by separating *what* your application does from *how* it talks to the outside world. +## Core idea -# Goal of wrapp: -Create an ecosystem which allows applications to compose themselves of different logical modules. -All modules share a DI framework, and strategies which determine how the outside world can interact with them. - -The DI Framework will allow a Module to declare other Modules it depends on - being optional, required or lazily. -The DI Framework will orchestrate the construction of all Modules, injecting dependencies as needed. - -The strategy system will allow modules to bootstrap themselves to e.g. the Webserver started by the application, or the Kafka Cluster connected to the app. -With the strategy system a module can declare itself as a WebModule (or probably more specific as an ActixModule, even though a general WebModule would be really cool). -This WebModule could then declare it's own routes, handlers, middlewares etc. which if the Module is registerd will attach themselves to the Server. -Then the Module could also declare itself as a KafkaModule, or a PostgresNotifyModule or a GRPC module, or a GraphQLModule etc. - -This way reusability components like a MetricsModule, a TaskQueueModule, and many more could be created which then -can simply be registered allowing their functionality work. - -To simplify the creation of Strategies Wrapp should also provide some useful tools. E.g. a simple Router which can be used to quickly build a new strategy. -In general it is a goal to make Strategies and Modules: - 1. As easy to implement as possible - 2. Backwards compatible for as long as possible +Wrapp takes care of the bootstrapping and wiring, so you can focus on your solutions. +It does this by providing three core concepts: +- **Modules** contain the logic of a domain (e.g. `User`, `Metrics`, `Health`). +- **Strategies** define how the outside world interacts with a module (e.g. an Actix web server or a Kafka consumer). A single module can opt into several strategies. +- A shared **DI (dependency injection)** framework wires everything together and constructs modules in the right order. +- Additional building blocks to simplify creating strategies ```mermaid graph LR; + wrapp --> wrapp-di + wrapp --> wrapp-strategies + wrapp --> wrapp-router + wrapp-strategies --> wrapp-actix + wrapp-di --> Module --> UserApp + wrapp-actix --> Module +``` -wrapp --> wrapp-di -wrapp --> wrapp-strategy -wrapp --> wrapp-router +For the full design rationale see [`docs/idea.md`](docs/idea.md). +For a work in progress API sketch see [`examples/prototype`](examples/prototype). + +### Non Goals + +- Wrapp is **not** a framework for a specific domain (e.g. web) +- Wrapp is **not** a replacement for existing frameworks or libraries (e.g. Actix, Tokio, Kafka clients) +- Wrapp is **not** trying to be compile-time validated. + +## Repository layout + +```text +wrapp/ # umbrella crate +features/ + wrapp-di/ # dependency injection + wrapp-config/ # config injection + wrapp-router/ # routing building block + wrapp-strategies/ # strategy trait +strategies/ # contains strategies for different frameworks + wrapp-actix/ +examples/ + prototype/ # design sketch of the target API +docs/ # scratchpad for ideas and roadmap +``` -wrapp-strategy --> wrapp-kafka -wrapp-strategy --> wrapp-actix -wrapp-strategy --> wrapp-sql-notify +## Development +This is a Cargo workspace, so the usual commands work: +```sh +cargo build +cargo test --all +``` -wrapp-di --> Module1 -wrapp-actix --> ActixWebModule --> Module1 --> UserApp -wrapp-kafka --> KafkaModule --> Module1 --> UserApp -wrapp-tauri --> TauriModule --> Module1 --> UserApp +A helper script bundles the common checks (format, lint, doc, dependency audit, tests): +```sh +./check.sh # run all checks +./check.sh fix # auto-fix formatting/lints where possible +./check.sh help # list available commands ``` -wrapp - overarching framework -wrapp-di - DI framework -wrapp-strategy - Trait for implementing new Strategies - e.g. a WebServer would be a strategy, or Listening to Kafka is a strategy -wrapp-router - Optional component to use for strategies - could e.g. allow routing through proc macros, and extracting app state +The toolchain is pinned via [`rust-toolchain.toml`](rust-toolchain.toml). + +## Roadmap + +See [`docs/todo`](docs/todo). + +## Contributing -E.g. +Wrapp is in an exploratory phase, so designs and APIs are still in flux. +Issues and ideas are welcome - but expect significant churn before anything stabilizes. -wrapp-actixs implements wrapp-strategy -wrapp-actixs thus has hooks to: collect all the routes and config it needs and start the actix server -wrapp-actixs exports a trait wrapp-actix-module, which if implemented by a module, has the functions needed to collect all routes and config -since actix has it's own router, it will not use wrapp-router, but the wrapp-axtic strategy will add the finished DI container to actix's app state, and will provide an etractor like WrappState, to access data from the Wrapp-DI +## License -or +Licensed under either of -wrapp-kafka implements wrapp-strategy -wrapp-kafka has a hook to collect all routes for messages, and has an implementation to start clients to listen to kafka -it exports a trait wrapp-kafka-module, which if implemented... -since this does not have it's own router, it can opt in to use the wrapp router, automatically allowing it to use e.g. middlewares and injection on it's endpoints. +- Apache License, Version 2.0 (LICENSE-APACHE or ) +- MIT license (LICENSE-MIT or ) -Possible Issues: -1. Version conflicts -If module has serverely different versions of e.g. wrapp-actixs, this could lead to annoyances +#### Contribution +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..d33ead3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.96.0" +profile = "default" +components = ["rust-analyzer", "rustfmt", "clippy"] diff --git a/strategies/wrapp-actix/Cargo.toml b/strategies/wrapp-actix/Cargo.toml index d050981..0769bae 100644 --- a/strategies/wrapp-actix/Cargo.toml +++ b/strategies/wrapp-actix/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "wrapp-actix" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + + +[lints] +workspace = true diff --git a/strategies/wrapp-actix/src/lib.rs b/strategies/wrapp-actix/src/lib.rs index 7d12d9a..56df860 100644 --- a/strategies/wrapp-actix/src/lib.rs +++ b/strategies/wrapp-actix/src/lib.rs @@ -1,4 +1,5 @@ -pub fn add(left: usize, right: usize) -> usize { +#[must_use] +pub const fn add(left: usize, right: usize) -> usize { left + right } diff --git a/todo b/todo deleted file mode 100644 index 5813f9c..0000000 --- a/todo +++ /dev/null @@ -1,15 +0,0 @@ -[~] Wrapp - [~] DI - [x] Implementation - [] Proc Macro - - [] Config - [] Implement Config Provider - [] Implement Injector - - [] Strategies - [] Figure out how to pass strategies - [] Axum-Utoipa or Actix-Utoipa - - [] Routing - [] See if we could reuse something existing - maybe Tower? \ No newline at end of file diff --git a/wrapp/Cargo.toml b/wrapp/Cargo.toml index 22d42e0..a2501fa 100644 --- a/wrapp/Cargo.toml +++ b/wrapp/Cargo.toml @@ -1,8 +1,13 @@ [package] name = "wrapp" -version = "0.1.0" -edition = "2021" +version = { workspace = true } +edition = { workspace = true } +license = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] + + +[lints] +workspace = true diff --git a/wrapp/src/lib.rs b/wrapp/src/lib.rs index 7d12d9a..56df860 100644 --- a/wrapp/src/lib.rs +++ b/wrapp/src/lib.rs @@ -1,4 +1,5 @@ -pub fn add(left: usize, right: usize) -> usize { +#[must_use] +pub const fn add(left: usize, right: usize) -> usize { left + right }