mirror of https://github.com/fafhrd91/actix-web
Merge b75fcd8fac into afd53045d4
This commit is contained in:
commit
9ac2309248
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
## 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.
|
||||
|
||||
## 4.12.1
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ compat = ["compat-routing-macros-force-pub"]
|
|||
# Opt-out forwards-compatibility for handler visibility inheritance fix.
|
||||
compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"]
|
||||
|
||||
# Enabling the retrieval of metadata for initialized resources, including path and HTTP method.
|
||||
experimental-introspection = []
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.5"
|
||||
actix-macros = { version = "0.2.3", optional = true }
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
external: Vec<ResourceDef>,
|
||||
extensions: Extensions,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
}
|
||||
|
||||
impl App<AppEntry> {
|
||||
|
|
@ -46,6 +48,10 @@ impl App<AppEntry> {
|
|||
factory_ref,
|
||||
external: Vec::new(),
|
||||
extensions: Extensions::new(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: Rc::new(RefCell::new(
|
||||
crate::introspection::IntrospectionCollector::new(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -366,6 +372,8 @@ where
|
|||
factory_ref: self.factory_ref,
|
||||
external: self.external,
|
||||
extensions: self.extensions,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: self.introspector,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -429,6 +437,8 @@ where
|
|||
factory_ref: self.factory_ref,
|
||||
external: self.external,
|
||||
extensions: self.extensions,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: self.introspector,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -453,6 +463,8 @@ where
|
|||
default: self.default,
|
||||
factory_ref: self.factory_ref,
|
||||
extensions: RefCell::new(Some(self.extensions)),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: Rc::clone(&self.introspector),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ where
|
|||
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
|
||||
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
|
||||
pub(crate) external: RefCell<Vec<ResourceDef>>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) introspector: Rc<RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
}
|
||||
|
||||
impl<T, B> ServiceFactory<Request> for AppInit<T, B>
|
||||
|
|
@ -72,6 +74,10 @@ where
|
|||
|
||||
// create App config to pass to child services
|
||||
let mut config = AppService::new(config, Rc::clone(&default));
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
config.introspector = Rc::clone(&self.introspector);
|
||||
}
|
||||
|
||||
// register services
|
||||
mem::take(&mut *self.services.borrow_mut())
|
||||
|
|
@ -80,6 +86,9 @@ where
|
|||
|
||||
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
let (config, services, _) = config.into_services();
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
let (config, services) = config.into_services();
|
||||
|
||||
// complete pipeline creation.
|
||||
|
|
@ -98,6 +107,10 @@ where
|
|||
|
||||
// external resources
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +123,8 @@ where
|
|||
|
||||
// construct app service and middleware service factory future.
|
||||
let endpoint_fut = self.endpoint.new_service(());
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
let introspector = Rc::clone(&self.introspector);
|
||||
|
||||
// take extensions or create new one as app data container.
|
||||
let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default();
|
||||
|
|
@ -130,6 +145,12 @@ where
|
|||
factory.create(&mut app_data);
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
{
|
||||
let tree = introspector.borrow_mut().finalize();
|
||||
app_data.insert(crate::web::Data::new(tree));
|
||||
}
|
||||
|
||||
Ok(AppInitService {
|
||||
service,
|
||||
app_data: Rc::new(app_data),
|
||||
|
|
|
|||
|
|
@ -30,6 +30,15 @@ pub struct AppService {
|
|||
Option<Guards>,
|
||||
Option<Rc<ResourceMap>>,
|
||||
)>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub current_prefix: String,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) introspector:
|
||||
std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) scope_id_stack: Vec<usize>,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pending_scope_id: Option<usize>,
|
||||
}
|
||||
|
||||
impl AppService {
|
||||
|
|
@ -40,6 +49,16 @@ impl AppService {
|
|||
default,
|
||||
root: true,
|
||||
services: Vec::new(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
current_prefix: "".to_string(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: std::rc::Rc::new(std::cell::RefCell::new(
|
||||
crate::introspection::IntrospectionCollector::new(),
|
||||
)),
|
||||
#[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)]
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) fn into_services(
|
||||
self,
|
||||
) -> (
|
||||
AppConfig,
|
||||
Vec<(
|
||||
ResourceDef,
|
||||
BoxedHttpServiceFactory,
|
||||
Option<Guards>,
|
||||
Option<Rc<ResourceMap>>,
|
||||
)>,
|
||||
std::rc::Rc<std::cell::RefCell<crate::introspection::IntrospectionCollector>>,
|
||||
) {
|
||||
(self.config, self.services, self.introspector)
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[cfg(not(feature = "experimental-introspection"))]
|
||||
pub(crate) fn into_services(
|
||||
self,
|
||||
) -> (
|
||||
|
|
@ -71,6 +108,14 @@ impl AppService {
|
|||
default: Rc::clone(&self.default),
|
||||
services: Vec::new(),
|
||||
root: false,
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
current_prefix: self.current_prefix.clone(),
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
introspector: std::rc::Rc::clone(&self.introspector),
|
||||
#[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 = (),
|
||||
> + '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
|
||||
.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.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use super::{Guard, GuardContext};
|
||||
use super::{Guard, GuardContext, GuardDetail};
|
||||
use crate::http::header::Accept;
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
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)]
|
||||
|
|
@ -96,4 +127,28 @@ mod tests {
|
|||
.match_star_star()
|
||||
.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 super::{Guard, GuardContext};
|
||||
use super::{Guard, GuardContext, GuardDetail};
|
||||
|
||||
/// Creates a guard that matches requests targeting a specific host.
|
||||
///
|
||||
|
|
@ -117,6 +117,41 @@ impl Guard for HostGuard {
|
|||
// all conditions passed
|
||||
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)]
|
||||
|
|
@ -239,4 +274,23 @@ mod tests {
|
|||
let host = Host("localhost");
|
||||
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.
|
||||
//!
|
||||
//! Commonly-used guards are provided in this module as well as a way of creating a guard from a
|
||||
//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be
|
||||
//! closure ([`fn_guard`]). The [`Not`], [`Any()`], and [`All()`] guards are noteworthy, as they can be
|
||||
//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on
|
||||
//! services multiple times (which might have different combining behavior than you want).
|
||||
//!
|
||||
|
|
@ -66,6 +66,17 @@ pub use self::{
|
|||
host::{Host, HostGuard},
|
||||
};
|
||||
|
||||
/// Enum to encapsulate various introspection details of a guard.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GuardDetail {
|
||||
/// Detail associated with 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.
|
||||
#[derive(Debug)]
|
||||
pub struct GuardContext<'a> {
|
||||
|
|
@ -124,12 +135,30 @@ impl<'a> GuardContext<'a> {
|
|||
pub trait Guard {
|
||||
/// Returns true if predicate condition is met for a given request.
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool;
|
||||
|
||||
/// Returns a nominal representation of the guard.
|
||||
fn name(&self) -> String {
|
||||
std::any::type_name::<Self>().to_string()
|
||||
}
|
||||
|
||||
/// Returns detailed introspection information, 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> {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
(**self).check(ctx)
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
(**self).name()
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
(**self).details()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a guard using the given function.
|
||||
|
|
@ -195,7 +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.
|
||||
///
|
||||
/// Construct an `AnyGuard` using [`Any`].
|
||||
/// Construct an `AnyGuard` using [`Any()`].
|
||||
pub struct AnyGuard {
|
||||
guards: Vec<Box<dyn Guard>>,
|
||||
}
|
||||
|
|
@ -219,6 +248,24 @@ impl Guard for AnyGuard {
|
|||
|
||||
false
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
format!(
|
||||
"AnyGuard({})",
|
||||
self.guards
|
||||
.iter()
|
||||
.map(|g| g.name())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
Some(
|
||||
self.guards
|
||||
.iter()
|
||||
.flat_map(|g| g.details().unwrap_or_default())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a guard that matches if all added guards match.
|
||||
|
|
@ -247,7 +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.
|
||||
///
|
||||
/// Construct an `AllGuard` using [`All`].
|
||||
/// Construct an `AllGuard` using [`All()`].
|
||||
pub struct AllGuard {
|
||||
guards: Vec<Box<dyn Guard>>,
|
||||
}
|
||||
|
|
@ -271,6 +318,24 @@ impl Guard for AllGuard {
|
|||
|
||||
true
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
format!(
|
||||
"AllGuard({})",
|
||||
self.guards
|
||||
.iter()
|
||||
.map(|g| g.name())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
Some(
|
||||
self.guards
|
||||
.iter()
|
||||
.flat_map(|g| g.details().unwrap_or_default())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps a guard and inverts the outcome of its `Guard` implementation.
|
||||
|
|
@ -291,6 +356,19 @@ impl<G: Guard> Guard for Not<G> {
|
|||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
!self.0.check(ctx)
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
format!("Not({})", self.0.name())
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
#[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.
|
||||
|
|
@ -320,6 +398,12 @@ impl Guard for MethodGuard {
|
|||
|
||||
ctx.head().method == self.0
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
Some(vec![GuardDetail::HttpMethods(vec![self.0.to_string()])])
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! method_guard {
|
||||
|
|
@ -382,6 +466,15 @@ impl Guard for HeaderGuard {
|
|||
|
||||
false
|
||||
}
|
||||
fn name(&self) -> String {
|
||||
format!("Header({}, {})", self.0, self.1.to_str().unwrap_or(""))
|
||||
}
|
||||
fn details(&self) -> Option<Vec<GuardDetail>> {
|
||||
Some(vec![GuardDetail::Headers(vec![(
|
||||
self.0.to_string(),
|
||||
self.1.to_str().unwrap_or("").to_string(),
|
||||
)])])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -108,6 +108,9 @@ mod thin_data;
|
|||
pub(crate) mod types;
|
||||
pub mod web;
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub mod introspection;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use crate::error::Result;
|
||||
pub use crate::{
|
||||
|
|
|
|||
|
|
@ -417,6 +417,8 @@ where
|
|||
B: MessageBody + 'static,
|
||||
{
|
||||
fn register(mut self, config: &mut AppService) {
|
||||
let routes = std::mem::take(&mut self.routes);
|
||||
|
||||
let guards = if self.guards.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -428,13 +430,71 @@ where
|
|||
} else {
|
||||
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 {
|
||||
rdef.set_name(name);
|
||||
}
|
||||
|
||||
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
|
||||
routes: self.routes,
|
||||
routes,
|
||||
default: self.default,
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -159,6 +159,11 @@ impl Route {
|
|||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
pub(crate) fn guards(&self) -> &Vec<Box<dyn Guard>> {
|
||||
&self.guards
|
||||
}
|
||||
|
||||
/// Set handler function, use request extractors for parameters.
|
||||
///
|
||||
/// # Examples
|
||||
|
|
|
|||
|
|
@ -384,14 +384,32 @@ where
|
|||
|
||||
// register nested services
|
||||
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
|
||||
.into_iter()
|
||||
.for_each(|mut srv| srv.register(&mut cfg));
|
||||
|
||||
let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef));
|
||||
|
||||
#[cfg(feature = "experimental-introspection")]
|
||||
let origin_scope = cfg.current_prefix.clone();
|
||||
|
||||
// external resources
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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