Skip to main content

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}