Skip to main content

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::mcp::McpServer;
24use crate::middleware::{Middleware, WithMiddleware};
25use crate::request::Request;
26use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
27use crate::server::ConnectionInfo;
28use crate::state::AppWithState;
29
30/// A pair of function pointers representing one entry in the controller chain.
31struct ControllerEntry {
32    is_matching: fn(&Request, &ConnectionInfo) -> bool,
33    process: fn(&Request, Response, &ConnectionInfo) -> Response,
34}
35
36/// Build a [`ControllerEntry`] from any type that implements [`Controller`].
37fn entry<C: Controller>() -> ControllerEntry {
38    ControllerEntry {
39        is_matching: C::is_matching,
40        process: C::process,
41    }
42}
43
44/// The built-in HTTP application. Serves static files, favicons, forms,
45/// file uploads, health probes, metrics, and a 404 fallback.
46///
47/// Use as-is or compose with the framework's building blocks:
48///
49/// ```rust,no_run
50/// use rust_web_server::app::App;
51/// use rust_web_server::middleware::{WithMiddleware, RateLimitLayer};
52/// use rust_web_server::core::New;
53///
54/// // Middleware stack around the built-in app
55/// let app = App::new().wrap(RateLimitLayer);
56/// ```
57///
58/// For user-defined routes with shared state, call [`App::with_state`]:
59///
60/// ```rust,no_run
61/// use rust_web_server::app::App;
62/// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
63/// use rust_web_server::core::New;
64///
65/// struct State { version: &'static str }
66///
67/// let app = App::with_state(State { version: "1.0" })
68///     .get("/version", |_req, _params, _conn, state| {
69///         let mut r = Response::new();
70///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
71///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
72///         r
73///     });
74/// ```
75#[derive(Copy, Clone)]
76pub struct App {}
77
78impl New for App {
79    fn new() -> Self {
80        App{}
81    }
82}
83
84impl Application for App {
85    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
86        let header_list = Header::get_header_list(request);
87        let response = Response::get_response(
88            STATUS_CODE_REASON_PHRASE.n501_not_implemented,
89            Some(header_list),
90            None,
91        );
92
93        let controllers = [
94            entry::<IndexController>(),
95            entry::<StyleController>(),
96            entry::<ScriptController>(),
97            entry::<FileUploadInitiateController>(),
98            entry::<FormUrlEncodedEnctypePostMethodController>(),
99            entry::<FormGetMethodController>(),
100            entry::<FormMultipartEnctypePostMethodController>(),
101            entry::<HealthController>(),
102            entry::<ReadyController>(),
103            entry::<MetricsController>(),
104            entry::<FaviconController>(),
105            entry::<StaticResourceController>(),
106            entry::<NotFoundController>(),
107        ];
108
109        for c in &controllers {
110            if (c.is_matching)(request, connection) {
111                return Ok((c.process)(request, response, connection));
112            }
113        }
114
115        Ok(response)
116    }
117}
118
119impl App {
120    /// Dispatch `request` through the controller chain and return the response.
121    ///
122    /// This is a convenience wrapper over [`Application::execute`] that uses a
123    /// synthetic loopback [`ConnectionInfo`]. Use it in tests or when no real
124    /// connection context is available. Prefer [`TestClient`] for structured
125    /// test code.
126    ///
127    /// [`TestClient`]: crate::test_client::TestClient
128    pub fn handle_request(request: Request) -> (Response, Request) {
129        use crate::server::Address;
130        let conn = ConnectionInfo {
131            client: Address { ip: "127.0.0.1".to_string(), port: 0 },
132            server: Address { ip: "127.0.0.1".to_string(), port: 7878 },
133            request_size: 16000,
134            sni_hostname: None,
135        };
136        let app = App::new();
137        let response = app.execute(&request, &conn).unwrap_or_else(|_| {
138            let header_list = Header::get_header_list(&request);
139            Response::get_response(
140                STATUS_CODE_REASON_PHRASE.n500_internal_server_error,
141                Some(header_list),
142                None,
143            )
144        });
145        (response, request)
146    }
147
148    /// Create a state-aware application. Routes registered on the returned
149    /// [`AppWithState<S>`] are tried first; unmatched requests fall through to
150    /// the built-in controller chain (static files, health probes, etc.).
151    ///
152    /// The state is stored as `Arc<S>` and shared across all handlers.
153    ///
154    /// # Example
155    ///
156    /// ```rust,no_run
157    /// use rust_web_server::app::App;
158    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
159    /// use rust_web_server::core::New;
160    ///
161    /// struct Db { url: String }
162    ///
163    /// let app = App::with_state(Db { url: "postgres://...".to_string() })
164    ///     .get("/ping", |_req, _params, _conn, db| {
165    ///         let mut r = Response::new();
166    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
167    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
168    ///         r
169    ///     });
170    /// ```
171    pub fn with_state<S: Send + Sync + 'static>(state: S) -> AppWithState<S> {
172        AppWithState::new(state)
173    }
174
175    /// Wrap this application in a middleware layer.
176    ///
177    /// Returns a [`WithMiddleware<App>`] that runs `layer` before every
178    /// request. Chain `.wrap()` calls to stack multiple layers:
179    ///
180    /// ```rust,no_run
181    /// use rust_web_server::app::App;
182    /// use rust_web_server::middleware::RateLimitLayer;
183    /// use rust_web_server::core::New;
184    ///
185    /// let app = App::new().wrap(RateLimitLayer);
186    /// ```
187    pub fn wrap<M: Middleware + 'static>(self, layer: M) -> WithMiddleware<App> {
188        WithMiddleware::new(self).wrap(layer)
189    }
190
191    /// Attach an MCP server to this application. Tools, resources, and
192    /// prompts are registered on the returned [`McpServer`]; requests that
193    /// do not match the MCP endpoint are forwarded to `self` (static files,
194    /// health probes, any custom routes registered before this call).
195    ///
196    /// ```rust,no_run
197    /// use rust_web_server::app::App;
198    /// use rust_web_server::mcp::{McpContent, extract_arg};
199    /// use rust_web_server::core::New;
200    ///
201    /// // Pure MCP — unmatched paths handled by built-in App
202    /// let app = App::new()
203    ///     .mcp("my-server", "1.0")
204    ///     .tool(
205    ///         "echo",
206    ///         "Echo text back",
207    ///         r#"{"type":"object","properties":{"text":{"type":"string"}}}"#,
208    ///         |args| Ok(McpContent::text(extract_arg(args, "text").unwrap_or_default())),
209    ///     );
210    /// ```
211    ///
212    /// To combine with custom HTTP routes, start from [`App::with_state`]:
213    ///
214    /// ```rust,no_run
215    /// use rust_web_server::app::App;
216    /// use rust_web_server::mcp::McpContent;
217    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
218    /// use rust_web_server::core::New;
219    ///
220    /// let app = App::with_state(())
221    ///     .get("/api/ping", |_, _, _, _| {
222    ///         let mut r = Response::new();
223    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
224    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
225    ///         r
226    ///     })
227    ///     .mcp("my-server", "1.0")
228    ///     .tool("ping", "Ping the server", "{}", |_| Ok(McpContent::text("pong")));
229    /// ```
230    pub fn mcp(self, name: impl Into<String>, version: impl Into<String>) -> McpServer {
231        McpServer::new(name, version).wrap(self)
232    }
233
234    /// Create an async state-aware application (requires the `http2` feature).
235    ///
236    /// Handlers are `async fn` closures that can `await` database queries,
237    /// HTTP clients, or any other async I/O. Unmatched routes fall through to
238    /// the built-in controller chain.
239    ///
240    /// # Example
241    ///
242    /// ```rust,no_run
243    /// use std::sync::Arc;
244    /// use rust_web_server::app::App;
245    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
246    /// use rust_web_server::core::New;
247    ///
248    /// struct Db { url: String }
249    ///
250    /// let app = App::with_async_state(Db { url: "postgres://...".to_string() })
251    ///     .get("/ping", |_req, _params, _conn, state| async move {
252    ///         // state: Arc<Db>
253    ///         let mut r = Response::new();
254    ///         r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
255    ///         r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
256    ///         r
257    ///     });
258    /// ```
259    #[cfg(feature = "http2")]
260    pub fn with_async_state<S: Send + Sync + 'static>(state: S) -> crate::async_state::AsyncAppWithState<S> {
261        crate::async_state::AsyncAppWithState::new(state)
262    }
263}