|
| 1 | +// Example showcasing the experimental introspection feature. |
| 2 | +// Run with: `cargo run --features experimental-introspection --example introspection` |
| 3 | + |
| 4 | +#[actix_web::main] |
| 5 | +async fn main() -> std::io::Result<()> { |
| 6 | + #[cfg(feature = "experimental-introspection")] |
| 7 | + { |
| 8 | + use actix_web::{dev::Service, guard, web, App, HttpResponse, HttpServer, Responder}; |
| 9 | + use serde::Deserialize; |
| 10 | + |
| 11 | + // Initialize logging |
| 12 | + env_logger::Builder::new() |
| 13 | + .filter_level(log::LevelFilter::Debug) |
| 14 | + .init(); |
| 15 | + |
| 16 | + // Custom guard to check if the Content-Type header is present. |
| 17 | + struct ContentTypeGuard; |
| 18 | + |
| 19 | + impl guard::Guard for ContentTypeGuard { |
| 20 | + fn check(&self, req: &guard::GuardContext<'_>) -> bool { |
| 21 | + req.head() |
| 22 | + .headers() |
| 23 | + .contains_key(actix_web::http::header::CONTENT_TYPE) |
| 24 | + } |
| 25 | + } |
| 26 | + |
| 27 | + // Data structure for endpoints that receive JSON. |
| 28 | + #[derive(Deserialize)] |
| 29 | + struct UserInfo { |
| 30 | + username: String, |
| 31 | + age: u8, |
| 32 | + } |
| 33 | + |
| 34 | + // GET /introspection for JSON response |
| 35 | + async fn introspection_handler_json( |
| 36 | + tree: web::Data<actix_web::introspection::IntrospectionTree>, |
| 37 | + ) -> impl Responder { |
| 38 | + let report = tree.report_as_json(); |
| 39 | + HttpResponse::Ok() |
| 40 | + .content_type("application/json") |
| 41 | + .body(report) |
| 42 | + } |
| 43 | + |
| 44 | + // GET /introspection/externals for external resources report |
| 45 | + async fn introspection_handler_externals( |
| 46 | + tree: web::Data<actix_web::introspection::IntrospectionTree>, |
| 47 | + ) -> impl Responder { |
| 48 | + let report = tree.report_externals_as_json(); |
| 49 | + HttpResponse::Ok() |
| 50 | + .content_type("application/json") |
| 51 | + .body(report) |
| 52 | + } |
| 53 | + |
| 54 | + // GET /introspection for plain text response |
| 55 | + async fn introspection_handler_text( |
| 56 | + tree: web::Data<actix_web::introspection::IntrospectionTree>, |
| 57 | + ) -> impl Responder { |
| 58 | + let report = tree.report_as_text(); |
| 59 | + HttpResponse::Ok().content_type("text/plain").body(report) |
| 60 | + } |
| 61 | + |
| 62 | + // GET /api/v1/item/{id} and GET /v1/item/{id} |
| 63 | + #[actix_web::get("/item/{id}")] |
| 64 | + async fn get_item(path: web::Path<u32>) -> impl Responder { |
| 65 | + let id = path.into_inner(); |
| 66 | + HttpResponse::Ok().body(format!("Requested item with id: {}", id)) |
| 67 | + } |
| 68 | + |
| 69 | + // POST /api/v1/info |
| 70 | + #[actix_web::post("/info")] |
| 71 | + async fn post_user_info(info: web::Json<UserInfo>) -> impl Responder { |
| 72 | + HttpResponse::Ok().json(format!( |
| 73 | + "User {} with age {} received", |
| 74 | + info.username, info.age |
| 75 | + )) |
| 76 | + } |
| 77 | + |
| 78 | + // /api/v1/guarded |
| 79 | + async fn guarded_handler() -> impl Responder { |
| 80 | + HttpResponse::Ok().body("Passed the Content-Type guard!") |
| 81 | + } |
| 82 | + |
| 83 | + // GET /api/v2/hello |
| 84 | + async fn hello_v2() -> impl Responder { |
| 85 | + HttpResponse::Ok().body("Hello from API v2!") |
| 86 | + } |
| 87 | + |
| 88 | + // GET /admin/dashboard |
| 89 | + async fn admin_dashboard() -> impl Responder { |
| 90 | + HttpResponse::Ok().body("Welcome to the Admin Dashboard!") |
| 91 | + } |
| 92 | + |
| 93 | + // GET /admin/settings |
| 94 | + async fn get_settings() -> impl Responder { |
| 95 | + HttpResponse::Ok().body("Current settings: ...") |
| 96 | + } |
| 97 | + |
| 98 | + // POST /admin/settings |
| 99 | + async fn update_settings() -> impl Responder { |
| 100 | + HttpResponse::Ok().body("Settings have been updated!") |
| 101 | + } |
| 102 | + |
| 103 | + // GET and POST on / |
| 104 | + async fn root_index() -> impl Responder { |
| 105 | + HttpResponse::Ok().body("Welcome to the Root Endpoint!") |
| 106 | + } |
| 107 | + |
| 108 | + // GET /alpha and /beta (named multi-pattern resource) |
| 109 | + async fn multi_pattern() -> impl Responder { |
| 110 | + HttpResponse::Ok().body("Hello from multi-pattern resource!") |
| 111 | + } |
| 112 | + |
| 113 | + // GET /acceptable (Acceptable guard) |
| 114 | + async fn acceptable_guarded() -> impl Responder { |
| 115 | + HttpResponse::Ok().body("Acceptable guard matched!") |
| 116 | + } |
| 117 | + |
| 118 | + // GET /hosted (Host guard) |
| 119 | + async fn host_guarded() -> impl Responder { |
| 120 | + HttpResponse::Ok().body("Host guard matched!") |
| 121 | + } |
| 122 | + |
| 123 | + // Additional endpoints for /extra |
| 124 | + fn extra_endpoints(cfg: &mut web::ServiceConfig) { |
| 125 | + cfg.service( |
| 126 | + web::scope("/extra") |
| 127 | + .route( |
| 128 | + "/ping", |
| 129 | + web::get().to(|| async { HttpResponse::Ok().body("pong") }), // GET /extra/ping |
| 130 | + ) |
| 131 | + .service( |
| 132 | + web::resource("/multi") |
| 133 | + .route(web::get().to(|| async { |
| 134 | + HttpResponse::Ok().body("GET response from /extra/multi") |
| 135 | + })) // GET /extra/multi |
| 136 | + .route(web::post().to(|| async { |
| 137 | + HttpResponse::Ok().body("POST response from /extra/multi") |
| 138 | + })), // POST /extra/multi |
| 139 | + ) |
| 140 | + .service( |
| 141 | + web::scope("{entities_id:\\d+}") |
| 142 | + .service( |
| 143 | + web::scope("/secure") |
| 144 | + .route( |
| 145 | + "", |
| 146 | + web::get().to(|| async { |
| 147 | + HttpResponse::Ok() |
| 148 | + .body("GET response from /extra/secure") |
| 149 | + }), |
| 150 | + ) // GET /extra/{entities_id}/secure/ |
| 151 | + .route( |
| 152 | + "/post", |
| 153 | + web::post().to(|| async { |
| 154 | + HttpResponse::Ok() |
| 155 | + .body("POST response from /extra/secure") |
| 156 | + }), |
| 157 | + ), // POST /extra/{entities_id}/secure/post |
| 158 | + ) |
| 159 | + .wrap_fn(|req, srv| { |
| 160 | + println!( |
| 161 | + "Request to /extra/secure with id: {}", |
| 162 | + req.match_info().get("entities_id").unwrap() |
| 163 | + ); |
| 164 | + let fut = srv.call(req); |
| 165 | + async move { |
| 166 | + let res = fut.await?; |
| 167 | + Ok(res) |
| 168 | + } |
| 169 | + }), |
| 170 | + ), |
| 171 | + ); |
| 172 | + } |
| 173 | + |
| 174 | + // Additional endpoints for /foo |
| 175 | + fn other_endpoints(cfg: &mut web::ServiceConfig) { |
| 176 | + cfg.service( |
| 177 | + web::scope("/extra") |
| 178 | + .route( |
| 179 | + "/ping", |
| 180 | + web::post() |
| 181 | + .to(|| async { HttpResponse::Ok().body("post from /extra/ping") }), // POST /foo/extra/ping |
| 182 | + ) |
| 183 | + .route( |
| 184 | + "/ping", |
| 185 | + web::delete() |
| 186 | + .to(|| async { HttpResponse::Ok().body("delete from /extra/ping") }), // DELETE /foo/extra/ping |
| 187 | + ), |
| 188 | + ); |
| 189 | + } |
| 190 | + |
| 191 | + // Create the HTTP server with all the routes and handlers |
| 192 | + let server = HttpServer::new(|| { |
| 193 | + App::new() |
| 194 | + // Get introspection report |
| 195 | + // curl --location '127.0.0.1:8080/introspection' --header 'Accept: application/json' |
| 196 | + // curl --location '127.0.0.1:8080/introspection' --header 'Accept: text/plain' |
| 197 | + // curl --location '127.0.0.1:8080/introspection/externals' |
| 198 | + .external_resource("app-external", "https://example.com/{id}") |
| 199 | + .service( |
| 200 | + web::resource("/introspection") |
| 201 | + .route( |
| 202 | + web::get() |
| 203 | + .guard(guard::Header("accept", "application/json")) |
| 204 | + .to(introspection_handler_json), |
| 205 | + ) |
| 206 | + .route( |
| 207 | + web::get() |
| 208 | + .guard(guard::Header("accept", "text/plain")) |
| 209 | + .to(introspection_handler_text), |
| 210 | + ), |
| 211 | + ) |
| 212 | + .service( |
| 213 | + web::resource("/introspection/externals") |
| 214 | + .route(web::get().to(introspection_handler_externals)), |
| 215 | + ) |
| 216 | + .service( |
| 217 | + web::resource(["/alpha", "/beta"]) |
| 218 | + .name("multi") |
| 219 | + .route(web::get().to(multi_pattern)), |
| 220 | + ) |
| 221 | + .route( |
| 222 | + "/acceptable", |
| 223 | + web::get() |
| 224 | + .guard(guard::Acceptable::new(mime::APPLICATION_JSON).match_star_star()) |
| 225 | + .to(acceptable_guarded), |
| 226 | + ) |
| 227 | + .route( |
| 228 | + "/hosted", |
| 229 | + web::get().guard(guard::Host("127.0.0.1")).to(host_guarded), |
| 230 | + ) |
| 231 | + // API endpoints under /api |
| 232 | + .service( |
| 233 | + web::scope("/api") |
| 234 | + .configure(|cfg| { |
| 235 | + cfg.external_resource("api-external", "https://api.example/{id}"); |
| 236 | + }) |
| 237 | + // Endpoints under /api/v1 |
| 238 | + .service( |
| 239 | + web::scope("/v1") |
| 240 | + .service(get_item) // GET /api/v1/item/{id} |
| 241 | + .service(post_user_info) // POST /api/v1/info |
| 242 | + .route( |
| 243 | + "/guarded", |
| 244 | + web::route().guard(ContentTypeGuard).to(guarded_handler), // /api/v1/guarded |
| 245 | + ), |
| 246 | + ) |
| 247 | + // Endpoints under /api/v2 |
| 248 | + .service(web::scope("/v2").route("/hello", web::get().to(hello_v2))), // GET /api/v2/hello |
| 249 | + ) |
| 250 | + // Endpoints under /v1 (outside /api) |
| 251 | + .service(web::scope("/v1").service(get_item)) // GET /v1/item/{id} |
| 252 | + // Admin endpoints under /admin |
| 253 | + .service( |
| 254 | + web::scope("/admin") |
| 255 | + .route("/dashboard", web::get().to(admin_dashboard)) // GET /admin/dashboard |
| 256 | + .service( |
| 257 | + web::resource("/settings") |
| 258 | + .route(web::get().to(get_settings)) // GET /admin/settings |
| 259 | + .route(web::post().to(update_settings)), // POST /admin/settings |
| 260 | + ), |
| 261 | + ) |
| 262 | + // Root endpoints |
| 263 | + .service( |
| 264 | + web::resource("/") |
| 265 | + .route(web::get().to(root_index)) // GET / |
| 266 | + .route(web::post().to(root_index)), // POST / |
| 267 | + ) |
| 268 | + // Endpoints under /bar |
| 269 | + .service(web::scope("/bar").configure(extra_endpoints)) // /bar/extra/ping, /bar/extra/multi, etc. |
| 270 | + // Endpoints under /foo |
| 271 | + .service(web::scope("/foo").configure(other_endpoints)) // /foo/extra/ping with POST and DELETE |
| 272 | + // Additional endpoints under /extra |
| 273 | + .configure(extra_endpoints) // /extra/ping, /extra/multi, etc. |
| 274 | + .configure(other_endpoints) |
| 275 | + // Endpoint that rejects GET on /not_guard (allows other methods) |
| 276 | + .route( |
| 277 | + "/not_guard", |
| 278 | + web::route() |
| 279 | + .guard(guard::Not(guard::Get())) |
| 280 | + .to(HttpResponse::MethodNotAllowed), |
| 281 | + ) |
| 282 | + // Endpoint that requires GET with header or POST on /all_guard |
| 283 | + .route( |
| 284 | + "/all_guard", |
| 285 | + web::route() |
| 286 | + .guard( |
| 287 | + guard::All(guard::Get()) |
| 288 | + .and(guard::Header("content-type", "plain/text")) |
| 289 | + .and(guard::Any(guard::Post())), |
| 290 | + ) |
| 291 | + .to(HttpResponse::MethodNotAllowed), |
| 292 | + ) |
| 293 | + }) |
| 294 | + .workers(5) |
| 295 | + .bind("127.0.0.1:8080")?; |
| 296 | + |
| 297 | + server.run().await |
| 298 | + } |
| 299 | + #[cfg(not(feature = "experimental-introspection"))] |
| 300 | + { |
| 301 | + eprintln!("This example requires the 'experimental-introspection' feature to be enabled."); |
| 302 | + std::process::exit(1); |
| 303 | + } |
| 304 | +} |
0 commit comments