Compare commits

...

39 Commits

Author SHA1 Message Date
Guillermo Céspedes Tabárez 9b50997153
Merge dfb4aa3baf into b6ad483699 2025-07-14 23:40:18 -07:00
dependabot[bot] b6ad483699
build(deps): bump tokio from 1.45.1 to 1.46.1 (#3689)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.45.1 to 1.46.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.45.1...tokio-1.46.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.46.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 01:52:41 +00:00
dependabot[bot] c002fd783c
build(deps): bump taiki-e/install-action from 2.54.3 to 2.56.13 (#3693)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.54.3 to 2.56.13.
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/v2.54.3...v2.56.13)

---
updated-dependencies:
- dependency-name: taiki-e/install-action
  dependency-version: 2.56.13
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 01:16:19 +00:00
Thomas de Zeeuw 95ad1caa23
Update socket2 to v0.6 (#3688) 2025-07-14 01:13:01 +00:00
dependabot[bot] eb906d077a
build(deps): bump trybuild from 1.0.105 to 1.0.106 (#3692)
Bumps [trybuild](https://github.com/dtolnay/trybuild) from 1.0.105 to 1.0.106.
- [Release notes](https://github.com/dtolnay/trybuild/releases)
- [Commits](https://github.com/dtolnay/trybuild/compare/1.0.105...1.0.106)

---
updated-dependencies:
- dependency-name: trybuild
  dependency-version: 1.0.106
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 01:01:17 +00:00
dependabot[bot] f08fa6b684
build(deps): bump h2 from 0.3.26 to 0.3.27 (#3691)
Bumps [h2](https://github.com/hyperium/h2) from 0.3.26 to 0.3.27.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.27/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.26...v0.3.27)

---
updated-dependencies:
- dependency-name: h2
  dependency-version: 0.3.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-14 00:50:00 +00:00
Guillermo Céspedes Tabárez dfb4aa3baf
Merge branch 'master' into introspection 2025-06-17 19:09:21 -03:00
dertin 7ff7768dc4 feat(introspection): implement experimental introspection feature with multiple App instances 2025-06-11 17:37:26 -03:00
Guillermo Céspedes Tabárez 2b52a60bc2
Merge branch 'master' into introspection 2025-06-10 20:50:56 -03:00
Guillermo Céspedes Tabárez 7296f6ffdb
Merge branch 'master' into introspection 2025-05-27 01:50:58 -03:00
Guillermo Céspedes Tabárez d1706dcdd6 Merge branch 'introspection' of github.com:dertin/actix-web into introspection 2025-05-19 21:57:12 -03:00
Guillermo Céspedes Tabárez 23fed2298e feat(introspection): enhance introspection handlers for JSON and plain text responses 2025-05-19 21:55:49 -03:00
Guillermo Céspedes Tabárez 360baa3130
Merge branch 'master' into introspection 2025-05-19 17:55:08 -03:00
Guillermo Céspedes Tabárez c8a7271d21 optimize debug log and apply clippy/fmt suggestions 2025-05-19 02:41:48 -03:00
Guillermo Céspedes Tabárez 0a9f6c1955 feat(introspection): enhance introspection feature with detailed route registration and full path tracking 2025-05-18 21:19:34 -03:00
Guillermo Céspedes Tabárez 2f64cdb60a fix Cargo.lock 2025-05-12 02:57:39 -03:00
Guillermo Céspedes Tabárez dcad1d9b89 Merge branch 'master' into introspection 2025-05-12 02:37:52 -03:00
Guillermo Céspedes Tabárez d501102610 feat(introspection): rename feature from `resources-introspection` to `experimental-introspection`
- Refactored introspection logic.
- Enhanced route introspection to register HTTP methods and guard names.
- Added example for testing the experimental introspection feature.
2025-05-12 02:29:11 -03:00
Guillermo Céspedes Tabárez 013a8ec6b6
Merge branch 'master' into introspection 2025-04-22 14:24:42 -03:00
Guillermo Céspedes Tabárez c809ee8440
Merge branch 'master' into introspection 2025-04-10 18:10:16 -03:00
Guillermo Céspedes Tabárez 585552f70f
Merge branch 'master' into introspection 2025-04-01 22:39:21 -03:00
Guillermo Céspedes Tabárez 57b5937db3
Merge branch 'master' into introspection 2025-03-21 16:21:25 -03:00
Guillermo Céspedes Tabárez 3506512ed9
Merge branch 'master' into introspection 2025-03-10 16:40:42 -03:00
Guillermo Céspedes Tabárez a449695661 Merge branch 'introspection' of https://github.com/dertin/actix-web into introspection 2025-03-05 02:37:44 -03:00
Guillermo Céspedes Tabárez aebab17c1e refactor(introspection): add GuardDetail enum and remove downcast_ref usage
- Added `GuardDetail` enum to encapsulate various introspection details of a guard.
- Refactored `HttpMethodsExtractor` implementation to use `GuardDetail` instead of `downcast_ref`.
2025-03-05 02:37:12 -03:00
Guillermo Céspedes Tabárez ae08dcf6dc refactor(introspection): add GuardDetail enum and remove downcast_ref usage
- Added `GuardDetail` enum to encapsulate various introspection details of a guard.
- Refactored `HttpMethodsExtractor` implementation to use `GuardDetail` instead of `downcast_ref`.
2025-03-05 02:34:39 -03:00
Guillermo Céspedes Tabárez ee7621594c Merge branch 'introspection' of https://github.com/dertin/actix-web into introspection 2025-03-04 05:50:41 -03:00
Guillermo Céspedes Tabárez 0548b127c3 Merge branch 'introspection' of https://github.com/dertin/actix-web into introspection 2025-03-04 05:50:04 -03:00
Guillermo Céspedes Tabárez 7821ba9c1b Merge branch 'introspection' of https://github.com/dertin/actix-web into introspection 2025-03-04 05:42:54 -03:00
Guillermo Céspedes Tabárez 3b99f86e89 fix(introspection): add conditional arbiter creation for io-uring support 2025-03-04 05:42:07 -03:00
Guillermo Céspedes Tabárez 2bb774a529 fix(introspection): add conditional arbiter creation for io-uring support 2025-03-04 05:36:49 -03:00
Guillermo Céspedes Tabárez a79dc9dc79 refactor: improve thread safety and add unit tests for introspection process 2025-03-04 05:09:18 -03:00
Guillermo Céspedes Tabárez c4be19942b ci: downgrade for msrv zerofrom to version 0.1.5 in justfile 2025-03-03 16:25:50 -03:00
Guillermo Céspedes Tabárez acd7c58097 chore: update changelog and fix docs for CI 2025-03-03 16:12:31 -03:00
Guillermo Céspedes Tabárez 176ea5da77 ci: downgrade for msrv litemap to version 0.7.4 in justfile 2025-03-03 16:05:01 -03:00
Guillermo Céspedes Tabárez 91fa813f0e fix(guards): replace take_guards with get_guards to prevent guard removal and fix test failures 2025-03-03 15:31:20 -03:00
Guillermo Céspedes Tabárez 819ee93e9b style: cargo fmt 2025-03-03 06:46:52 -03:00
Guillermo Céspedes Tabárez 041322ef9c misc: remove debug print 2025-03-03 06:10:45 -03:00
Guillermo Céspedes Tabárez 0d87cc53a1 feat(resources-introspection): add support for resource metadata retrieval 2025-03-03 05:27:52 -03:00
19 changed files with 934 additions and 67 deletions

View File

@ -49,7 +49,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.54.3
uses: taiki-e/install-action@v2.56.13
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -83,7 +83,7 @@ jobs:
uses: actions-rust-lang/setup-rust-toolchain@v1.13.0
- name: Install just, cargo-hack
uses: taiki-e/install-action@v2.54.3
uses: taiki-e/install-action@v2.56.13
with:
tool: just,cargo-hack

View File

@ -64,7 +64,7 @@ jobs:
toolchain: ${{ matrix.version.version }}
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean
uses: taiki-e/install-action@v2.54.3
uses: taiki-e/install-action@v2.56.13
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
@ -113,7 +113,7 @@ jobs:
toolchain: nightly
- name: Install just
uses: taiki-e/install-action@v2.54.3
uses: taiki-e/install-action@v2.56.13
with:
tool: just

View File

@ -24,7 +24,7 @@ jobs:
components: llvm-tools
- name: Install just, cargo-llvm-cov, cargo-nextest
uses: taiki-e/install-action@v2.54.3
uses: taiki-e/install-action@v2.56.13
with:
tool: just,cargo-llvm-cov,cargo-nextest

View File

@ -77,7 +77,7 @@ jobs:
toolchain: ${{ vars.RUST_VERSION_EXTERNAL_TYPES }}
- name: Install just
uses: taiki-e/install-action@v2.54.3
uses: taiki-e/install-action@v2.56.13
with:
tool: just

192
Cargo.lock generated
View File

@ -147,7 +147,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"slab",
"socket2 0.5.10",
"socket2 0.6.0",
"tokio",
]
@ -393,7 +393,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"smallvec",
"socket2 0.5.10",
"socket2 0.6.0",
"static_assertions",
"time",
"tokio",
@ -817,9 +817,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.2.22"
version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [
"jobserver",
"libc",
@ -891,18 +891,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.37"
version = "4.5.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
dependencies = [
"anstyle",
"clap_lex",
@ -1299,9 +1299,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.11"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.59.0",
@ -1509,9 +1509,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "h2"
version = "0.3.26"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
@ -1785,6 +1785,17 @@ dependencies = [
"libc",
]
[[package]]
name = "io-uring"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"libc",
]
[[package]]
name = "ipconfig"
version = "0.3.2"
@ -1914,12 +1925,12 @@ checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.53.0",
]
[[package]]
@ -2488,9 +2499,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "resolv-conf"
version = "0.7.3"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7c8f7f733062b66dc1c63f9db168ac0b97a9210e247fa90fdc9ad08f51b302"
checksum = "95325155c684b1c89f7765e30bc1c42e4a6da51ca513615660cb8a62ef9a88e3"
[[package]]
name = "ring"
@ -2794,9 +2805,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.8"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
dependencies = [
"serde",
]
@ -2882,6 +2893,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "spin"
version = "0.5.2"
@ -3066,17 +3087,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.1"
version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
dependencies = [
"backtrace",
"bytes",
"io-uring 0.7.8",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2 0.5.10",
"tokio-macros",
"windows-sys 0.52.0",
@ -3165,7 +3188,7 @@ checksum = "748482e3e13584a34664a710168ad5068e8cb1d968aa4ffa887e83ca6dd27967"
dependencies = [
"bytes",
"futures-util",
"io-uring",
"io-uring 0.6.4",
"libc",
"slab",
"socket2 0.4.10",
@ -3187,44 +3210,42 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.22"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"toml_write",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_write"
version = "0.1.1"
name = "toml_datetime"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
dependencies = [
"serde",
]
[[package]]
name = "toml_parser"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
[[package]]
name = "tracing"
@ -3306,9 +3327,9 @@ dependencies = [
[[package]]
name = "trybuild"
version = "1.0.105"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c9bf9513a2f4aeef5fdac8677d7d349c79fdbcc03b9c86da6e9d254f1e43be2"
checksum = "65af40ad689f2527aebbd37a0a816aea88ff5f774ceabe99de5be02f2f91dae2"
dependencies = [
"glob",
"serde",
@ -3657,13 +3678,29 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@ -3676,6 +3713,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@ -3688,6 +3731,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@ -3700,12 +3749,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[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_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@ -3718,6 +3779,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@ -3730,6 +3797,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@ -3742,6 +3815,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@ -3754,14 +3833,17 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
[[package]]
name = "winreg"

View File

@ -53,7 +53,7 @@ serde = "1"
serde_json = "1"
serde_urlencoded = "0.7"
slab = "0.4"
socket2 = "0.5"
socket2 = "0.6"
tls-openssl = { version = "0.10.55", package = "openssl", optional = true }
tokio = { version = "1.38.2", features = ["sync"] }

View File

@ -2,6 +2,8 @@
## Unreleased
- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods.
## 4.11.0
- Add `Logger::log_level()` method.

View File

@ -123,6 +123,9 @@ compat = ["compat-routing-macros-force-pub"]
# Opt-out forwards-compatibility for handler visibility inheritance fix.
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
experimental-introspection = []
[dependencies]
actix-codec = "0.5"
actix-macros = { version = "0.2.3", optional = true }
@ -158,7 +161,7 @@ serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6.1"
socket2 = "0.5"
socket2 = "0.6"
time = { version = "0.3", default-features = false, features = ["formatting"] }
tracing = "0.1.30"
url = "2.5.4"

View File

@ -0,0 +1,255 @@
// Example showcasing the experimental introspection feature.
// Run with: `cargo run --features experimental-introspection --example introspection`
#[actix_web::main]
async fn main() -> std::io::Result<()> {
#[cfg(feature = "experimental-introspection")]
{
use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;
// Initialize logging
env_logger::Builder::new()
.filter_level(log::LevelFilter::Debug)
.init();
// Custom guard to check if the Content-Type header is present.
struct ContentTypeGuard;
impl guard::Guard for ContentTypeGuard {
fn check(&self, req: &guard::GuardContext<'_>) -> bool {
req.head()
.headers()
.contains_key(actix_web::http::header::CONTENT_TYPE)
}
}
// Data structure for endpoints that receive JSON.
#[derive(Deserialize)]
struct UserInfo {
username: String,
age: u8,
}
// GET /introspection for JSON response
async fn introspection_handler_json(
tree: web::Data<actix_web::introspection::IntrospectionTree>,
) -> impl Responder {
let report = tree.report_as_json();
HttpResponse::Ok()
.content_type("application/json")
.body(report)
}
// GET /introspection for plain text response
async fn introspection_handler_text(
tree: web::Data<actix_web::introspection::IntrospectionTree>,
) -> impl Responder {
let report = tree.report_as_text();
HttpResponse::Ok().content_type("text/plain").body(report)
}
// GET /api/v1/item/{id} and GET /v1/item/{id}
#[actix_web::get("/item/{id}")]
async fn get_item(path: web::Path<u32>) -> impl Responder {
let id = path.into_inner();
HttpResponse::Ok().body(format!("Requested item with id: {}", id))
}
// POST /api/v1/info
#[actix_web::post("/info")]
async fn post_user_info(info: web::Json<UserInfo>) -> impl Responder {
HttpResponse::Ok().json(format!(
"User {} with age {} received",
info.username, info.age
))
}
// /api/v1/guarded
async fn guarded_handler() -> impl Responder {
HttpResponse::Ok().body("Passed the Content-Type guard!")
}
// GET /api/v2/hello
async fn hello_v2() -> impl Responder {
HttpResponse::Ok().body("Hello from API v2!")
}
// GET /admin/dashboard
async fn admin_dashboard() -> impl Responder {
HttpResponse::Ok().body("Welcome to the Admin Dashboard!")
}
// GET /admin/settings
async fn get_settings() -> impl Responder {
HttpResponse::Ok().body("Current settings: ...")
}
// POST /admin/settings
async fn update_settings() -> impl Responder {
HttpResponse::Ok().body("Settings have been updated!")
}
// GET and POST on /
async fn root_index() -> impl Responder {
HttpResponse::Ok().body("Welcome to the Root Endpoint!")
}
// Additional endpoints for /extra
fn extra_endpoints(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/extra")
.route(
"/ping",
web::get().to(|| async { HttpResponse::Ok().body("pong") }), // GET /extra/ping
)
.service(
web::resource("/multi")
.route(web::get().to(|| async {
HttpResponse::Ok().body("GET response from /extra/multi")
})) // GET /extra/multi
.route(web::post().to(|| async {
HttpResponse::Ok().body("POST response from /extra/multi")
})), // POST /extra/multi
)
.service(
web::scope("{entities_id:\\d+}")
.service(
web::scope("/secure")
.route(
"",
web::get().to(|| async {
HttpResponse::Ok()
.body("GET response from /extra/secure")
}),
) // GET /extra/{entities_id}/secure/
.route(
"/post",
web::post().to(|| async {
HttpResponse::Ok()
.body("POST response from /extra/secure")
}),
), // POST /extra/{entities_id}/secure/post
)
.wrap_fn(|req, srv| {
println!(
"Request to /extra/secure with id: {}",
req.match_info().get("entities_id").unwrap()
);
let fut = srv.call(req);
async move {
let res = fut.await?;
Ok(res)
}
}),
),
);
}
// Additional endpoints for /foo
fn other_endpoints(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/extra")
.route(
"/ping",
web::post()
.to(|| async { HttpResponse::Ok().body("post from /extra/ping") }), // POST /foo/extra/ping
)
.route(
"/ping",
web::delete()
.to(|| async { HttpResponse::Ok().body("delete from /extra/ping") }), // DELETE /foo/extra/ping
),
);
}
// Create the HTTP server with all the routes and handlers
let server = HttpServer::new(|| {
App::new()
// Get introspection report
// curl --location '127.0.0.1:8080/introspection' --header 'Accept: application/json'
// curl --location '127.0.0.1:8080/introspection' --header 'Accept: text/plain'
.service(
web::resource("/introspection")
.route(
web::get()
.guard(guard::Header("accept", "application/json"))
.to(introspection_handler_json),
)
.route(
web::get()
.guard(guard::Header("accept", "text/plain"))
.to(introspection_handler_text),
),
)
// API endpoints under /api
.service(
web::scope("/api")
// Endpoints under /api/v1
.service(
web::scope("/v1")
.service(get_item) // GET /api/v1/item/{id}
.service(post_user_info) // POST /api/v1/info
.route(
"/guarded",
web::route().guard(ContentTypeGuard).to(guarded_handler), // /api/v1/guarded
),
)
// Endpoints under /api/v2
.service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), // GET /api/v2/hello
)
// Endpoints under /v1 (outside /api)
.service(web::scope("/v1").service(get_item)) // GET /v1/item/{id}
// Admin endpoints under /admin
.service(
web::scope("/admin")
.route("/dashboard", web::get().to(admin_dashboard)) // GET /admin/dashboard
.service(
web::resource("/settings")
.route(web::get().to(get_settings)) // GET /admin/settings
.route(web::post().to(update_settings)), // POST /admin/settings
),
)
// Root endpoints
.service(
web::resource("/")
.route(web::get().to(root_index)) // GET /
.route(web::post().to(root_index)), // POST /
)
// Endpoints under /bar
.service(web::scope("/bar").configure(extra_endpoints)) // /bar/extra/ping, /bar/extra/multi, etc.
// Endpoints under /foo
.service(web::scope("/foo").configure(other_endpoints)) // /foo/extra/ping with POST and DELETE
// Additional endpoints under /extra
.configure(extra_endpoints) // /extra/ping, /extra/multi, etc.
.configure(other_endpoints)
// Endpoint that rejects GET on /not_guard (allows other methods)
.route(
"/not_guard",
web::route()
.guard(guard::Not(guard::Get()))
.to(HttpResponse::MethodNotAllowed),
)
// Endpoint that requires GET with header or POST on /all_guard
.route(
"/all_guard",
web::route()
.guard(
guard::All(guard::Get())
.and(guard::Header("content-type", "plain/text"))
.and(guard::Any(guard::Post())),
)
.to(HttpResponse::MethodNotAllowed),
)
})
.workers(5)
.bind("127.0.0.1:8080")?;
server.run().await
}
#[cfg(not(feature = "experimental-introspection"))]
{
eprintln!("This example requires the 'experimental-introspection' feature to be enabled.");
std::process::exit(1);
}
}

View File

@ -0,0 +1,52 @@
// Example showcasing the experimental introspection feature with multiple App instances.
// Run with: `cargo run --features experimental-introspection --example introspection_multi_servers`
#[actix_web::main]
async fn main() -> std::io::Result<()> {
#[cfg(feature = "experimental-introspection")]
{
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use futures_util::future;
async fn introspection_handler(
tree: web::Data<actix_web::introspection::IntrospectionTree>,
) -> impl Responder {
HttpResponse::Ok()
.content_type("text/plain")
.body(tree.report_as_text())
}
async fn index() -> impl Responder {
HttpResponse::Ok().body("Hello from app")
}
let srv1 = HttpServer::new(|| {
App::new()
.service(web::resource("/a").route(web::get().to(index)))
.service(
web::resource("/introspection").route(web::get().to(introspection_handler)),
)
})
.workers(8)
.bind("127.0.0.1:8081")?
.run();
let srv2 = HttpServer::new(|| {
App::new()
.service(web::resource("/b").route(web::get().to(index)))
.service(
web::resource("/introspection").route(web::get().to(introspection_handler)),
)
})
.workers(3)
.bind("127.0.0.1:8082")?
.run();
future::try_join(srv1, srv2).await?;
}
#[cfg(not(feature = "experimental-introspection"))]
{
eprintln!("This example requires the 'experimental-introspection' feature to be enabled.");
}
Ok(())
}

View File

@ -30,6 +30,8 @@ pub struct App<T> {
data_factories: Vec<FnDataFactory>,
external: Vec<ResourceDef>,
extensions: Extensions,
#[cfg(feature = "experimental-introspection")]
introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
}
impl App<AppEntry> {
@ -46,6 +48,10 @@ impl App<AppEntry> {
factory_ref,
external: Vec::new(),
extensions: Extensions::new(),
#[cfg(feature = "experimental-introspection")]
introspector: Rc::new(RefCell::new(
crate::introspection::IntrospectionCollector::new(),
)),
}
}
}
@ -366,6 +372,8 @@ where
factory_ref: self.factory_ref,
external: self.external,
extensions: self.extensions,
#[cfg(feature = "experimental-introspection")]
introspector: self.introspector,
}
}
@ -429,6 +437,8 @@ where
factory_ref: self.factory_ref,
external: self.external,
extensions: self.extensions,
#[cfg(feature = "experimental-introspection")]
introspector: self.introspector,
}
}
}
@ -453,6 +463,8 @@ where
default: self.default,
factory_ref: self.factory_ref,
extensions: RefCell::new(Some(self.extensions)),
#[cfg(feature = "experimental-introspection")]
introspector: Rc::clone(&self.introspector),
}
}
}

