// 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, ) -> 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, ) -> 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, ) -> 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) -> 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) -> 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); } }