churust_core/app.rs
1//! Application assembly: the [`Churust`] entry point, the [`AppBuilder`] DSL,
2//! the immutable [`App`], the [`Plugin`] trait, and the resolved
3//! [`ServerConfig`].
4
5use crate::call::Call;
6use crate::pipeline::{Endpoint, Middleware, Next, Phase};
7use crate::response::Response;
8use crate::router::{Match, RouteBuilder, Router};
9use crate::state::StateMap;
10use bytes::Bytes;
11use futures_util::FutureExt;
12use http::header::ALLOW;
13use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri};
14use std::collections::VecDeque;
15use std::sync::Arc;
16
17/// A reusable bundle of behavior that installs itself into an [`AppBuilder`]
18/// at build time — Churust's analogue of Ktor's `install(Plugin)`.
19///
20/// A plugin typically registers one or more [`Middleware`] (and may add state)
21/// inside [`install`](Plugin::install). Pass a plugin to
22/// [`AppBuilder::install`]; the builder boxes it and calls `install`.
23///
24/// ```
25/// use churust_core::{App, AppBuilder, Churust, Call, Middleware, Next, Plugin, Response, TestClient};
26/// use async_trait::async_trait;
27/// use std::sync::Arc;
28/// use http::{header::HeaderName, HeaderValue};
29///
30/// struct Mark;
31/// #[async_trait]
32/// impl Middleware for Mark {
33/// async fn handle(&self, call: Call, next: Next) -> Response {
34/// let mut res = next.run(call).await;
35/// res.headers.insert(HeaderName::from_static("x-plugin"), HeaderValue::from_static("on"));
36/// res
37/// }
38/// }
39///
40/// struct MarkPlugin;
41/// impl Plugin for MarkPlugin {
42/// fn install(self: Box<Self>, app: &mut AppBuilder) {
43/// app.add_middleware(Arc::new(Mark));
44/// }
45/// }
46/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
47/// let app = Churust::server()
48/// .install(MarkPlugin)
49/// .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
50/// .build();
51/// let res = TestClient::new(app).get("/").send().await;
52/// assert_eq!(res.header("x-plugin"), Some("on"));
53/// # });
54/// ```
55pub trait Plugin {
56 /// Install this plugin into the builder (register middleware, state, etc.).
57 /// Consumes the boxed plugin.
58 fn install(self: Box<Self>, app: &mut AppBuilder);
59}
60
61/// The server configuration resolved at build time and carried by an [`App`].
62///
63/// Populated from defaults, an optional [`Config`](crate::Config), and the
64/// builder's DSL setters. Read it back from a built app with
65/// [`App::config`].
66#[derive(Clone, Debug)]
67pub struct ServerConfig {
68 /// Bind address host.
69 pub host: String,
70 /// Bind port.
71 pub port: u16,
72 /// Maximum accepted request body size in bytes; larger bodies get `413`.
73 pub max_body_bytes: usize,
74 /// Per-request timeout in milliseconds; `0` disables the timeout.
75 pub request_timeout_ms: u64,
76 /// TLS settings, or `None` for plaintext HTTP.
77 pub tls: Option<crate::config::TlsSection>,
78}
79
80impl Default for ServerConfig {
81 fn default() -> Self {
82 Self {
83 host: "127.0.0.1".into(),
84 port: 8080,
85 max_body_bytes: 1 << 20,
86 request_timeout_ms: 30_000,
87 tls: None,
88 }
89 }
90}
91
92/// The fluent builder for an application, returned by [`Churust::server`].
93///
94/// Chain DSL methods to configure the server ([`host`](AppBuilder::host),
95/// [`port`](AppBuilder::port), [`tls`](AppBuilder::tls), ...), register shared
96/// [`state`](AppBuilder::state), [`install`](AppBuilder::install) plugins, and
97/// define routes with [`routing`](AppBuilder::routing). Finish with
98/// [`build`](AppBuilder::build) to get an [`App`], or
99/// [`start`](AppBuilder::start) to build and serve in one step. DSL setters take
100/// precedence over a [`with_config`](AppBuilder::with_config) applied earlier.
101///
102/// ```
103/// use churust_core::{Churust, Call, TestClient};
104/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
105/// let app = Churust::server()
106/// .port(3000)
107/// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
108/// .build();
109/// assert_eq!(app.config().port, 3000);
110/// let res = TestClient::new(app).get("/").send().await;
111/// assert_eq!(res.text(), "hi");
112/// # });
113/// ```
114pub struct AppBuilder {
115 router: Router,
116 middleware: Vec<(Phase, Arc<dyn Middleware>)>,
117 config: ServerConfig,
118 state: StateMap,
119}
120
121impl AppBuilder {
122 fn new() -> Self {
123 Self {
124 router: Router::new(),
125 middleware: Vec::new(),
126 config: ServerConfig::default(),
127 state: StateMap::default(),
128 }
129 }
130
131 /// Set the bind host (default `"127.0.0.1"`). Returns `self` for chaining.
132 pub fn host(mut self, host: impl Into<String>) -> Self {
133 self.config.host = host.into();
134 self
135 }
136 /// Set the bind port (default `8080`). Returns `self` for chaining.
137 pub fn port(mut self, port: u16) -> Self {
138 self.config.port = port;
139 self
140 }
141 /// Set the maximum accepted request body size in bytes (default `1 MiB`).
142 /// Larger bodies are rejected with `413 Payload Too Large`. Returns `self`
143 /// for chaining.
144 pub fn max_body_bytes(mut self, n: usize) -> Self {
145 self.config.max_body_bytes = n;
146 self
147 }
148
149 /// Apply a fully-resolved [`Config`](crate::Config), overwriting the current
150 /// server settings.
151 ///
152 /// This is lowest precedence: DSL setters called *after* it (e.g. another
153 /// [`port`](AppBuilder::port)) win. Use it to seed the builder from a config
154 /// file/env, then override specific fields in code. Returns `self` for
155 /// chaining.
156 pub fn with_config(mut self, cfg: crate::config::Config) -> Self {
157 self.config.host = cfg.server.host;
158 self.config.port = cfg.server.port;
159 self.config.max_body_bytes = cfg.server.max_body_bytes;
160 self.config.request_timeout_ms = cfg.server.request_timeout_ms;
161 self.config.tls = cfg.tls;
162 self
163 }
164
165 /// Set the per-request timeout in milliseconds (default `30000`). A value of
166 /// `0` disables the timeout. Requests exceeding it get `408 Request Timeout`.
167 /// Returns `self` for chaining.
168 pub fn request_timeout_ms(mut self, ms: u64) -> Self {
169 self.config.request_timeout_ms = ms;
170 self
171 }
172
173 /// Enable TLS, reading the certificate chain from `cert_path` and the
174 /// private key from `key_path` (PEM). The files are loaded when the server
175 /// starts; this only records the paths. Requires the `tls` feature to have
176 /// any effect at serve time. Returns `self` for chaining.
177 pub fn tls(mut self, cert_path: impl Into<String>, key_path: impl Into<String>) -> Self {
178 self.config.tls = Some(crate::config::TlsSection {
179 cert: cert_path.into(),
180 key: key_path.into(),
181 });
182 self
183 }
184
185 /// Register a shared application-state value of type `T`, retrieved later
186 /// via the [`State`](crate::State) extractor or
187 /// [`Call::state`](crate::Call::state). One value is held per type;
188 /// registering another `T` replaces it. Returns `self` for chaining.
189 ///
190 /// ```
191 /// use churust_core::{Churust, State, TestClient};
192 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
193 /// #[derive(Clone)]
194 /// struct Db { name: &'static str }
195 /// let app = Churust::server()
196 /// .state(Db { name: "postgres" })
197 /// .routing(|r| { r.get("/", |db: State<Db>| async move { db.name }); })
198 /// .build();
199 /// assert_eq!(TestClient::new(app).get("/").send().await.text(), "postgres");
200 /// # });
201 /// ```
202 pub fn state<T: Send + Sync + 'static>(mut self, value: T) -> Self {
203 self.state.insert(value);
204 self
205 }
206
207 /// Install a [`Plugin`], letting it register middleware/state. Consumes the
208 /// plugin value. Returns `self` for chaining.
209 pub fn install<P: Plugin + 'static>(mut self, plugin: P) -> Self {
210 Box::new(plugin).install(&mut self);
211 self
212 }
213
214 /// Register a [`Middleware`] in a specific [`Phase`]. Plugins use this to
215 /// place their middleware precisely; the builder sorts all middleware by
216 /// phase (stably) at [`build`](AppBuilder::build) time.
217 pub fn add_middleware_in(&mut self, phase: Phase, mw: Arc<dyn Middleware>) {
218 self.middleware.push((phase, mw));
219 }
220
221 /// Register a [`Middleware`] in the default [`Phase::Plugins`] phase — the
222 /// common case for application middleware.
223 pub fn add_middleware(&mut self, mw: Arc<dyn Middleware>) {
224 self.add_middleware_in(Phase::Plugins, mw);
225 }
226
227 /// Define routes via the [`RouteBuilder`] DSL. The closure receives a
228 /// mutable builder on which to register handlers and nested scopes. Returns
229 /// `self` for chaining.
230 ///
231 /// ```
232 /// use churust_core::{Churust, Call, TestClient};
233 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
234 /// let app = Churust::server()
235 /// .routing(|r| {
236 /// r.get("/", |_c: Call| async { "home" });
237 /// r.post("/echo", |mut c: Call| async move { c.receive_text().await.unwrap_or_default() });
238 /// })
239 /// .build();
240 /// assert_eq!(TestClient::new(app).get("/").send().await.text(), "home");
241 /// # });
242 /// ```
243 pub fn routing(mut self, f: impl FnOnce(&mut RouteBuilder)) -> Self {
244 let mut b = RouteBuilder::new(&mut self.router);
245 f(&mut b);
246 self
247 }
248
249 /// Build the app and serve it until Ctrl-C — a shorthand for
250 /// `self.build().start().await`. Binds a socket, so it does not return until
251 /// shutdown.
252 ///
253 /// ```no_run
254 /// use churust_core::{Churust, Call};
255 /// # async fn run() -> std::io::Result<()> {
256 /// Churust::server()
257 /// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
258 /// .start()
259 /// .await
260 /// # }
261 /// ```
262 pub async fn start(self) -> std::io::Result<()> {
263 self.build().start().await
264 }
265
266 /// Finish building into an immutable, cheaply-cloneable [`App`].
267 ///
268 /// Installed middleware is sorted by [`Phase`] (stably, preserving install
269 /// order within a phase) and the configuration is frozen.
270 pub fn build(self) -> App {
271 let mut mw = self.middleware;
272 mw.sort_by_key(|(phase, _)| *phase); // stable: install order preserved within a phase
273 let middleware: Vec<Arc<dyn Middleware>> = mw.into_iter().map(|(_, m)| m).collect();
274 App {
275 inner: Arc::new(AppInner {
276 router: self.router,
277 middleware,
278 config: self.config,
279 state: Arc::new(self.state),
280 }),
281 }
282 }
283}
284
285struct AppInner {
286 router: Router,
287 middleware: Vec<Arc<dyn Middleware>>,
288 config: ServerConfig,
289 state: Arc<StateMap>,
290}
291
292/// An assembled, immutable, cheaply-cloneable application.
293///
294/// Produced by [`AppBuilder::build`]. Internally reference-counted, so cloning
295/// is cheap and clones share the same router, middleware, state, and config.
296/// Serve it with [`start`](App::start) (or
297/// [`start_with_shutdown`](App::start_with_shutdown)), drive a single request
298/// in-process with [`process`](App::process), or hand it to a
299/// [`TestClient`](crate::TestClient) for testing.
300///
301/// ```
302/// use churust_core::{Churust, Call, TestClient};
303/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
304/// let app = Churust::server()
305/// .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
306/// .build();
307/// let clone = app.clone(); // cheap; shares the same inner app
308/// let res = TestClient::new(clone).get("/").send().await;
309/// assert_eq!(res.status().as_u16(), 200);
310/// # });
311/// ```
312#[derive(Clone)]
313pub struct App {
314 inner: Arc<AppInner>,
315}
316
317impl App {
318 /// The resolved [`ServerConfig`] this app was built with.
319 pub fn config(&self) -> &ServerConfig {
320 &self.inner.config
321 }
322
323 /// The single request entry point: run one request through the full
324 /// pipeline (middleware then routed handler) and return the [`Response`].
325 ///
326 /// Both the hyper engine and the [`TestClient`](crate::TestClient) call
327 /// this. It is panic-isolated — a panicking handler is caught and turned
328 /// into `500 Internal Server Error` rather than crashing the task.
329 ///
330 /// ```
331 /// use churust_core::{Churust, Call};
332 /// use http::{HeaderMap, Method};
333 /// use bytes::Bytes;
334 /// # tokio::runtime::Runtime::new().unwrap().block_on(async {
335 /// let app = Churust::server()
336 /// .routing(|r| { r.get("/", |_c: Call| async { "ok" }); })
337 /// .build();
338 /// let res = app.process(Method::GET, "/".parse().unwrap(), HeaderMap::new(), Bytes::new()).await;
339 /// assert_eq!(res.status.as_u16(), 200);
340 /// # });
341 /// ```
342 /// THE single request entry point used by the engine and `TestClient`.
343 /// Panic-isolated: a panicking handler yields 500.
344 pub async fn process(
345 &self,
346 method: Method,
347 uri: Uri,
348 headers: HeaderMap,
349 body: Bytes,
350 ) -> Response {
351 self.process_with_extensions(method, uri, headers, body, http::Extensions::new())
352 .await
353 }
354
355 /// Like [`App::process`], but seeds the `Call` with pre-built extensions
356 /// (e.g. a captured WebSocket upgrade handle). Advanced/engine use.
357 pub async fn process_with_extensions(
358 &self,
359 method: Method,
360 uri: Uri,
361 headers: HeaderMap,
362 body: Bytes,
363 extensions: http::Extensions,
364 ) -> Response {
365 let app = self.clone();
366 let fut = async move {
367 let mut call = Call::new(method, uri, headers, body);
368 call.seed_extensions(extensions);
369 call.set_state(app.inner.state.clone());
370 app.run_pipeline(call).await
371 };
372 match std::panic::AssertUnwindSafe(fut).catch_unwind().await {
373 Ok(res) => res,
374 Err(_) => Response::text("Internal Server Error")
375 .with_status(StatusCode::INTERNAL_SERVER_ERROR),
376 }
377 }
378
379 /// Bind the configured address and serve until Ctrl-C (SIGINT), then drain
380 /// in-flight connections gracefully. Does not return until shutdown.
381 ///
382 /// # Errors
383 ///
384 /// Returns an [`std::io::Error`] if the configured `host:port` is invalid or
385 /// the socket cannot be bound (e.g. the port is in use).
386 ///
387 /// ```no_run
388 /// use churust_core::{Churust, Call};
389 /// # async fn run() -> std::io::Result<()> {
390 /// let app = Churust::server()
391 /// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
392 /// .build();
393 /// app.start().await
394 /// # }
395 /// ```
396 pub async fn start(self) -> std::io::Result<()> {
397 let addr = format!("{}:{}", self.inner.config.host, self.inner.config.port)
398 .parse::<std::net::SocketAddr>()
399 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
400 let shutdown = async {
401 let _ = tokio::signal::ctrl_c().await;
402 };
403 crate::engine::serve(self, addr, shutdown).await
404 }
405
406 /// Bind and serve until the provided `shutdown` future resolves, then drain
407 /// gracefully. Use this to wire a custom shutdown signal (e.g. in tests, or
408 /// to combine SIGTERM with SIGINT).
409 ///
410 /// # Errors
411 ///
412 /// Returns an [`std::io::Error`] if the configured `host:port` is invalid or
413 /// the socket cannot be bound.
414 ///
415 /// ```no_run
416 /// use churust_core::{Churust, Call};
417 /// # async fn run() -> std::io::Result<()> {
418 /// let app = Churust::server()
419 /// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
420 /// .build();
421 /// let (tx, rx) = tokio::sync::oneshot::channel::<()>();
422 /// // ... later: tx.send(()) to trigger shutdown ...
423 /// app.start_with_shutdown(async { let _ = rx.await; }).await
424 /// # }
425 /// ```
426 pub async fn start_with_shutdown<F>(self, shutdown: F) -> std::io::Result<()>
427 where
428 F: std::future::Future<Output = ()> + Send + 'static,
429 {
430 let addr = format!("{}:{}", self.inner.config.host, self.inner.config.port)
431 .parse::<std::net::SocketAddr>()
432 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidInput, e))?;
433 crate::engine::serve(self, addr, shutdown).await
434 }
435
436 async fn run_pipeline(&self, call: Call) -> Response {
437 let inner = self.inner.clone();
438 let endpoint: Endpoint = Arc::new(move |mut call: Call| {
439 let inner = inner.clone();
440 Box::pin(async move {
441 let path = call.path().to_string();
442 let method = call.method().clone();
443 let mut lookup = inner.router.route(&method, &path);
444
445 // RFC 9110 §9.3.2: HEAD must be available wherever GET is.
446 // Only synthesized when no HEAD route was registered, so an
447 // explicit HEAD handler always wins.
448 let mut synthesized_head = false;
449 if method == Method::HEAD && !matches!(lookup, Match::Found { .. }) {
450 if let m @ Match::Found { .. } = inner.router.route(&Method::GET, &path) {
451 lookup = m;
452 synthesized_head = true;
453 }
454 }
455
456 // Automatic OPTIONS, only where no handler claimed the request.
457 // CORS runs in the Plugins phase and short-circuits preflight
458 // before this endpoint is reached, so an installed Cors keeps
459 // priority over this.
460 if method == Method::OPTIONS && !matches!(lookup, Match::Found { .. }) {
461 let mut allow = inner.router.methods_for(&path);
462 if !allow.is_empty() {
463 if allow.contains(&Method::GET) && !allow.contains(&Method::HEAD) {
464 allow.push(Method::HEAD);
465 }
466 allow.push(Method::OPTIONS);
467 let value = allow
468 .iter()
469 .map(|m| m.as_str())
470 .collect::<Vec<_>>()
471 .join(", ");
472 return Response::new(StatusCode::NO_CONTENT).with_header(
473 ALLOW,
474 HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
475 );
476 }
477 }
478
479 match lookup {
480 Match::Found { handler, params } => {
481 call.set_params(params);
482 let res = handler.handle(call).await;
483 if synthesized_head {
484 strip_body(res)
485 } else {
486 res
487 }
488 }
489 Match::MethodNotAllowed { allow } => {
490 let value = allow
491 .iter()
492 .map(|m| m.as_str())
493 .collect::<Vec<_>>()
494 .join(", ");
495 Response::new(StatusCode::METHOD_NOT_ALLOWED).with_header(
496 ALLOW,
497 HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
498 )
499 }
500 Match::NotFound => {
501 Response::text("Not Found").with_status(StatusCode::NOT_FOUND)
502 }
503 // A malformed escape or non-UTF-8 bytes in the path. The
504 // request is unintelligible rather than merely unmatched,
505 // so it is a 400 and not a 404.
506 Match::BadPath => {
507 Response::text("Bad Request").with_status(StatusCode::BAD_REQUEST)
508 }
509 }
510 }) as _
511 });
512
513 let chain: VecDeque<Arc<dyn Middleware>> = self.inner.middleware.iter().cloned().collect();
514 Next::new(chain, endpoint).run(call).await
515 }
516}
517
518/// Drop a response body for a synthesized `HEAD` reply, keeping status and
519/// headers.
520///
521/// RFC 9110 §9.3.2 says a `HEAD` response should carry the same header fields a
522/// `GET` would, so a buffered body's length is preserved as `Content-Length`
523/// before the bytes are discarded — clients do use `HEAD` to size a resource.
524///
525/// A streamed body has no known length and is dropped rather than drained:
526/// draining it would do exactly the work the client declined to ask for. The
527/// same section permits omitting fields that are only determined while
528/// generating the content, which is precisely this case.
529fn strip_body(mut res: Response) -> Response {
530 if let Some(bytes) = res.body.as_bytes() {
531 if let Ok(value) = HeaderValue::from_str(&bytes.len().to_string()) {
532 res.headers.insert(http::header::CONTENT_LENGTH, value);
533 }
534 }
535 res.body = crate::body::Body::empty();
536 res
537}
538
539/// The framework entry point — a zero-sized namespace for starting an
540/// [`AppBuilder`].
541///
542/// Begin with [`Churust::server`] for an empty builder, or
543/// [`Churust::from_config`] to seed it from `churust.toml` plus `CHURUST_*`
544/// environment variables.
545///
546/// ```
547/// use churust_core::{Churust, Call, TestClient};
548/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
549/// let app = Churust::server()
550/// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
551/// .build();
552/// assert_eq!(TestClient::new(app).get("/").send().await.text(), "hi");
553/// # });
554/// ```
555pub struct Churust;
556
557impl Churust {
558 /// Start a fresh [`AppBuilder`] with default configuration.
559 pub fn server() -> AppBuilder {
560 AppBuilder::new()
561 }
562
563 /// Start an [`AppBuilder`] pre-loaded from `churust.toml` and `CHURUST_*`
564 /// environment variables (via [`Config::load_default`](crate::Config::load_default)).
565 /// Chain DSL setters afterward to override individual fields.
566 ///
567 /// ```no_run
568 /// use churust_core::{Churust, Call};
569 /// // Loads churust.toml + env, then overrides the port in code.
570 /// let app = Churust::from_config()
571 /// .port(3000)
572 /// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
573 /// .build();
574 /// let _ = app;
575 /// ```
576 pub fn from_config() -> AppBuilder {
577 AppBuilder::new().with_config(crate::config::Config::load_default())
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584 use crate::pipeline::Next;
585 use async_trait::async_trait;
586
587 #[tokio::test]
588 async fn middleware_runs_in_phase_order() {
589 use crate::pipeline::{Next, Phase};
590 use async_trait::async_trait;
591 use std::sync::{Arc, Mutex};
592
593 #[derive(Clone)]
594 struct Recorder {
595 log: Arc<Mutex<Vec<&'static str>>>,
596 tag: &'static str,
597 }
598 #[async_trait]
599 impl Middleware for Recorder {
600 async fn handle(&self, call: Call, next: Next) -> Response {
601 self.log.lock().unwrap().push(self.tag);
602 next.run(call).await
603 }
604 }
605
606 let log = Arc::new(Mutex::new(Vec::new()));
607 let mut builder = Churust::server();
608 // Install OUT of phase order; expect execution IN phase order.
609 builder.add_middleware_in(
610 Phase::Fallback,
611 Arc::new(Recorder {
612 log: log.clone(),
613 tag: "fallback",
614 }),
615 );
616 builder.add_middleware_in(
617 Phase::Setup,
618 Arc::new(Recorder {
619 log: log.clone(),
620 tag: "setup",
621 }),
622 );
623 builder.add_middleware_in(
624 Phase::Monitoring,
625 Arc::new(Recorder {
626 log: log.clone(),
627 tag: "monitoring",
628 }),
629 );
630 let app = builder
631 .routing(|r| {
632 r.get("/", |_c: Call| async { "ok" });
633 })
634 .build();
635 let _ = get(&app, "/").await;
636 assert_eq!(
637 *log.lock().unwrap(),
638 vec!["setup", "monitoring", "fallback"]
639 );
640 }
641
642 fn app() -> App {
643 Churust::server()
644 .routing(|r| {
645 r.get("/", |_c: Call| async { "home" });
646 r.get("/boom", |_c: Call| async {
647 panic!("handler exploded");
648 #[allow(unreachable_code)]
649 ""
650 });
651 })
652 .build()
653 }
654
655 async fn get(app: &App, path: &str) -> Response {
656 app.process(
657 Method::GET,
658 path.parse::<Uri>().unwrap(),
659 HeaderMap::new(),
660 Bytes::new(),
661 )
662 .await
663 }
664
665 #[tokio::test]
666 async fn routes_to_handler() {
667 let res = get(&app(), "/").await;
668 assert_eq!(res.status, StatusCode::OK);
669 assert_eq!(res.body, Bytes::from("home"));
670 }
671
672 #[tokio::test]
673 async fn unknown_path_is_404() {
674 let res = get(&app(), "/missing").await;
675 assert_eq!(res.status, StatusCode::NOT_FOUND);
676 }
677
678 #[tokio::test]
679 async fn panicking_handler_yields_500_not_crash() {
680 let res = get(&app(), "/boom").await;
681 assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
682 }
683
684 #[tokio::test]
685 async fn process_with_extensions_seeds_call() {
686 #[derive(Clone)]
687 struct Marker(u32);
688 let app = Churust::server()
689 .routing(|r| {
690 r.get("/", |c: Call| async move {
691 format!("{}", c.get::<Marker>().map(|m| m.0).unwrap_or(0))
692 });
693 })
694 .build();
695 let mut ext = http::Extensions::new();
696 ext.insert(Marker(7));
697 let res = app
698 .process_with_extensions(
699 Method::GET,
700 "/".parse().unwrap(),
701 HeaderMap::new(),
702 Bytes::new(),
703 ext,
704 )
705 .await;
706 assert_eq!(res.body, Bytes::from("7"));
707 }
708
709 #[tokio::test]
710 async fn state_extractor_end_to_end() {
711 use crate::extract::State;
712 #[derive(Clone)]
713 struct Counter(u32);
714
715 let app = Churust::server()
716 .state(Counter(5))
717 .routing(|r| {
718 r.get(
719 "/n",
720 |s: State<Counter>| async move { format!("n={}", s.0 .0) },
721 );
722 })
723 .build();
724 let res = get(&app, "/n").await;
725 assert_eq!(res.body, Bytes::from("n=5"));
726 }
727
728 #[tokio::test]
729 async fn middleware_installed_via_plugin_runs() {
730 struct MarkPlugin;
731 struct Mark;
732 #[async_trait]
733 impl Middleware for Mark {
734 async fn handle(&self, call: Call, next: Next) -> Response {
735 let mut res = next.run(call).await;
736 res.headers.insert(
737 http::header::HeaderName::from_static("x-plugin"),
738 HeaderValue::from_static("on"),
739 );
740 res
741 }
742 }
743 impl Plugin for MarkPlugin {
744 fn install(self: Box<Self>, app: &mut AppBuilder) {
745 app.add_middleware(Arc::new(Mark));
746 }
747 }
748
749 let app = Churust::server()
750 .install(MarkPlugin)
751 .routing(|r| {
752 r.get("/", |_c: Call| async { "ok" });
753 })
754 .build();
755 let res = get(&app, "/").await;
756 assert_eq!(res.headers.get("x-plugin").unwrap(), "on");
757 }
758}