View File

@ -41,6 +41,8 @@ where
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
pub(crate) external: RefCell<Vec<ResourceDef>>,
#[cfg(feature = "experimental-introspection")]
pub(crate) introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
}
impl<T, B> ServiceFactory<Request> for AppInit<T, B>
@ -72,6 +74,10 @@ where
// create App config to pass to child services
let mut config = AppService::new(config, Rc::clone(&default));
#[cfg(feature = "experimental-introspection")]
{
config.introspector = Rc::clone(&self.introspector);
}
// register services
mem::take(&mut *self.services.borrow_mut())
@ -80,6 +86,9 @@ where
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
#[cfg(feature = "experimental-introspection")]
let (config, services, _) = config.into_services();
#[cfg(not(feature = "experimental-introspection"))]
let (config, services) = config.into_services();
// complete pipeline creation.
@ -110,6 +119,8 @@ where
// construct app service and middleware service factory future.
let endpoint_fut = self.endpoint.new_service(());
#[cfg(feature = "experimental-introspection")]
let introspector = Rc::clone(&self.introspector);
// take extensions or create new one as app data container.
let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default();
@ -130,6 +141,12 @@ where
factory.create(&mut app_data);
}
#[cfg(feature = "experimental-introspection")]
{
let tree = introspector.borrow_mut().finalize();
app_data.insert(crate::web::Data::new(tree));
}
Ok(AppInitService {
service,
app_data: Rc::new(app_data),

View File

@ -30,6 +30,11 @@ pub struct AppService {
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
#[cfg(feature = "experimental-introspection")]
pub current_prefix: String,
#[cfg(feature = "experimental-introspection")]
pub(crate) introspector:
std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
}
impl AppService {
@ -40,6 +45,12 @@ impl AppService {
default,
root: true,
services: Vec::new(),
#[cfg(feature = "experimental-introspection")]
current_prefix: "".to_string(),
#[cfg(feature = "experimental-introspection")]
introspector: std::rc::Rc::new(std::cell::RefCell::new(
crate::introspection::IntrospectionCollector::new(),
)),
}
}
@ -49,6 +60,24 @@ impl AppService {
}
#[allow(clippy::type_complexity)]
#[cfg(feature = "experimental-introspection")]
pub(crate) fn into_services(
self,
) -> (
AppConfig,
Vec<(
ResourceDef,
BoxedHttpServiceFactory,
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
) {
(self.config, self.services, self.introspector)
}
#[allow(clippy::type_complexity)]
#[cfg(not(feature = "experimental-introspection"))]
pub(crate) fn into_services(
self,
) -> (
@ -71,6 +100,10 @@ impl AppService {
default: Rc::clone(&self.default),
services: Vec::new(),
root: false,
#[cfg(feature = "experimental-introspection")]
current_prefix: self.current_prefix.clone(),
#[cfg(feature = "experimental-introspection")]
introspector: std::rc::Rc::clone(&self.introspector),
}
}
@ -101,9 +134,70 @@ impl AppService {
InitError = (),
> + 'static,
{
#[cfg(feature = "experimental-introspection")]
{
use std::borrow::Borrow;
// Build the full path for introspection
let pat = rdef.pattern().unwrap_or("").to_string();
let full_path = if self.current_prefix.is_empty() {
pat.clone()
} else {
format!(
"{}/{}",
self.current_prefix.trim_end_matches('/'),
pat.trim_start_matches('/')
)
};
// Extract methods and guards for introspection
let guard_list: &[Box<dyn Guard>] = guards.borrow().as_ref().map_or(&[], |v| &v[..]);
let methods = guard_list
.iter()
.flat_map(|g| g.details().unwrap_or_default())
.flat_map(|d| {
if let crate::guard::GuardDetail::HttpMethods(v) = d {
v.into_iter()
.filter_map(|s| s.parse().ok())
.collect::<Vec<_>>()
} else {
Vec::new()
}
})
.collect::<Vec<_>>();
let guard_names = guard_list
.iter()
.map(|g| g.name().to_string())
.collect::<Vec<_>>();
// Determine if the registered service is a resource
let is_resource = rdef.pattern().is_some();
self.introspector.borrow_mut().register_pattern_detail(
full_path,
methods,
guard_names,
is_resource,
);
}
self.services
.push((rdef, boxed::factory(factory.into_factory()), guards, nested));
}
/// Update the current path prefix.
#[cfg(feature = "experimental-introspection")]
pub(crate) fn update_prefix(&mut self, prefix: &str) {
self.current_prefix = if self.current_prefix.is_empty() {
prefix.to_string()
} else {
format!(
"{}/{}",
self.current_prefix.trim_end_matches('/'),
prefix.trim_start_matches('/')
)
};
}
}
/// Application connection config.

View File

@ -11,7 +11,7 @@
//! or handler. This interface is defined by the [`Guard`] trait.
//!
//! Commonly-used guards are provided in this module as well as a way of creating a guard from a
//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be
//! closure ([`fn_guard`]). The [`Not`], [`Any()`], and [`All()`] guards are noteworthy, as they can be
//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on
//! services multiple times (which might have different combining behavior than you want).
//!
@ -66,6 +66,17 @@ pub use self::{
host::{Host, HostGuard},
};
/// Enum to encapsulate various introspection details of a Guard.
#[derive(Debug, Clone)]
pub enum GuardDetail {
/// Detail associated with HTTP methods.
HttpMethods(Vec<String>),
/// Detail associated with headers (header, value).
Headers(Vec<(String, String)>),
/// Generic detail.
Generic(String),
}
/// Provides access to request parts that are useful during routing.
#[derive(Debug)]
pub struct GuardContext<'a> {
@ -124,12 +135,28 @@ impl<'a> GuardContext<'a> {
pub trait Guard {
/// Returns true if predicate condition is met for a given request.
fn check(&self, ctx: &GuardContext<'_>) -> bool;
/// Returns a nominal representation of the guard.
fn name(&self) -> String {
std::any::type_name::<Self>().to_string()
}
/// Returns detailed introspection information.
fn details(&self) -> Option<Vec<GuardDetail>> {
None
}
}
impl Guard for Rc<dyn Guard> {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
(**self).check(ctx)
}
fn name(&self) -> String {
(**self).name()
}
fn details(&self) -> Option<Vec<GuardDetail>> {
(**self).details()
}
}
/// Creates a guard using the given function.
@ -195,7 +222,7 @@ pub fn Any<F: Guard + 'static>(guard: F) -> AnyGuard {
///
/// That is, only one contained guard needs to match in order for the aggregate guard to match.
///
/// Construct an `AnyGuard` using [`Any`].
/// Construct an `AnyGuard` using [`Any()`].
pub struct AnyGuard {
guards: Vec<Box<dyn Guard>>,
}
@ -219,6 +246,24 @@ impl Guard for AnyGuard {
false
}
fn name(&self) -> String {
format!(
"AnyGuard({})",
self.guards
.iter()
.map(|g| g.name())
.collect::<Vec<_>>()
.join(", ")
)
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(
self.guards
.iter()
.flat_map(|g| g.details().unwrap_or_default())
.collect(),
)
}
}
/// Creates a guard that matches if all added guards match.
@ -247,7 +292,7 @@ pub fn All<F: Guard + 'static>(guard: F) -> AllGuard {
///
/// That is, **all** contained guard needs to match in order for the aggregate guard to match.
///
/// Construct an `AllGuard` using [`All`].
/// Construct an `AllGuard` using [`All()`].
pub struct AllGuard {
guards: Vec<Box<dyn Guard>>,
}
@ -271,6 +316,24 @@ impl Guard for AllGuard {
true
}
fn name(&self) -> String {
format!(
"AllGuard({})",
self.guards
.iter()
.map(|g| g.name())
.collect::<Vec<_>>()
.join(", ")
)
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(
self.guards
.iter()
.flat_map(|g| g.details().unwrap_or_default())
.collect(),
)
}
}
/// Wraps a guard and inverts the outcome of its `Guard` implementation.
@ -291,6 +354,12 @@ impl<G: Guard> Guard for Not<G> {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
!self.0.check(ctx)
}
fn name(&self) -> String {
format!("Not({})", self.0.name())
}
fn details(&self) -> Option<Vec<GuardDetail>> {
self.0.details()
}
}
/// Creates a guard that matches a specified HTTP method.
@ -320,6 +389,12 @@ impl Guard for MethodGuard {
ctx.head().method == self.0
}
fn name(&self) -> String {
self.0.to_string()
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])])
}
}
macro_rules! method_guard {
@ -382,6 +457,15 @@ impl Guard for HeaderGuard {
false
}
fn name(&self) -> String {
format!("Header({}, {})", self.0, self.1.to_str().unwrap_or(""))
}
fn details(&self) -> Option<Vec<GuardDetail>> {
Some(vec![GuardDetail::Headers(vec![(
self.0.to_string(),
self.1.to_str().unwrap_or("").to_string(),
)])])
}
}
#[cfg(test)]

