rust_web_server/app/mod.rs
1#[cfg(test)]
2mod tests;
3
4pub mod controller;
5
6use crate::app::controller::favicon::FaviconController;
7use crate::app::controller::health::HealthController;
8use crate::app::controller::ready::ReadyController;
9use crate::app::controller::metrics::MetricsController;
10use crate::app::controller::file::initiate::FileUploadInitiateController;
11use crate::app::controller::form::get_method::FormGetMethodController;
12use crate::app::controller::form::multipart_enctype_post_method::FormMultipartEnctypePostMethodController;
13use crate::app::controller::form::url_encoded_enctype_post_method::FormUrlEncodedEnctypePostMethodController;
14use crate::app::controller::index::IndexController;
15use crate::app::controller::not_found::NotFoundController;
16use crate::app::controller::script::ScriptController;
17use crate::app::controller::static_resource::StaticResourceController;
18use crate::app::controller::style::StyleController;
19use crate::application::Application;
20use crate::controller::Controller;
21use crate::core::New;
22use crate::header::Header;
23use crate::middleware::{Middleware, WithMiddleware};
24use crate::request::Request;
25use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
26use crate::server::ConnectionInfo;
27use crate::state::AppWithState;
28
29/// A pair of function pointers representing one entry in the controller chain.
30struct ControllerEntry {
31 is_matching: fn(&Request, &ConnectionInfo) -> bool,
32 process: fn(&Request, Response, &ConnectionInfo) -> Response,
33}
34
35/// Build a [`ControllerEntry`] from any type that implements [`Controller`].
36fn entry<C: Controller>() -> ControllerEntry {
37 ControllerEntry {
38 is_matching: C::is_matching,
39 process: C::process,
40 }
41}
42
43/// The built-in HTTP application. Serves static files, favicons, forms,
44/// file uploads, health probes, metrics, and a 404 fallback.
45///
46/// Use as-is or compose with the framework's building blocks:
47///
48/// ```rust,no_run
49/// use rust_web_server::app::App;
50/// use rust_web_server::middleware::{WithMiddleware, RateLimitLayer};
51/// use rust_web_server::core::New;
52///
53/// // Middleware stack around the built-in app
54/// let app = App::new().wrap(RateLimitLayer);
55/// ```
56///
57/// For user-defined routes with shared state, call [`App::with_state`]:
58///
59/// ```rust,no_run
60/// use rust_web_server::app::App;
61/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
62/// use rust_web_server::core::New;
63///
64/// struct State { version: &'static str }
65///
66/// let app = App::with_state(State { version: "1.0" })
67/// .get("/version", |_req, _params, _conn, state| {
68/// let mut r = Response::new();
69/// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
70/// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
71/// r
72/// });
73/// ```
74#[derive(Copy, Clone)]
75pub struct App {}
76
77impl New for App {
78 fn new() -> Self {
79 App{}
80 }
81}
82
83impl Application for App {
84 fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
85 let header_list = Header::get_header_list(request);
86 let response = Response::get_response(
87 STATUS_CODE_REASON_PHRASE.n501_not_implemented,
88 Some(header_list),
89 None,
90 );
91
92 let controllers = [
93 entry::<IndexController>(),
94 entry::<StyleController>(),
95 entry::<ScriptController>(),
96 entry::<FileUploadInitiateController>(),
97 entry::<FormUrlEncodedEnctypePostMethodController>(),
98 entry::<FormGetMethodController>(),
99 entry::<FormMultipartEnctypePostMethodController>(),
100 entry::<HealthController>(),
101 entry::<ReadyController>(),
102 entry::<MetricsController>(),
103 entry::<FaviconController>(),
104 entry::<StaticResourceController>(),
105 entry::<NotFoundController>(),
106 ];
107
108 for c in &controllers {
109 if (c.is_matching)(request, connection) {
110 return Ok((c.process)(request, response, connection));
111 }
112 }
113
114 Ok(response)
115 }
116}
117
118impl App {
119 /// Dispatch `request` through the controller chain and return the response.
120 ///
121 /// This is a convenience wrapper over [`Application::execute`] that uses a
122 /// synthetic loopback [`ConnectionInfo`]. Use it in tests or when no real
123 /// connection context is available. Prefer [`TestClient`] for structured
124 /// test code.
125 ///
126 /// [`TestClient`]: crate::test_client::TestClient
127 pub fn handle_request(request: Request) -> (Response, Request) {
128 use crate::server::Address;
129 let conn = ConnectionInfo {
130 client: Address { ip: "127.0.0.1".to_string(), port: 0 },
131 server: Address { ip: "127.0.0.1".to_string(), port: 7878 },
132 request_size: 16000,
133 };
134 let app = App::new();
135 let response = app.execute(&request, &conn).unwrap_or_else(|_| {
136 let header_list = Header::get_header_list(&request);
137 Response::get_response(
138 STATUS_CODE_REASON_PHRASE.n500_internal_server_error,
139 Some(header_list),
140 None,
141 )
142 });
143 (response, request)
144 }
145
146 /// Create a state-aware application. Routes registered on the returned
147 /// [`AppWithState<S>`] are tried first; unmatched requests fall through to
148 /// the built-in controller chain (static files, health probes, etc.).
149 ///
150 /// The state is stored as `Arc<S>` and shared across all handlers.
151 ///
152 /// # Example
153 ///
154 /// ```rust,no_run
155 /// use rust_web_server::app::App;
156 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
157 /// use rust_web_server::core::New;
158 ///
159 /// struct Db { url: String }
160 ///
161 /// let app = App::with_state(Db { url: "postgres://...".to_string() })
162 /// .get("/ping", |_req, _params, _conn, db| {
163 /// let mut r = Response::new();
164 /// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
165 /// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
166 /// r
167 /// });
168 /// ```
169 pub fn with_state<S: Send + Sync + 'static>(state: S) -> AppWithState<S> {
170 AppWithState::new(state)
171 }
172
173 /// Wrap this application in a middleware layer.
174 ///
175 /// Returns a [`WithMiddleware<App>`] that runs `layer` before every
176 /// request. Chain `.wrap()` calls to stack multiple layers:
177 ///
178 /// ```rust,no_run
179 /// use rust_web_server::app::App;
180 /// use rust_web_server::middleware::RateLimitLayer;
181 /// use rust_web_server::core::New;
182 ///
183 /// let app = App::new().wrap(RateLimitLayer);
184 /// ```
185 pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<App> {
186 WithMiddleware::new(self).wrap(layer)
187 }
188
189 /// Create an async state-aware application (requires the `http2` feature).
190 ///
191 /// Handlers are `async fn` closures that can `await` database queries,
192 /// HTTP clients, or any other async I/O. Unmatched routes fall through to
193 /// the built-in controller chain.
194 ///
195 /// # Example
196 ///
197 /// ```rust,no_run
198 /// use std::sync::Arc;
199 /// use rust_web_server::app::App;
200 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
201 /// use rust_web_server::core::New;
202 ///
203 /// struct Db { url: String }
204 ///
205 /// let app = App::with_async_state(Db { url: "postgres://...".to_string() })
206 /// .get("/ping", |_req, _params, _conn, state| async move {
207 /// // state: Arc<Db>
208 /// let mut r = Response::new();
209 /// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
210 /// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
211 /// r
212 /// });
213 /// ```
214 #[cfg(feature = "http2")]
215 pub fn with_async_state<S: Send + Sync + 'static>(state: S) -> crate::async_state::AsyncAppWithState<S> {
216 crate::async_state::AsyncAppWithState::new(state)
217 }
218}