Skip to main content

ntex/web/
config.rs

1use std::{any::Any, any::TypeId, cell::UnsafeCell, net::SocketAddr, rc::Rc};
2
3use urly::Url;
4
5use crate::service::cfg::{CfgContext, Configuration};
6use crate::{router::ResourceDef, util::ByteString, util::HashMap};
7
8use super::httprequest::{HttpRequest, HttpRequestInner};
9use super::service::{AppServiceFactory, ServiceFactoryWrapper, WebServiceFactory};
10use super::{Resource, Route, State};
11
12/// Application configuration
13#[derive(Debug)]
14pub struct WebAppConfig {
15    name: ByteString,
16    secure: bool,
17    host: String,
18    addr: SocketAddr,
19    config: CfgContext,
20    state: HashMap<TypeId, Box<dyn Any + Sync + Send>>,
21    pub(super) pool_size: usize,
22}
23
24impl Default for WebAppConfig {
25    fn default() -> Self {
26        WebAppConfig::new()
27    }
28}
29
30impl Configuration for WebAppConfig {
31    const NAME: &str = "Web app configuration";
32
33    fn ctx(&self) -> &CfgContext {
34        &self.config
35    }
36
37    fn set_ctx(&mut self, ctx: CfgContext) {
38        self.config = ctx;
39    }
40}
41
42impl WebAppConfig {
43    #[must_use]
44    /// Create an default `WebAppConfig` instance.
45    pub fn new() -> Self {
46        WebAppConfig::with(
47            "ntex:web",
48            false,
49            "127.0.0.1:8080".parse().unwrap(),
50            "localhost:8080".to_owned(),
51        )
52    }
53
54    #[must_use]
55    /// Create an `WebAppConfig` instance.
56    pub fn with(name: &str, secure: bool, addr: SocketAddr, host: String) -> Self {
57        WebAppConfig {
58            secure,
59            host,
60            addr,
61            name: name.into(),
62            pool_size: 128,
63            state: HashMap::default(),
64            config: CfgContext::default(),
65        }
66    }
67
68    /// Server name
69    pub fn name(&self) -> &ByteString {
70        &self.name
71    }
72
73    /// Server host name
74    ///
75    /// Host name is used by application router as a hostname for url generation.
76    /// Check [`ConnectionInfo::host()`](crate::web::dev::ConnectionInfo::host)
77    /// documentation for more information.
78    ///
79    /// By default host name is set to a "localhost:8080" value.
80    pub fn host(&self) -> &str {
81        &self.host
82    }
83
84    /// Returns true if connection is secure(https)
85    pub fn secure(&self) -> bool {
86        self.secure
87    }
88
89    /// Returns the socket address of the local half of this TCP connection
90    pub fn local_addr(&self) -> SocketAddr {
91        self.addr
92    }
93
94    /// Get an application state object stored with `.set_state()` method.
95    pub fn state<T: 'static>(&self) -> Option<&T> {
96        self.state
97            .get(&TypeId::of::<T>())
98            .and_then(|boxed| boxed.downcast_ref())
99    }
100
101    #[must_use]
102    /// Set server host name.
103    ///
104    /// By default host name is set to a "localhost:8080" value.
105    pub fn set_host(mut self, host: String) -> Self {
106        self.host = host;
107        self
108    }
109
110    #[must_use]
111    /// Connection is secure(https).
112    pub fn set_secure(mut self) -> Self {
113        self.secure = true;
114        self
115    }
116
117    #[must_use]
118    /// Set the socket address of the local half of the TCP connection.
119    pub fn set_local_addr(mut self, addr: SocketAddr) -> Self {
120        self.addr = addr;
121        self
122    }
123
124    #[must_use]
125    /// Set size of `HttpRequest` pool size.
126    ///
127    /// By default pool size is 128.
128    pub fn set_pool_size(mut self, size: usize) -> Self {
129        self.pool_size = size;
130        self
131    }
132
133    #[must_use]
134    /// Set application level arbitrary state item.
135    ///
136    /// Application state stored with `.set_state()` method is available
137    /// via `HttpRequest::app_state()` method at runtime.
138    pub fn set_state<T: Send + Sync + 'static>(mut self, val: T) -> Self {
139        self.state
140            .insert(TypeId::of::<T>(), Box::new(val))
141            .and_then(|item| item.downcast::<T>().map(|boxed| *boxed).ok());
142        self
143    }
144
145    /// Get message from the pool.
146    pub(crate) fn get_request(&self) -> Option<HttpRequest> {
147        CACHE.with(|cache| cache.with(self.config.id(), |cache| cache.pop().map(HttpRequest)))
148    }
149}
150
151/// Put message from the pool.
152pub(crate) fn put_request(id: usize, pool_size: usize, req: &mut Rc<HttpRequestInner>) {
153    CACHE.with(|cache| {
154        cache.with(id, |cache| {
155            if cache.len() < pool_size
156                && let Some(inner) = Rc::get_mut(req)
157            {
158                // the head and the path may share the connection read buffer,
159                // a pooled request must not keep it alive
160                inner.head.clear();
161                inner.path.set(Url::new());
162                cache.push(req.clone());
163            }
164        });
165    });
166}
167
168/// Service config is used for external configuration.
169///
170/// Part of application configuration could be offloaded
171/// to set of external methods. This could help with
172/// modularization of big application configuration.
173#[derive(derive_more::Debug)]
174#[debug("ServiceConfig")]
175pub struct ServiceConfig<St, In = ()> {
176    pub(super) services: Vec<Box<dyn AppServiceFactory<St, In>>>,
177    pub(super) external: Vec<ResourceDef>,
178}
179
180impl<St: State, In: 'static> ServiceConfig<St, In> {
181    /// Create a service configuration with the given external resources.
182    pub fn new(external: Vec<ResourceDef>) -> Self {
183        Self {
184            external,
185            services: Vec::new(),
186        }
187    }
188
189    /// Register a route for an application path.
190    ///
191    /// This has the same behavior as [`App::route()`]: it creates a separate
192    /// resource containing the route and promotes the route's method and custom
193    /// guards to resource guards.
194    ///
195    /// [`App::route()`]: super::App::route
196    pub fn route(&mut self, path: &str, mut route: Route<St, In>) -> &mut Self {
197        self.service(
198            Resource::new(path)
199                .add_guards(route.take_guards())
200                .route(route),
201        )
202    }
203
204    /// Registers a web service with the current application or scope.
205    ///
206    /// This has the same routing behavior as [`App::service()`]. The service
207    /// can be a resource, scope, attribute-macro handler, or custom
208    /// [`WebServiceFactory`]. When this configuration is used by a scope, the
209    /// service is registered below that scope's path prefix.
210    ///
211    /// ```rust
212    /// use ntex::web::{self, App, ServiceConfig};
213    ///
214    /// fn configure(cfg: &mut ServiceConfig<()>) {
215    ///     cfg.service(
216    ///         web::resource("/health").to(async || "OK"),
217    ///     );
218    /// }
219    ///
220    /// App::default().configure(configure);
221    /// ```
222    ///
223    /// [`App::service()`]: super::App::service
224    pub fn service<F>(&mut self, factory: F) -> &mut Self
225    where
226        F: WebServiceFactory<St, In> + 'static,
227    {
228        self.services
229            .push(Box::new(ServiceFactoryWrapper::new(factory)));
230        self
231    }
232
233    /// Register an external resource.
234    ///
235    /// External resources are useful for URL generation purposes only
236    /// and are never considered for matching at request time. Calls to
237    /// `HttpRequest::url_for()` will work as expected.
238    ///
239    /// This is same as `App::external_resource()` method.
240    pub fn external_resource(&mut self, name: impl AsRef<str>, url: impl AsRef<str>) -> &mut Self {
241        let mut rdef = ResourceDef::new(url.as_ref());
242        rdef.set_name(name.as_ref());
243        self.external.push(rdef);
244        self
245    }
246}
247
248thread_local! {
249    static CACHE: LocalCache = LocalCache::new();
250}
251
252/// Request's objects pool
253struct LocalCache {
254    cache: UnsafeCell<Vec<Vec<Rc<HttpRequestInner>>>>,
255}
256
257impl LocalCache {
258    fn new() -> Self {
259        Self {
260            cache: UnsafeCell::new(Vec::with_capacity(16)),
261        }
262    }
263
264    fn with<F, R>(&self, idx: usize, f: F) -> R
265    where
266        F: FnOnce(&mut Vec<Rc<HttpRequestInner>>) -> R,
267    {
268        let cache = unsafe { &mut *self.cache.get() };
269
270        while cache.len() <= idx {
271            cache.push(Vec::new());
272        }
273        f(&mut cache[idx])
274    }
275}
276
277#[cfg(test)]
278#[allow(clippy::assert_is_empty)]
279mod tests {
280    use super::*;
281    use crate::http::{Method, StatusCode};
282    use crate::util::Bytes;
283    use crate::web::test::{TestRequest, call_service, init_service, read_body};
284    use crate::web::{self, App, HttpRequest, HttpResponse};
285
286    #[crate::rt_test]
287    async fn test_webappconfig() {
288        let cfg = WebAppConfig::default()
289            .set_host("www.example.org".to_string())
290            .set_local_addr("127.0.0.1:8080".parse().unwrap())
291            .set_pool_size(256);
292        assert_eq!(cfg.host(), "www.example.org");
293        assert_eq!(cfg.local_addr(), "127.0.0.1:8080".parse().unwrap());
294        assert_eq!(cfg.pool_size, 256);
295        assert_eq!(cfg.name(), "ntex:web");
296        assert!(!cfg.secure());
297        assert!(cfg.set_secure().secure());
298    }
299
300    #[crate::rt_test]
301    async fn test_configure_external_resource() {
302        let srv = init_service(
303            App::new()
304                .configure(|cfg| {
305                    cfg.external_resource("youtube", "https://youtube.com/watch/{video_id}");
306                })
307                .route(
308                    "/test",
309                    web::get().to(async move |req: HttpRequest| {
310                        HttpResponse::Ok()
311                            .body(format!("{}", req.url_for("youtube", ["12345"]).unwrap()))
312                    }),
313                ),
314        )
315        .await;
316        let req = TestRequest::with_uri("/test").to_request();
317        let resp = call_service(&srv, req).await;
318        assert_eq!(resp.status(), StatusCode::OK);
319        let body = read_body(resp).await;
320        assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
321    }
322
323    #[crate::rt_test]
324    async fn test_configure_service() {
325        let srv = init_service(App::new().configure(|cfg| {
326            cfg.service(
327                web::resource("/test").route(web::get().to(async || HttpResponse::Created())),
328            )
329            .route("/index.html", web::get().to(async || HttpResponse::Ok()));
330        }))
331        .await;
332
333        let req = TestRequest::with_uri("/test")
334            .method(Method::GET)
335            .to_request();
336        let resp = call_service(&srv, req).await;
337        assert_eq!(resp.status(), StatusCode::CREATED);
338
339        let req = TestRequest::with_uri("/index.html")
340            .method(Method::GET)
341            .to_request();
342        let resp = call_service(&srv, req).await;
343        assert_eq!(resp.status(), StatusCode::OK);
344    }
345
346    #[test]
347    fn test_new_service_config() {
348        let cfg: ServiceConfig<(), ()> = ServiceConfig::new(Vec::new());
349        assert!(cfg.services.is_empty());
350        assert!(cfg.external.is_empty());
351    }
352}