mirror of https://github.com/fafhrd91/actix-web
Merge a3e428f7fb into fe890a73b2
This commit is contained in:
commit
890b36714c
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add `experimental-introspection` feature for retrieving configured route paths and HTTP methods.
|
||||||
|
|
||||||
## 4.12.0
|
## 4.12.0
|
||||||
|
|
||||||
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
|
- `actix_web::response::builder::HttpResponseBuilder::streaming()` now sets `Content-Type` to `application/octet-stream` if `Content-Type` does not exist.
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,9 @@ compat = ["compat-routing-macros-force-pub"]
|
||||||
# Opt-out forwards-compatibility for handler visibility inheritance fix.
|
# Opt-out forwards-compatibility for handler visibility inheritance fix.
|
||||||
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
|
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]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-macros = { version = "0.2.3", optional = true }
|
actix-macros = { version = "0.2.3", optional = true }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,8 @@ pub struct App<T> {
|
||||||
data_factories: Vec<FnDataFactory>,
|
data_factories: Vec<FnDataFactory>,
|
||||||
external: Vec<ResourceDef>,
|
external: Vec<ResourceDef>,
|
||||||
extensions: Extensions,
|
extensions: Extensions,
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App<AppEntry> {
|
impl App<AppEntry> {
|
||||||
|
|
@ -46,6 +48,10 @@ impl App<AppEntry> {
|
||||||
factory_ref,
|
factory_ref,
|
||||||
external: Vec::new(),
|
external: Vec::new(),
|
||||||
extensions: Extensions::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,
|
factory_ref: self.factory_ref,
|
||||||
external: self.external,
|
external: self.external,
|
||||||
extensions: self.extensions,
|
extensions: self.extensions,
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
introspector: self.introspector,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -429,6 +437,8 @@ where
|
||||||
factory_ref: self.factory_ref,
|
factory_ref: self.factory_ref,
|
||||||
external: self.external,
|
external: self.external,
|
||||||
extensions: self.extensions,
|
extensions: self.extensions,
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
introspector: self.introspector,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -453,6 +463,8 @@ where
|
||||||
default: self.default,
|
default: self.default,
|
||||||
factory_ref: self.factory_ref,
|
factory_ref: self.factory_ref,
|
||||||
extensions: RefCell::new(Some(self.extensions)),
|
extensions: RefCell::new(Some(self.extensions)),
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
introspector: Rc::clone(&self.introspector),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ where
|
||||||
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
|
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
|
||||||
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
|
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
|
||||||
pub(crate) external: RefCell<Vec<ResourceDef>>,
|
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>
|
impl<T, B> ServiceFactory<Request> for AppInit<T, B>
|
||||||
|
|
@ -72,6 +74,10 @@ where
|
||||||
|
|
||||||
// create App config to pass to child services
|
// create App config to pass to child services
|
||||||
let mut config = AppService::new(config, Rc::clone(&default));
|
let mut config = AppService::new(config, Rc::clone(&default));
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
config.introspector = Rc::clone(&self.introspector);
|
||||||
|
}
|
||||||
|
|
||||||
// register services
|
// register services
|
||||||
mem::take(&mut *self.services.borrow_mut())
|
mem::take(&mut *self.services.borrow_mut())
|
||||||
|
|
@ -80,6 +86,9 @@ where
|
||||||
|
|
||||||
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
|
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();
|
let (config, services) = config.into_services();
|
||||||
|
|
||||||
// complete pipeline creation.
|
// complete pipeline creation.
|
||||||
|
|
@ -110,6 +119,8 @@ where
|
||||||
|
|
||||||
// construct app service and middleware service factory future.
|
// construct app service and middleware service factory future.
|
||||||
let endpoint_fut = self.endpoint.new_service(());
|
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.
|
// take extensions or create new one as app data container.
|
||||||
let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default();
|
let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default();
|
||||||
|
|
@ -130,6 +141,12 @@ where
|
||||||
factory.create(&mut app_data);
|
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 {
|
Ok(AppInitService {
|
||||||
service,
|
service,
|
||||||
app_data: Rc::new(app_data),
|
app_data: Rc::new(app_data),
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,11 @@ pub struct AppService {
|
||||||
Option<Guards>,
|
Option<Guards>,
|
||||||
Option<Rc<ResourceMap>>,
|
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 {
|
impl AppService {
|
||||||
|
|
@ -40,6 +45,12 @@ impl AppService {
|
||||||
default,
|
default,
|
||||||
root: true,
|
root: true,
|
||||||
services: Vec::new(),
|
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)]
|
#[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(
|
pub(crate) fn into_services(
|
||||||
self,
|
self,
|
||||||
) -> (
|
) -> (
|
||||||
|
|
@ -71,6 +100,10 @@ impl AppService {
|
||||||
default: Rc::clone(&self.default),
|
default: Rc::clone(&self.default),
|
||||||
services: Vec::new(),
|
services: Vec::new(),
|
||||||
root: false,
|
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 = (),
|
InitError = (),
|
||||||
> + 'static,
|
> + '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
|
self.services
|
||||||
.push((rdef, boxed::factory(factory.into_factory()), guards, nested));
|
.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.
|
/// Application connection config.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
//! or handler. This interface is defined by the [`Guard`] trait.
|
//! 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
|
//! 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
|
//! 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).
|
//! services multiple times (which might have different combining behavior than you want).
|
||||||
//!
|
//!
|
||||||
|
|
@ -66,6 +66,17 @@ pub use self::{
|
||||||
host::{Host, HostGuard},
|
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.
|
/// Provides access to request parts that are useful during routing.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct GuardContext<'a> {
|
pub struct GuardContext<'a> {
|
||||||
|
|
@ -124,12 +135,28 @@ impl<'a> GuardContext<'a> {
|
||||||
pub trait Guard {
|
pub trait Guard {
|
||||||
/// Returns true if predicate condition is met for a given request.
|
/// Returns true if predicate condition is met for a given request.
|
||||||
fn check(&self, ctx: &GuardContext<'_>) -> bool;
|
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> {
|
impl Guard for Rc<dyn Guard> {
|
||||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
(**self).check(ctx)
|
(**self).check(ctx)
|
||||||
}
|
}
|
||||||
|
fn name(&self) -> String {
|
||||||
|
(**self).name()
|
||||||
|
}
|
||||||
|
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||||
|
(**self).details()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a guard using the given function.
|
/// 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.
|
/// 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 {
|
pub struct AnyGuard {
|
||||||
guards: Vec<Box<dyn Guard>>,
|
guards: Vec<Box<dyn Guard>>,
|
||||||
}
|
}
|
||||||
|
|
@ -219,6 +246,24 @@ impl Guard for AnyGuard {
|
||||||
|
|
||||||
false
|
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.
|
/// 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.
|
/// 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 {
|
pub struct AllGuard {
|
||||||
guards: Vec<Box<dyn Guard>>,
|
guards: Vec<Box<dyn Guard>>,
|
||||||
}
|
}
|
||||||
|
|
@ -271,6 +316,24 @@ impl Guard for AllGuard {
|
||||||
|
|
||||||
true
|
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.
|
/// 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 {
|
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||||
!self.0.check(ctx)
|
!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.
|
/// Creates a guard that matches a specified HTTP method.
|
||||||
|
|
@ -320,6 +389,12 @@ impl Guard for MethodGuard {
|
||||||
|
|
||||||
ctx.head().method == self.0
|
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 {
|
macro_rules! method_guard {
|
||||||
|
|
@ -382,6 +457,15 @@ impl Guard for HeaderGuard {
|
||||||
|
|
||||||
false
|
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)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -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(¤t_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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -108,6 +108,9 @@ mod thin_data;
|
||||||
pub(crate) mod types;
|
pub(crate) mod types;
|
||||||
pub mod web;
|
pub mod web;
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pub mod introspection;
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use crate::error::Result;
|
pub use crate::error::Result;
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
|
|
|
||||||
|
|
@ -417,6 +417,8 @@ where
|
||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
fn register(mut self, config: &mut AppService) {
|
fn register(mut self, config: &mut AppService) {
|
||||||
|
let routes = std::mem::take(&mut self.routes);
|
||||||
|
|
||||||
let guards = if self.guards.is_empty() {
|
let guards = if self.guards.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -428,13 +430,55 @@ where
|
||||||
} else {
|
} else {
|
||||||
ResourceDef::new(self.rdef.clone())
|
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 {
|
if let Some(ref name) = self.name {
|
||||||
rdef.set_name(name);
|
rdef.set_name(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
||||||
routes: self.routes,
|
routes,
|
||||||
default: self.default,
|
default: self.default,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -159,6 +159,11 @@ impl Route {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pub(crate) fn guards(&self) -> &Vec<Box<dyn Guard>> {
|
||||||
|
&self.guards
|
||||||
|
}
|
||||||
|
|
||||||
/// Set handler function, use request extractors for parameters.
|
/// Set handler function, use request extractors for parameters.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
|
|
||||||
|
|
@ -384,6 +384,11 @@ where
|
||||||
|
|
||||||
// register nested services
|
// register nested services
|
||||||
let mut cfg = config.clone_config();
|
let mut cfg = config.clone_config();
|
||||||
|
|
||||||
|
// Update the prefix for the nested scope
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
cfg.update_prefix(&self.rdef);
|
||||||
|
|
||||||
self.services
|
self.services
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.for_each(|mut srv| srv.register(&mut cfg));
|
.for_each(|mut srv| srv.register(&mut cfg));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue