mirror of https://github.com/fafhrd91/actix-web
Merge b75fcd8fac into afd53045d4
This commit is contained in:
commit
9ac2309248
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Add `experimental-introspection` feature to report configured routes (paths, methods, guards, resource metadata), include reachability hints for shadowed/conflicting routes, and provide a separate report for external resources.
|
||||||
- Minimum supported Rust version (MSRV) is now 1.82.
|
- Minimum supported Rust version (MSRV) is now 1.82.
|
||||||
|
|
||||||
## 4.12.1
|
## 4.12.1
|
||||||
|
|
|
||||||
|
|
@ -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,304 @@
|
||||||
|
// 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/externals for external resources report
|
||||||
|
async fn introspection_handler_externals(
|
||||||
|
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||||
|
) -> impl Responder {
|
||||||
|
let report = tree.report_externals_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!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /alpha and /beta (named multi-pattern resource)
|
||||||
|
async fn multi_pattern() -> impl Responder {
|
||||||
|
HttpResponse::Ok().body("Hello from multi-pattern resource!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /acceptable (Acceptable guard)
|
||||||
|
async fn acceptable_guarded() -> impl Responder {
|
||||||
|
HttpResponse::Ok().body("Acceptable guard matched!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /hosted (Host guard)
|
||||||
|
async fn host_guarded() -> impl Responder {
|
||||||
|
HttpResponse::Ok().body("Host guard matched!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
// curl --location '127.0.0.1:8080/introspection/externals'
|
||||||
|
.external_resource("app-external", "https://example.com/{id}")
|
||||||
|
.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),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/introspection/externals")
|
||||||
|
.route(web::get().to(introspection_handler_externals)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource(["/alpha", "/beta"])
|
||||||
|
.name("multi")
|
||||||
|
.route(web::get().to(multi_pattern)),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/acceptable",
|
||||||
|
web::get()
|
||||||
|
.guard(guard::Acceptable::new(mime::APPLICATION_JSON).match_star_star())
|
||||||
|
.to(acceptable_guarded),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/hosted",
|
||||||
|
web::get().guard(guard::Host("127.0.0.1")).to(host_guarded),
|
||||||
|
)
|
||||||
|
// API endpoints under /api
|
||||||
|
.service(
|
||||||
|
web::scope("/api")
|
||||||
|
.configure(|cfg| {
|
||||||
|
cfg.external_resource("api-external", "https://api.example/{id}");
|
||||||
|
})
|
||||||
|
// 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.
|
||||||
|
|
@ -98,6 +107,10 @@ where
|
||||||
|
|
||||||
// external resources
|
// external resources
|
||||||
for mut rdef in mem::take(&mut *self.external.borrow_mut()) {
|
for mut rdef in mem::take(&mut *self.external.borrow_mut()) {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
self.introspector.borrow_mut().register_external(&rdef, "/");
|
||||||
|
}
|
||||||
rmap.add(&mut rdef, None);
|
rmap.add(&mut rdef, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,6 +123,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 +145,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,15 @@ 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>>,
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pub(crate) scope_id_stack: Vec<usize>,
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pending_scope_id: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
|
|
@ -40,6 +49,16 @@ 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(),
|
||||||
|
)),
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
scope_id_stack: Vec::new(),
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pending_scope_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +68,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 +108,14 @@ 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),
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
scope_id_stack: self.scope_id_stack.clone(),
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pending_scope_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,9 +146,90 @@ impl AppService {
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> + 'static,
|
> + 'static,
|
||||||
{
|
{
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
use std::borrow::Borrow;
|
||||||
|
|
||||||
|
// 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<_>>();
|
||||||
|
let guard_details = crate::introspection::guard_reports_from_iter(guard_list.iter());
|
||||||
|
|
||||||
|
let is_resource = nested.is_none();
|
||||||
|
let full_paths = crate::introspection::expand_patterns(&self.current_prefix, &rdef);
|
||||||
|
let patterns = rdef
|
||||||
|
.pattern_iter()
|
||||||
|
.map(|pattern| pattern.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let resource_name = rdef.name().map(|name| name.to_string());
|
||||||
|
let is_prefix = rdef.is_prefix();
|
||||||
|
let scope_id = if nested.is_some() {
|
||||||
|
self.pending_scope_id.take()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let parent_scope_id = self.scope_id_stack.last().copied();
|
||||||
|
|
||||||
|
for full_path in full_paths {
|
||||||
|
let info = crate::introspection::RouteInfo::new(
|
||||||
|
full_path,
|
||||||
|
methods.clone(),
|
||||||
|
guard_names.clone(),
|
||||||
|
guard_details.clone(),
|
||||||
|
patterns.clone(),
|
||||||
|
resource_name.clone(),
|
||||||
|
);
|
||||||
|
self.introspector.borrow_mut().register_service(
|
||||||
|
info,
|
||||||
|
is_resource,
|
||||||
|
is_prefix,
|
||||||
|
scope_id,
|
||||||
|
parent_scope_id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
let next = ResourceDef::root_prefix(prefix);
|
||||||
|
|
||||||
|
if self.current_prefix.is_empty() {
|
||||||
|
self.current_prefix = next.pattern().unwrap_or("").to_string();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current = ResourceDef::root_prefix(&self.current_prefix);
|
||||||
|
let joined = current.join(&next);
|
||||||
|
self.current_prefix = joined.pattern().unwrap_or("").to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
pub(crate) fn prepare_scope_id(&mut self) -> usize {
|
||||||
|
let scope_id = self.introspector.borrow_mut().next_scope_id();
|
||||||
|
self.pending_scope_id = Some(scope_id);
|
||||||
|
scope_id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Application connection config.
|
/// Application connection config.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{Guard, GuardContext};
|
use super::{Guard, GuardContext, GuardDetail};
|
||||||
use crate::http::header::Accept;
|
use crate::http::header::Accept;
|
||||||
|
|
||||||
/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
|
/// A guard that verifies that an `Accept` header is present and it contains a compatible MIME type.
|
||||||
|
|
@ -63,6 +63,37 @@ impl Guard for Acceptable {
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
if self.match_star_star {
|
||||||
|
format!("Acceptable({}, match_star_star=true)", self.mime)
|
||||||
|
} else {
|
||||||
|
format!("Acceptable({})", self.mime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "experimental-introspection"))]
|
||||||
|
{
|
||||||
|
std::any::type_name::<Self>().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
let mut details = Vec::new();
|
||||||
|
details.push(GuardDetail::Generic(format!("mime={}", self.mime)));
|
||||||
|
if self.match_star_star {
|
||||||
|
details.push(GuardDetail::Generic("match_star_star=true".to_string()));
|
||||||
|
}
|
||||||
|
Some(details)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "experimental-introspection"))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -96,4 +127,28 @@ mod tests {
|
||||||
.match_star_star()
|
.match_star_star()
|
||||||
.check(&req.guard_ctx()));
|
.check(&req.guard_ctx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
#[test]
|
||||||
|
fn acceptable_guard_details_include_mime() {
|
||||||
|
let guard = Acceptable::new(mime::APPLICATION_JSON).match_star_star();
|
||||||
|
let details = guard.details().expect("missing guard details");
|
||||||
|
|
||||||
|
assert!(details.iter().any(|detail| match detail {
|
||||||
|
GuardDetail::Generic(value) => value == "match_star_star=true",
|
||||||
|
_ => false,
|
||||||
|
}));
|
||||||
|
let expected = format!("mime={}", mime::APPLICATION_JSON);
|
||||||
|
assert!(details.iter().any(|detail| match detail {
|
||||||
|
GuardDetail::Generic(value) => value == &expected,
|
||||||
|
_ => false,
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
guard.name(),
|
||||||
|
format!(
|
||||||
|
"Acceptable({}, match_star_star=true)",
|
||||||
|
mime::APPLICATION_JSON
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use actix_http::{header, uri::Uri, RequestHead, Version};
|
use actix_http::{header, uri::Uri, RequestHead, Version};
|
||||||
|
|
||||||
use super::{Guard, GuardContext};
|
use super::{Guard, GuardContext, GuardDetail};
|
||||||
|
|
||||||
/// Creates a guard that matches requests targeting a specific host.
|
/// Creates a guard that matches requests targeting a specific host.
|
||||||
///
|
///
|
||||||
|
|
@ -117,6 +117,41 @@ impl Guard for HostGuard {
|
||||||
// all conditions passed
|
// all conditions passed
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
if let Some(ref scheme) = self.scheme {
|
||||||
|
format!("Host({}, scheme={})", self.host, scheme)
|
||||||
|
} else {
|
||||||
|
format!("Host({})", self.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "experimental-introspection"))]
|
||||||
|
{
|
||||||
|
std::any::type_name::<Self>().to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
let mut details = vec![GuardDetail::Headers(vec![(
|
||||||
|
"host".to_string(),
|
||||||
|
self.host.clone(),
|
||||||
|
)])];
|
||||||
|
|
||||||
|
if let Some(ref scheme) = self.scheme {
|
||||||
|
details.push(GuardDetail::Generic(format!("scheme={scheme}")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(details)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "experimental-introspection"))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -239,4 +274,23 @@ mod tests {
|
||||||
let host = Host("localhost");
|
let host = Host("localhost");
|
||||||
assert!(!host.check(&req.guard_ctx()));
|
assert!(!host.check(&req.guard_ctx()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
#[test]
|
||||||
|
fn host_guard_details_include_host_and_scheme() {
|
||||||
|
let host = Host("example.com").scheme("https");
|
||||||
|
let details = host.details().expect("missing guard details");
|
||||||
|
|
||||||
|
assert!(details.iter().any(|detail| match detail {
|
||||||
|
GuardDetail::Headers(headers) => headers
|
||||||
|
.iter()
|
||||||
|
.any(|(name, value)| name == "host" && value == "example.com"),
|
||||||
|
_ => false,
|
||||||
|
}));
|
||||||
|
assert!(details.iter().any(|detail| match detail {
|
||||||
|
GuardDetail::Generic(value) => value == "scheme=https",
|
||||||
|
_ => false,
|
||||||
|
}));
|
||||||
|
assert_eq!(host.name(), "Host(example.com, scheme=https)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 explicit HTTP method guards.
|
||||||
|
HttpMethods(Vec<String>),
|
||||||
|
/// Detail associated with headers (header, value).
|
||||||
|
Headers(Vec<(String, String)>),
|
||||||
|
/// Generic detail, typically used for compound guard representations.
|
||||||
|
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,30 @@ 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, when available.
|
||||||
|
///
|
||||||
|
/// This is best-effort and may omit complex guard logic.
|
||||||
|
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 +224,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 +248,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 +294,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 +318,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 +356,19 @@ 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>> {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
Some(vec![GuardDetail::Generic(self.name())])
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "experimental-introspection"))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a guard that matches a specified HTTP method.
|
/// Creates a guard that matches a specified HTTP method.
|
||||||
|
|
@ -320,6 +398,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 +466,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)]
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,71 @@ where
|
||||||
} else {
|
} else {
|
||||||
ResourceDef::new(self.rdef.clone())
|
ResourceDef::new(self.rdef.clone())
|
||||||
};
|
};
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
use crate::http::Method;
|
||||||
|
|
||||||
|
let full_paths = crate::introspection::expand_patterns(&config.current_prefix, &rdef);
|
||||||
|
let patterns = rdef
|
||||||
|
.pattern_iter()
|
||||||
|
.map(|pattern| pattern.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let guards_routes = routes.iter().map(|r| r.guards()).collect::<Vec<_>>();
|
||||||
|
let scope_id = config.scope_id_stack.last().copied();
|
||||||
|
let resource_guards: &[Box<dyn Guard>] = guards.as_deref().unwrap_or(&[]);
|
||||||
|
let resource_name = self.name.clone();
|
||||||
|
|
||||||
|
for route_guards in guards_routes {
|
||||||
|
// Log the guards and methods for introspection
|
||||||
|
let mut guard_names = Vec::new();
|
||||||
|
let mut methods = Vec::new();
|
||||||
|
|
||||||
|
for guard in resource_guards.iter().chain(route_guards.iter()) {
|
||||||
|
guard_names.push(guard.name());
|
||||||
|
methods.extend(
|
||||||
|
guard
|
||||||
|
.details()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let guard_details = crate::introspection::guard_reports_from_iter(
|
||||||
|
resource_guards.iter().chain(route_guards.iter()),
|
||||||
|
);
|
||||||
|
|
||||||
|
for full_path in &full_paths {
|
||||||
|
let info = crate::introspection::RouteInfo::new(
|
||||||
|
full_path.clone(),
|
||||||
|
methods.clone(),
|
||||||
|
guard_names.clone(),
|
||||||
|
guard_details.clone(),
|
||||||
|
patterns.clone(),
|
||||||
|
resource_name.clone(),
|
||||||
|
);
|
||||||
|
config
|
||||||
|
.introspector
|
||||||
|
.borrow_mut()
|
||||||
|
.register_route(info, scope_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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,14 +384,32 @@ 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")]
|
||||||
|
{
|
||||||
|
let scope_id = config.prepare_scope_id();
|
||||||
|
cfg.scope_id_stack.push(scope_id);
|
||||||
|
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));
|
||||||
|
|
||||||
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
|
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
let origin_scope = cfg.current_prefix.clone();
|
||||||
|
|
||||||
// external resources
|
// external resources
|
||||||
for mut rdef in mem::take(&mut self.external) {
|
for mut rdef in mem::take(&mut self.external) {
|
||||||
|
#[cfg(feature = "experimental-introspection")]
|
||||||
|
{
|
||||||
|
cfg.introspector
|
||||||
|
.borrow_mut()
|
||||||
|
.register_external(&rdef, &origin_scope);
|
||||||
|
}
|
||||||
rmap.add(&mut rdef, None);
|
rmap.add(&mut rdef, None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
#![cfg(feature = "experimental-introspection")]
|
||||||
|
|
||||||
|
use actix_web::{guard, test, web, App, HttpResponse};
|
||||||
|
|
||||||
|
async fn introspection_handler(
|
||||||
|
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(tree.report_as_json())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn externals_handler(
|
||||||
|
tree: web::Data<actix_web::introspection::IntrospectionTree>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.content_type("application/json")
|
||||||
|
.body(tree.report_externals_as_json())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_item<'a>(items: &'a [serde_json::Value], path: &str) -> &'a serde_json::Value {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.get("full_path").and_then(|v| v.as_str()) == Some(path))
|
||||||
|
.unwrap_or_else(|| panic!("missing route for {path}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_external<'a>(items: &'a [serde_json::Value], name: &str) -> &'a serde_json::Value {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.get("name").and_then(|v| v.as_str()) == Some(name))
|
||||||
|
.unwrap_or_else(|| panic!("missing external resource for {name}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn introspection_report_includes_details_and_metadata() {
|
||||||
|
let app = test::init_service(
|
||||||
|
App::new()
|
||||||
|
.external_resource("app-external", "https://example.com/{id}")
|
||||||
|
.service(
|
||||||
|
web::resource(["/alpha", "/beta"])
|
||||||
|
.name("multi")
|
||||||
|
.route(web::get().to(HttpResponse::Ok)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::resource("/guarded")
|
||||||
|
.guard(guard::Header("accept", "text/plain"))
|
||||||
|
.route(web::get().to(HttpResponse::Ok)),
|
||||||
|
)
|
||||||
|
.service(
|
||||||
|
web::scope("/scoped")
|
||||||
|
.guard(guard::Header("x-scope", "1"))
|
||||||
|
.configure(|cfg| {
|
||||||
|
cfg.external_resource("scope-external", "https://scope.example/{id}");
|
||||||
|
})
|
||||||
|
.service(web::resource("/item").route(web::get().to(HttpResponse::Ok))),
|
||||||
|
)
|
||||||
|
.service(web::resource("/introspection").route(web::get().to(introspection_handler)))
|
||||||
|
.service(
|
||||||
|
web::resource("/introspection/externals").route(web::get().to(externals_handler)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let req = test::TestRequest::get().uri("/introspection").to_request();
|
||||||
|
let resp = test::call_service(&app, req).await;
|
||||||
|
assert!(resp.status().is_success());
|
||||||
|
|
||||||
|
let body = test::read_body(resp).await;
|
||||||
|
let items: Vec<serde_json::Value> =
|
||||||
|
serde_json::from_slice(&body).expect("invalid introspection json");
|
||||||
|
|
||||||
|
let alpha = find_item(&items, "/alpha");
|
||||||
|
let patterns = alpha
|
||||||
|
.get("patterns")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("patterns missing");
|
||||||
|
let patterns = patterns
|
||||||
|
.iter()
|
||||||
|
.filter_map(|v| v.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert!(patterns.contains(&"/alpha"));
|
||||||
|
assert!(patterns.contains(&"/beta"));
|
||||||
|
assert_eq!(
|
||||||
|
alpha.get("resource_name").and_then(|v| v.as_str()),
|
||||||
|
Some("multi")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
alpha.get("resource_type").and_then(|v| v.as_str()),
|
||||||
|
Some("resource")
|
||||||
|
);
|
||||||
|
|
||||||
|
let guarded = find_item(&items, "/guarded");
|
||||||
|
let guards = guarded
|
||||||
|
.get("guards")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("guards missing");
|
||||||
|
assert!(guards
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.as_str() == Some("Header(accept, text/plain)")));
|
||||||
|
|
||||||
|
let guard_details = guarded
|
||||||
|
.get("guards_detail")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("guards_detail missing");
|
||||||
|
assert!(!guard_details.is_empty());
|
||||||
|
|
||||||
|
let alpha_guards = alpha
|
||||||
|
.get("guards")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("alpha guards missing");
|
||||||
|
let alpha_guard_details = alpha
|
||||||
|
.get("guards_detail")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("alpha guards_detail missing");
|
||||||
|
assert!(alpha_guards.is_empty());
|
||||||
|
assert!(!alpha_guard_details.is_empty());
|
||||||
|
|
||||||
|
let scoped = find_item(&items, "/scoped");
|
||||||
|
assert_eq!(
|
||||||
|
scoped.get("resource_type").and_then(|v| v.as_str()),
|
||||||
|
Some("scope")
|
||||||
|
);
|
||||||
|
let scoped_guards = scoped
|
||||||
|
.get("guards")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("scoped guards missing");
|
||||||
|
assert!(scoped_guards
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.as_str() == Some("Header(x-scope, 1)")));
|
||||||
|
|
||||||
|
let req = test::TestRequest::get()
|
||||||
|
.uri("/introspection/externals")
|
||||||
|
.to_request();
|
||||||
|
let resp = test::call_service(&app, req).await;
|
||||||
|
assert!(resp.status().is_success());
|
||||||
|
|
||||||
|
let body = test::read_body(resp).await;
|
||||||
|
let externals: Vec<serde_json::Value> =
|
||||||
|
serde_json::from_slice(&body).expect("invalid externals json");
|
||||||
|
|
||||||
|
let app_external = find_external(&externals, "app-external");
|
||||||
|
let app_patterns = app_external
|
||||||
|
.get("patterns")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("app external patterns missing");
|
||||||
|
assert!(app_patterns
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.as_str() == Some("https://example.com/{id}")));
|
||||||
|
assert_eq!(
|
||||||
|
app_external.get("origin_scope").and_then(|v| v.as_str()),
|
||||||
|
Some("/")
|
||||||
|
);
|
||||||
|
|
||||||
|
let scope_external = find_external(&externals, "scope-external");
|
||||||
|
let scope_patterns = scope_external
|
||||||
|
.get("patterns")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.expect("scope external patterns missing");
|
||||||
|
assert!(scope_patterns
|
||||||
|
.iter()
|
||||||
|
.any(|v| v.as_str() == Some("https://scope.example/{id}")));
|
||||||
|
assert_eq!(
|
||||||
|
scope_external.get("origin_scope").and_then(|v| v.as_str()),
|
||||||
|
Some("/scoped")
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue