rust_web_server/app/mod.rs
1#[cfg(test)]
2mod tests;
3
4pub mod controller;
5
6use std::sync::Arc;
7
8use crate::app::controller::favicon::FaviconController;
9use crate::app::controller::health::HealthController;
10use crate::app::controller::ready::ReadyController;
11use crate::app::controller::metrics::MetricsController;
12use crate::app::controller::file::initiate::FileUploadInitiateController;
13use crate::app::controller::form::get_method::FormGetMethodController;
14use crate::app::controller::form::multipart_enctype_post_method::FormMultipartEnctypePostMethodController;
15use crate::app::controller::form::url_encoded_enctype_post_method::FormUrlEncodedEnctypePostMethodController;
16use crate::app::controller::index::IndexController;
17use crate::app::controller::not_found::NotFoundController;
18use crate::app::controller::script::ScriptController;
19use crate::app::controller::static_resource::StaticResourceController;
20use crate::app::controller::style::StyleController;
21use crate::application::Application;
22use crate::controller::Controller;
23use crate::core::New;
24use crate::header::Header;
25use crate::mcp::McpServer;
26use crate::middleware::{Middleware, WithMiddleware};
27use crate::request::Request;
28use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
29use crate::server::ConnectionInfo;
30use crate::server_config::ServerConfig;
31use crate::state::AppWithState;
32
33/// A pair of function pointers representing one entry in the controller chain.
34struct ControllerEntry {
35 is_matching: fn(&Request, &ConnectionInfo) -> bool,
36 process: fn(&Request, Response, &ConnectionInfo) -> Response,
37}
38
39/// Build a [`ControllerEntry`] from any type that implements [`Controller`].
40fn entry<C: Controller>() -> ControllerEntry {
41 ControllerEntry {
42 is_matching: C::is_matching,
43 process: C::process,
44 }
45}
46
47/// The built-in HTTP application. Serves static files, favicons, forms,
48/// file uploads, health probes, metrics, and a 404 fallback.
49///
50/// Use as-is or compose with the framework's building blocks:
51///
52/// ```rust,no_run
53/// use rust_web_server::app::App;
54/// use rust_web_server::middleware::{WithMiddleware, RateLimitLayer};
55/// use rust_web_server::core::New;
56///
57/// // Middleware stack around the built-in app
58/// let app = App::new().wrap(RateLimitLayer);
59/// ```
60///
61/// For user-defined routes with shared state, call [`App::with_state`]:
62///
63/// ```rust,no_run
64/// use rust_web_server::app::App;
65/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
66/// use rust_web_server::core::New;
67///
68/// struct State { version: &'static str }
69///
70/// let app = App::with_state(State { version: "1.0" })
71/// .get("/version", |_req, _params, _conn, state| {
72/// let mut r = Response::new();
73/// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
74/// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
75/// r
76/// });
77/// ```
78///
79/// # Per-instance configuration
80///
81/// By default `App::new()` reads CORS, CSP, and other settings from
82/// environment variables on each request (matching the current process state,
83/// including hot-reloaded values). Call [`App::with_config`] to pin the
84/// configuration to a specific [`ServerConfig`] — this is the recommended
85/// pattern for parallel integration tests, which should not touch environment
86/// variables at all.
87///
88/// ```rust,ignore
89/// use rust_web_server::app::App;
90/// use rust_web_server::server_config::ServerConfig;
91/// use rust_web_server::test_client::TestClient;
92///
93/// // No env writes, no lock needed — safe to run in parallel.
94/// let app = App::with_config(ServerConfig {
95/// cors_allow_all: false,
96/// cors_allow_origins: "https://trusted.example.com".to_string(),
97/// ..ServerConfig::default()
98/// });
99/// let client = TestClient::new(app);
100/// ```
101#[derive(Clone)]
102pub struct App {
103 /// When `Some`, every request is served using this fixed config.
104 /// When `None`, config is read from environment variables on each request
105 /// (supports hot-reload via SIGHUP / `POST /admin/config/reload`).
106 config: Option<Arc<ServerConfig>>,
107}
108
109impl New for App {
110 fn new() -> Self {
111 App { config: None }
112 }
113}
114
115impl App {
116 /// Create an `App` pinned to an explicit [`ServerConfig`].
117 ///
118 /// All CORS, CSP, and other header settings are taken from `config` rather
119 /// than from environment variables. The configuration is fixed for the
120 /// lifetime of the `App` instance — SIGHUP / hot-reload do not affect it.
121 ///
122 /// This is the preferred constructor for integration tests: build a
123 /// [`ServerConfig`] with the exact settings under test, pass it here, and
124 /// use [`TestClient`] to drive requests. No environment writes and no
125 /// [`test_env::lock()`] are needed.
126 ///
127 /// [`TestClient`]: crate::test_client::TestClient
128 /// [`test_env::lock()`]: crate::test_env::lock
129 pub fn with_config(config: ServerConfig) -> Self {
130 App { config: Some(Arc::new(config)) }
131 }
132}
133
134impl Application for App {
135 // `App` dispatches through a fixed if-chain of built-in `Controller`s
136 // rather than a `Router`: the built-in set is small, static, and known
137 // at compile time, so a linear scan is simpler and just as fast as a
138 // segment-matching router would be here. This is a deliberate choice,
139 // not an oversight — `Router` (src/router/mod.rs) is the right tool for
140 // user-defined routes with named path params/wildcards, and `AppWithState`
141 // / `AsyncAppWithState` use exactly that, falling through to this same
142 // controller chain for anything they don't handle themselves.
143 fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
144 let config = match &self.config {
145 Some(c) => (**c).clone(),
146 None => ServerConfig::from_env(),
147 };
148 let header_list = Header::get_header_list_with_config(request, &config);
149 let response = Response::get_response(
150 STATUS_CODE_REASON_PHRASE.n501_not_implemented,
151 Some(header_list),
152 None,
153 );
154
155 let controllers = [
156 entry::<IndexController>(),
157 entry::<StyleController>(),
158 entry::<ScriptController>(),
159 entry::<FileUploadInitiateController>(),
160 entry::<FormUrlEncodedEnctypePostMethodController>(),
161 entry::<FormGetMethodController>(),
162 entry::<FormMultipartEnctypePostMethodController>(),
163 entry::<HealthController>(),
164 entry::<ReadyController>(),
165 entry::<MetricsController>(),
166 entry::<FaviconController>(),
167 entry::<StaticResourceController>(),
168 entry::<NotFoundController>(),
169 ];
170
171 for c in &controllers {
172 if (c.is_matching)(request, connection) {
173 return Ok((c.process)(request, response, connection));
174 }
175 }
176
177 Ok(response)
178 }
179}
180
181impl App {
182 /// Dispatch `request` through the controller chain and return the response.
183 ///
184 /// This is a convenience wrapper over [`Application::execute`] that uses a
185 /// synthetic loopback [`ConnectionInfo`]. Use it in tests or when no real
186 /// connection context is available. Prefer [`TestClient`] for structured
187 /// test code.
188 ///
189 /// [`TestClient`]: crate::test_client::TestClient
190 pub fn handle_request(request: Request) -> (Response, Request) {
191 use crate::server::Address;
192 let conn = ConnectionInfo {
193 client: Address { ip: "127.0.0.1".to_string(), port: 0 },
194 server: Address { ip: "127.0.0.1".to_string(), port: 7878 },
195 request_size: 16000,
196 sni_hostname: None,
197 };
198 let app = App::new();
199 let response = app.execute(&request, &conn).unwrap_or_else(|_| {
200 let header_list = Header::get_header_list_with_config(&request, &ServerConfig::from_env());
201 Response::get_response(
202 STATUS_CODE_REASON_PHRASE.n500_internal_server_error,
203 Some(header_list),
204 None,
205 )
206 });
207 (response, request)
208 }
209
210 /// Create a state-aware application. Routes registered on the returned
211 /// [`AppWithState<S>`] are tried first; unmatched requests fall through to
212 /// the built-in controller chain (static files, health probes, etc.).
213 ///
214 /// The state is stored as `Arc<S>` and shared across all handlers.
215 ///
216 /// # Example
217 ///
218 /// ```rust,no_run
219 /// use rust_web_server::app::App;
220 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
221 /// use rust_web_server::core::New;
222 ///
223 /// struct Db { url: String }
224 ///
225 /// let app = App::with_state(Db { url: "postgres://...".to_string() })
226 /// .get("/ping", |_req, _params, _conn, db| {
227 /// let mut r = Response::new();
228 /// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
229 /// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
230 /// r
231 /// });
232 /// ```
233 pub fn with_state<S: Send + Sync + 'static>(state: S) -> AppWithState<S> {
234 AppWithState::new(state)
235 }
236
237 /// Wrap this application in a middleware layer.
238 ///
239 /// Returns a [`WithMiddleware<App>`] that runs `layer` before every
240 /// request. Chain `.wrap()` calls to stack multiple layers:
241 ///
242 /// ```rust,no_run
243 /// use rust_web_server::app::App;
244 /// use rust_web_server::middleware::RateLimitLayer;
245 /// use rust_web_server::core::New;
246 ///
247 /// let app = App::new().wrap(RateLimitLayer);
248 /// ```
249 pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<App> {
250 WithMiddleware::new(self).wrap(layer)
251 }
252
253 /// Attach an MCP server to this application. Tools, resources, and
254 /// prompts are registered on the returned [`McpServer`]; requests that
255 /// do not match the MCP endpoint are forwarded to `self` (static files,
256 /// health probes, any custom routes registered before this call).
257 ///
258 /// ```rust,no_run
259 /// use rust_web_server::app::App;
260 /// use rust_web_server::mcp::{McpContent, extract_arg};
261 /// use rust_web_server::core::New;
262 ///
263 /// // Pure MCP — unmatched paths handled by built-in App
264 /// let app = App::new()
265 /// .mcp("my-server", "1.0")
266 /// .tool(
267 /// "echo",
268 /// "Echo text back",
269 /// r#"{"type":"object","properties":{"text":{"type":"string"}}}"#,
270 /// |args| Ok(McpContent::text(extract_arg(args, "text").unwrap_or_default())),
271 /// );
272 /// ```
273 ///
274 /// To combine with custom HTTP routes, start from [`App::with_state`]:
275 ///
276 /// ```rust,no_run
277 /// use rust_web_server::app::App;
278 /// use rust_web_server::mcp::McpContent;
279 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
280 /// use rust_web_server::core::New;
281 ///
282 /// let app = App::with_state(())
283 /// .get("/api/ping", |_, _, _, _| {
284 /// let mut r = Response::new();
285 /// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
286 /// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
287 /// r
288 /// })
289 /// .mcp("my-server", "1.0")
290 /// .tool("ping", "Ping the server", "{}", |_| Ok(McpContent::text("pong")));
291 /// ```
292 pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> McpServer {
293 McpServer::new(name, version).wrap(self)
294 }
295
296 /// Create an async state-aware application (requires the `http2` feature).
297 ///
298 /// Handlers are `async fn` closures that can `await` database queries,
299 /// HTTP clients, or any other async I/O. Unmatched routes fall through to
300 /// the built-in controller chain.
301 ///
302 /// # Example
303 ///
304 /// ```rust,no_run
305 /// use std::sync::Arc;
306 /// use rust_web_server::app::App;
307 /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
308 /// use rust_web_server::core::New;
309 ///
310 /// struct Db { url: String }
311 ///
312 /// let app = App::with_async_state(Db { url: "postgres://...".to_string() })
313 /// .get("/ping", |_req, _params, _conn, state| async move {
314 /// // state: Arc<Db>
315 /// let mut r = Response::new();
316 /// r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
317 /// r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
318 /// r
319 /// });
320 /// ```
321 #[cfg(feature = "http2")]
322 pub fn with_async_state<S: Send + Sync + 'static>(state: S) -> crate::async_state::AsyncAppWithState<S> {
323 crate::async_state::AsyncAppWithState::new(state)
324 }
325}