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 match inner.router.route(call.method(), call.path()) {
442 Match::Found { handler, params } => {
443 call.set_params(params);
444 handler.handle(call).await
445 }
446 Match::MethodNotAllowed { allow } => {
447 let value = allow
448 .iter()
449 .map(|m| m.as_str())
450 .collect::<Vec<_>>()
451 .join(", ");
452 Response::new(StatusCode::METHOD_NOT_ALLOWED).with_header(
453 ALLOW,
454 HeaderValue::from_str(&value).unwrap_or(HeaderValue::from_static("")),
455 )
456 }
457 Match::NotFound => {
458 Response::text("Not Found").with_status(StatusCode::NOT_FOUND)
459 }
460 }
461 }) as _
462 });
463
464 let chain: VecDeque<Arc<dyn Middleware>> = self.inner.middleware.iter().cloned().collect();
465 Next::new(chain, endpoint).run(call).await
466 }
467}
468
469/// The framework entry point — a zero-sized namespace for starting an
470/// [`AppBuilder`].
471///
472/// Begin with [`Churust::server`] for an empty builder, or
473/// [`Churust::from_config`] to seed it from `churust.toml` plus `CHURUST_*`
474/// environment variables.
475///
476/// ```
477/// use churust_core::{Churust, Call, TestClient};
478/// # tokio::runtime::Runtime::new().unwrap().block_on(async {
479/// let app = Churust::server()
480/// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
481/// .build();
482/// assert_eq!(TestClient::new(app).get("/").send().await.text(), "hi");
483/// # });
484/// ```
485pub struct Churust;
486
487impl Churust {
488 /// Start a fresh [`AppBuilder`] with default configuration.
489 pub fn server() -> AppBuilder {
490 AppBuilder::new()
491 }
492
493 /// Start an [`AppBuilder`] pre-loaded from `churust.toml` and `CHURUST_*`
494 /// environment variables (via [`Config::load_default`](crate::Config::load_default)).
495 /// Chain DSL setters afterward to override individual fields.
496 ///
497 /// ```no_run
498 /// use churust_core::{Churust, Call};
499 /// // Loads churust.toml + env, then overrides the port in code.
500 /// let app = Churust::from_config()
501 /// .port(3000)
502 /// .routing(|r| { r.get("/", |_c: Call| async { "hi" }); })
503 /// .build();
504 /// let _ = app;
505 /// ```
506 pub fn from_config() -> AppBuilder {
507 AppBuilder::new().with_config(crate::config::Config::load_default())
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use crate::pipeline::Next;
515 use async_trait::async_trait;
516
517 #[tokio::test]
518 async fn middleware_runs_in_phase_order() {
519 use crate::pipeline::{Next, Phase};
520 use async_trait::async_trait;
521 use std::sync::{Arc, Mutex};
522
523 #[derive(Clone)]
524 struct Recorder {
525 log: Arc<Mutex<Vec<&'static str>>>,
526 tag: &'static str,
527 }
528 #[async_trait]
529 impl Middleware for Recorder {
530 async fn handle(&self, call: Call, next: Next) -> Response {
531 self.log.lock().unwrap().push(self.tag);
532 next.run(call).await
533 }
534 }
535
536 let log = Arc::new(Mutex::new(Vec::new()));
537 let mut builder = Churust::server();
538 // Install OUT of phase order; expect execution IN phase order.
539 builder.add_middleware_in(
540 Phase::Fallback,
541 Arc::new(Recorder {
542 log: log.clone(),
543 tag: "fallback",
544 }),
545 );
546 builder.add_middleware_in(
547 Phase::Setup,
548 Arc::new(Recorder {
549 log: log.clone(),
550 tag: "setup",
551 }),
552 );
553 builder.add_middleware_in(
554 Phase::Monitoring,
555 Arc::new(Recorder {
556 log: log.clone(),
557 tag: "monitoring",
558 }),
559 );
560 let app = builder
561 .routing(|r| {
562 r.get("/", |_c: Call| async { "ok" });
563 })
564 .build();
565 let _ = get(&app, "/").await;
566 assert_eq!(
567 *log.lock().unwrap(),
568 vec!["setup", "monitoring", "fallback"]
569 );
570 }
571
572 fn app() -> App {
573 Churust::server()
574 .routing(|r| {
575 r.get("/", |_c: Call| async { "home" });
576 r.get("/boom", |_c: Call| async {
577 panic!("handler exploded");
578 #[allow(unreachable_code)]
579 ""
580 });
581 })
582 .build()
583 }
584
585 async fn get(app: &App, path: &str) -> Response {
586 app.process(
587 Method::GET,
588 path.parse::<Uri>().unwrap(),
589 HeaderMap::new(),
590 Bytes::new(),
591 )
592 .await
593 }
594
595 #[tokio::test]
596 async fn routes_to_handler() {
597 let res = get(&app(), "/").await;
598 assert_eq!(res.status, StatusCode::OK);
599 assert_eq!(res.body, Bytes::from("home"));
600 }
601
602 #[tokio::test]
603 async fn unknown_path_is_404() {
604 let res = get(&app(), "/missing").await;
605 assert_eq!(res.status, StatusCode::NOT_FOUND);
606 }
607
608 #[tokio::test]
609 async fn panicking_handler_yields_500_not_crash() {
610 let res = get(&app(), "/boom").await;
611 assert_eq!(res.status, StatusCode::INTERNAL_SERVER_ERROR);
612 }
613
614 #[tokio::test]
615 async fn process_with_extensions_seeds_call() {
616 #[derive(Clone)]
617 struct Marker(u32);
618 let app = Churust::server()
619 .routing(|r| {
620 r.get("/", |c: Call| async move {
621 format!("{}", c.get::<Marker>().map(|m| m.0).unwrap_or(0))
622 });
623 })
624 .build();
625 let mut ext = http::Extensions::new();
626 ext.insert(Marker(7));
627 let res = app
628 .process_with_extensions(
629 Method::GET,
630 "/".parse().unwrap(),
631 HeaderMap::new(),
632 Bytes::new(),
633 ext,
634 )
635 .await;
636 assert_eq!(res.body, Bytes::from("7"));
637 }
638
639 #[tokio::test]
640 async fn state_extractor_end_to_end() {
641 use crate::extract::State;
642 #[derive(Clone)]
643 struct Counter(u32);
644
645 let app = Churust::server()
646 .state(Counter(5))
647 .routing(|r| {
648 r.get(
649 "/n",
650 |s: State<Counter>| async move { format!("n={}", s.0 .0) },
651 );
652 })
653 .build();
654 let res = get(&app, "/n").await;
655 assert_eq!(res.body, Bytes::from("n=5"));
656 }
657
658 #[tokio::test]
659 async fn middleware_installed_via_plugin_runs() {
660 struct MarkPlugin;
661 struct Mark;
662 #[async_trait]
663 impl Middleware for Mark {
664 async fn handle(&self, call: Call, next: Next) -> Response {
665 let mut res = next.run(call).await;
666 res.headers.insert(
667 http::header::HeaderName::from_static("x-plugin"),
668 HeaderValue::from_static("on"),
669 );
670 res
671 }
672 }
673 impl Plugin for MarkPlugin {
674 fn install(self: Box<Self>, app: &mut AppBuilder) {
675 app.add_middleware(Arc::new(Mark));
676 }
677 }
678
679 let app = Churust::server()
680 .install(MarkPlugin)
681 .routing(|r| {
682 r.get("/", |_c: Call| async { "ok" });
683 })
684 .build();
685 let res = get(&app, "/").await;
686 assert_eq!(res.headers.get("x-plugin").unwrap(), "on");
687 }
688}