View File

@ -0,0 +1,209 @@
use std::{collections::HashMap, fmt::Write as FmtWrite};
use serde::Serialize;
use crate::http::Method;
#[derive(Clone)]
pub struct RouteDetail {
methods: Vec<Method>,
guards: Vec<String>,
is_resource: bool,
}
#[derive(Debug, Clone, Copy)]
pub enum ResourceType {
App,
Scope,
Resource,
}
#[derive(Debug, Clone)]
pub struct IntrospectionNode {
pub kind: ResourceType,
pub pattern: String,
pub full_path: String,
pub methods: Vec<Method>,
pub guards: Vec<String>,
pub children: Vec<IntrospectionNode>,
}
#[derive(Debug, Clone, Serialize)]
pub struct IntrospectionReportItem {
pub full_path: String,
pub methods: Vec<String>,
pub guards: Vec<String>,
}
impl IntrospectionNode {
pub fn new(kind: ResourceType, pattern: String, full_path: String) -> Self {
IntrospectionNode {
kind,
pattern,
full_path,
methods: Vec::new(),
guards: Vec::new(),
children: Vec::new(),
}
}
}
impl From<&IntrospectionNode> for Vec<IntrospectionReportItem> {
fn from(node: &IntrospectionNode) -> Self {
fn collect_report_items(
node: &IntrospectionNode,
parent_path: &str,
report_items: &mut Vec<IntrospectionReportItem>,
) {
let full_path = if parent_path.is_empty() {
node.pattern.clone()
} else {
format!(
"{}/{}",
parent_path.trim_end_matches('/'),
node.pattern.trim_start_matches('/')
)
};
if !node.methods.is_empty() || !node.guards.is_empty() {
let filtered_guards: Vec<String> = node
.guards
.iter()
.filter(|guard| !node.methods.iter().any(|m| m.to_string() == **guard))
.cloned()
.collect();
report_items.push(IntrospectionReportItem {
full_path: full_path.clone(),
methods: node.methods.iter().map(|m| m.to_string()).collect(),
guards: filtered_guards,
});
}
for child in &node.children {
collect_report_items(child, &full_path, report_items);
}
}
let mut report_items = Vec::new();
collect_report_items(node, "/", &mut report_items);
report_items
}
}
#[derive(Clone, Default)]
pub struct IntrospectionCollector {
details: HashMap<String, RouteDetail>,
}
impl IntrospectionCollector {
pub fn new() -> Self {
Self {
details: HashMap::new(),
}
}
pub fn register_pattern_detail(
&mut self,
full_path: String,
methods: Vec<Method>,
guards: Vec<String>,
is_resource: bool,
) {
self.details
.entry(full_path)
.and_modify(|d| {
update_unique(&mut d.methods, &methods);
update_unique(&mut d.guards, &guards);
if !d.is_resource && is_resource {
d.is_resource = true;
}
})
.or_insert(RouteDetail {
methods,
guards,
is_resource,
});
}
pub fn finalize(&mut self) -> IntrospectionTree {
let detail_registry = std::mem::take(&mut self.details);
let mut root = IntrospectionNode::new(ResourceType::App, "".into(), "".into());
for (full_path, _) in detail_registry.iter() {
let parts: Vec<&str> = full_path.split('/').collect();
let mut current_node = &mut root;
for (i, part) in parts.iter().enumerate() {
let existing_child_index = current_node
.children
.iter()
.position(|n| n.pattern == *part);
let child_index = if let Some(idx) = existing_child_index {
idx
} else {
let child_full_path = parts[..=i].join("/");
let kind = if detail_registry
.get(&child_full_path)
.is_some_and(|d| d.is_resource)
{
ResourceType::Resource
} else {
ResourceType::Scope
};
let new_node = IntrospectionNode::new(kind, part.to_string(), child_full_path);
current_node.children.push(new_node);
current_node.children.len() - 1
};
current_node = &mut current_node.children[child_index];
if let ResourceType::Resource = current_node.kind {
if let Some(detail) = detail_registry.get(&current_node.full_path) {
update_unique(&mut current_node.methods, &detail.methods);
update_unique(&mut current_node.guards, &detail.guards);
}
}
}
}
IntrospectionTree { root }
}
}
#[derive(Clone)]
pub struct IntrospectionTree {
pub root: IntrospectionNode,
}
impl IntrospectionTree {
pub fn report_as_text(&self) -> String {
let report_items: Vec<IntrospectionReportItem> = (&self.root).into();
let mut buf = String::new();
for item in report_items {
writeln!(
buf,
"{} => Methods: {:?} | Guards: {:?}",
item.full_path, item.methods, item.guards
)
.unwrap();
}
buf
}
pub fn report_as_json(&self) -> String {
let report_items: Vec<IntrospectionReportItem> = (&self.root).into();
serde_json::to_string_pretty(&report_items).unwrap()
}
}
fn update_unique<T: Clone + PartialEq>(existing: &mut Vec<T>, new_items: &[T]) {
for item in new_items {
if !existing.contains(item) {
existing.push(item.clone());
}
}
}

