Skip to main content

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