View File

@ -108,6 +108,9 @@ mod thin_data;
pub(crate) mod types;
pub mod web;
#[cfg(feature = "experimental-introspection")]
pub mod introspection;
#[doc(inline)]
pub use crate::error::Result;
pub use crate::{

View File

@ -417,6 +417,8 @@ where
B: MessageBody + 'static,
{
fn register(mut self, config: &mut AppService) {
let routes = std::mem::take(&mut self.routes);
let guards = if self.guards.is_empty() {
None
} else {
@ -428,13 +430,55 @@ where
} else {
ResourceDef::new(self.rdef.clone())
};
#[cfg(feature = "experimental-introspection")]
{
use crate::http::Method;
let guards_routes = routes.iter().map(|r| r.guards()).collect::<Vec<_>>();
let pat = rdef.pattern().unwrap_or("").to_string();
let full_path = if config.current_prefix.is_empty() {
pat.clone()
} else {
format!(
"{}/{}",
config.current_prefix.trim_end_matches('/'),
pat.trim_start_matches('/')
)
};
for route_guards in guards_routes {
// Log the guards and methods for introspection
let guard_names = route_guards.iter().map(|g| g.name()).collect::<Vec<_>>();
let methods = route_guards
.iter()
.flat_map(|g| g.details().unwrap_or_default())
.flat_map(|d| {
if let crate::guard::GuardDetail::HttpMethods(v) = d {
v.into_iter()
.filter_map(|s| s.parse::<Method>().ok())
.collect::<Vec<_>>()
} else {
Vec::new()
}
})
.collect::<Vec<_>>();
config.introspector.borrow_mut().register_pattern_detail(
full_path.clone(),
methods,
guard_names,
true,
);
}
}
if let Some(ref name) = self.name {
rdef.set_name(name);
}
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
routes: self.routes,
routes,
default: self.default,
});

View File

@ -159,6 +159,11 @@ impl Route {
self
}
#[cfg(feature = "experimental-introspection")]
pub(crate) fn guards(&self) -> &Vec<Box<dyn Guard>> {
&self.guards
}
/// Set handler function, use request extractors for parameters.
///
/// # Examples

View File

@ -384,6 +384,11 @@ where
// register nested services
let mut cfg = config.clone_config();
// Update the prefix for the nested scope
#[cfg(feature = "experimental-introspection")]
cfg.update_prefix(&self.rdef);
self.services
.into_iter()
.for_each(|mut srv| srv.register(&mut cfg));