Skip to main content

rumtk_web/utils/
app.rs

1/*
2 * rumtk attempts to implement HL7 and medical protocols for interoperability in medicine.
3 * This toolkit aims to be reliable, simple, performant, and standards compliant.
4 * Copyright (C) 2025  Luis M. Santos, M.D.
5 * Copyright (C) 2025  Nick Stephenson
6 * Copyright (C) 2025  Ethan Dixon
7 * Copyright (C) 2025  MedicalMasses L.L.C.
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 */
23use crate::components::{form::Forms, UserComponents};
24use crate::css::DEFAULT_OUT_CSS_DIR;
25use crate::pages::UserPages;
26use crate::utils::defaults::DEFAULT_LOCAL_LISTENING_ADDRESS;
27use crate::utils::matcher::*;
28use crate::{
29    rumtk_web_api_process, rumtk_web_compile_css_bundle, rumtk_web_init_api_endpoints,
30    rumtk_web_init_components, rumtk_web_init_forms, rumtk_web_init_job_manager,
31    rumtk_web_init_pages,
32};
33use crate::{rumtk_web_fetch, rumtk_web_load_conf};
34
35use rumtk_core::core::RUMResult;
36use rumtk_core::dependencies::clap;
37use rumtk_core::strings::RUMString;
38use rumtk_core::threading::threading_functions::get_default_system_thread_count;
39use rumtk_core::types::{RUMCLIParser, RUMTcpListener};
40use rumtk_core::{rumtk_init_threads, rumtk_resolve_task};
41
42use crate::api::UserAPIEndpoints;
43use axum::routing::{get, post};
44use axum::Router;
45use tower_http::compression::{CompressionLayer, DefaultPredicate};
46use tower_http::services::ServeDir;
47
48const DEFAULT_UPLOAD_LIMIT: usize = 10240;
49
50///
51/// RUMTK WebApp CLI Args
52///
53#[derive(RUMCLIParser, Debug)]
54#[command(author, version, about, long_about = None)]
55struct Args {
56    ///
57    /// Website title to use internally. It can be omitted if defined in the app.json config file
58    /// bundled with your app.
59    ///
60    #[arg(long, default_value = "")]
61    pub title: RUMString,
62    ///
63    /// Website description string. It can be omitted if defined in the app.json config file
64    /// bundled with your app.
65    ///
66    #[arg(long, default_value = "")]
67    pub description: RUMString,
68    ///
69    /// Company to display in website.
70    ///
71    #[arg(long, default_value = "")]
72    pub company: RUMString,
73    ///
74    /// Copyright year to display in website.
75    ///
76    #[arg(short, long, default_value = "")]
77    pub copyright: RUMString,
78    ///
79    /// Directory to scan on startup to find custom CSS sources to bundle into a minified CSS file
80    /// that can be quickly pulled by the app client side.
81    ///
82    /// This option can provide an alternative to direct component retrieval of CSS fragments.
83    /// Meaning, you could bundle all of your fragments into the master bundle at startup and
84    /// turn off component level ```custom_css_enabled``` option in the ```app.json``` config.
85    ///
86    #[arg(long, default_value = DEFAULT_OUT_CSS_DIR)]
87    pub css_source_dir: RUMString,
88    ///
89    /// Is the interface meant to be bound to the loopback address and remain hidden from the
90    /// outside world.
91    ///
92    /// It follows the format ```IPv4:port``` and it is a string.
93    ///
94    /// If a NIC IP is defined via `--ip`, that value will override this flag.
95    ///
96    #[arg(short, long, default_value = DEFAULT_LOCAL_LISTENING_ADDRESS)]
97    pub ip: RUMString,
98    ///
99    /// Specify the size limit for a file upload post request.
100    ///
101    #[arg(long, default_value_t = DEFAULT_UPLOAD_LIMIT)]
102    pub upload_limit: usize,
103    ///
104    /// How many threads to use to serve the website. By default, we use
105    /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
106    /// cpus available. We use the system's total count of cpus by default.
107    ///
108    #[arg(long, default_value_t = get_default_system_thread_count())]
109    pub threads: usize,
110    ///
111    /// How many threads to use to serve the website. By default, we use
112    /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
113    /// cpus available. We use the system's total count of cpus by default.
114    ///
115    #[arg(long, default_value_t = false)]
116    pub skip_default_css: bool,
117}
118
119async fn run_app(args: &Args, skip_serve: bool) -> RUMResult<()> {
120    let state = rumtk_web_load_conf!(&args);
121    let comression_layer: CompressionLayer = CompressionLayer::new()
122        .br(true)
123        .deflate(true)
124        .gzip(true)
125        .zstd(true)
126        .compress_when(DefaultPredicate::new());
127    let app = Router::new()
128        /* Robots.txt */
129        .route("/robots.txt", get(rumtk_web_fetch!(default_robots_matcher)))
130        /* Components */
131        .route(
132            "/component/{*name}",
133            get(rumtk_web_fetch!(default_component_matcher)),
134        )
135        /* Pages */
136        .route("/", get(rumtk_web_fetch!(default_page_matcher)))
137        .route("/{*page}", get(rumtk_web_fetch!(default_page_matcher)))
138        /* Post Handling */
139        .route("/api/", post(rumtk_web_api_process!(default_api_matcher)))
140        //.layer(DefaultBodyLimit::max(args.upload_limit))
141        .route(
142            "/api/{*page}",
143            post(rumtk_web_api_process!(default_api_matcher)),
144        )
145        //.layer(DefaultBodyLimit::max(args.upload_limit))
146        /* Services */
147        .nest_service("/static", ServeDir::new("static"))
148        .with_state(state)
149        .layer(comression_layer);
150
151    let listener = RUMTcpListener::bind(&args.ip.as_str())
152        .await
153        .expect("There was an issue biding the listener.");
154    println!("listening on {}", listener.local_addr().unwrap());
155
156    if !skip_serve {
157        axum::serve(listener, app)
158            .await
159            .expect("There was an issue with the server.");
160    }
161
162    Ok(())
163}
164
165pub fn app_main(
166    pages: &UserPages,
167    components: &UserComponents,
168    forms: &Forms,
169    apis: &UserAPIEndpoints,
170    skip_serve: bool,
171    skip_default_css: bool,
172) {
173    let args = Args::parse();
174
175    rumtk_web_init_components!(components);
176    rumtk_web_init_pages!(pages);
177    rumtk_web_init_forms!(forms);
178    rumtk_web_init_api_endpoints!(apis);
179    rumtk_web_compile_css_bundle!(
180        &args.css_source_dir,
181        &args.skip_default_css | skip_default_css
182    );
183
184    let rt = rumtk_init_threads!(&args.threads);
185    rumtk_web_init_job_manager!(&args.threads);
186    let task = run_app(&args, skip_serve);
187    rumtk_resolve_task!(rt, task);
188}
189
190///
191/// This is the main macro for defining your applet and launching it.
192/// Usage is very simple and the only decision from a user is whether to pass a list of
193/// [UserPages](crate::pages::UserPages) or a list of [UserPages](crate::pages::UserPages) and a list
194/// of [UserComponents](crate::components::UserComponents).
195///
196/// These lists are used to automatically register your pages
197/// (e.g. `/index => ('index', my_index_function)`) and your custom components
198/// (e.g. `button => ('button', my_button_function)`
199///
200/// This macro will load CSS from predefined sources, concatenate their contents with predefined CSS,
201/// minified the concatenated results, and generate a bundle css file containing the minified results.
202/// The CSS bundle is written to file `./static/css/bundle.min.css`.
203///
204/// ***Note: anything in ./static will be considered static assets that need to be served.***
205///
206/// This macro will also parse the command line automatically with a few predefined options and
207/// use that information to override the config defaults.
208///
209/// By default, the app is launched to `127.0.0.1:3000` which is the loopback address.
210///
211/// App is served with the best compression algorithm allowed by the client browser.
212///
213/// For testing purposes, the function
214///
215/// ## Example Usage
216///
217/// ### With Page and Component definition
218/// ```
219///     use rumtk_core::strings::{rumtk_format};
220///     use rumtk_web::{
221///         rumtk_web_run_app,
222///         rumtk_web_render_component,
223///         rumtk_web_render_html,
224///         rumtk_web_get_text_item
225///     };
226///     use rumtk_web::components::form::{FormElementBuilder, props::InputProps, FormElements};
227///     use rumtk_web::{SharedAppState, RenderedPageComponents};
228///     use rumtk_web::{APIPath, URLPath, URLParams, HTMLResult, RUMString, RouterForm, FormData, RUMWebData};
229///     use rumtk_web::defaults::{DEFAULT_TEXT_ITEM, PARAMS_CONTENTS, PARAMS_CSS_CLASS};
230///     use rumtk_web::utils::types::RUMWebTemplate;
231///
232///
233///
234///
235///     // About page
236///     pub fn about(app_state: SharedAppState) -> RenderedPageComponents {
237///         let title_coop = rumtk_web_render_component!("title", [("type", "coop_values")], app_state.clone());
238///         let title_team = rumtk_web_render_component!("title", [("type", "meet_the_team")], app_state.clone());
239///     
240///         let text_card_story = rumtk_web_render_component!("text_card", [("type", "story")], app_state.clone());
241///         let text_card_coop = rumtk_web_render_component!("text_card", [("type", "coop_values")], app_state.clone());
242///     
243///         let portrait_card = rumtk_web_render_component!("portrait_card", [("section", "company"), ("type", "personnel")], app_state.clone());
244///     
245///         let spacer_5 = rumtk_web_render_component!("spacer", [("size", "5")], app_state.clone());
246///     
247///         vec![
248///             text_card_story,
249///             spacer_5.clone(),
250///             title_coop,
251///             text_card_coop,
252///             spacer_5,
253///             title_team,
254///             portrait_card
255///         ]
256///     }
257///
258///     //Custom component
259///     #[derive(RUMWebTemplate, Debug)]
260///     #[template(
261///             source = "
262///                <style>
263///
264///                </style>
265///                {% if custom_css_enabled %}
266///                    <link href='/static/components/div.css' rel='stylesheet'>
267///                {% endif %}
268///                <div class='div-{{css_class}}'>{{contents|safe}}</div>
269///            ",
270///            ext = "html"
271///     )]
272///     struct MyDiv {
273///         contents: RUMString,
274///         css_class: RUMString,
275///         custom_css_enabled: bool,
276///     }
277///
278///     fn my_div(path_components: URLPath, params: URLParams, state: SharedAppState) -> HTMLResult {
279///         let contents = rumtk_web_get_text_item!(params, PARAMS_CONTENTS, DEFAULT_TEXT_ITEM);
280///         let css_class = rumtk_web_get_text_item!(params, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM);
281///
282///         let custom_css_enabled = state.read().expect("Lock failure").get_config().custom_css;
283///
284///         rumtk_web_render_html!(MyDiv {
285///             contents: RUMString::from(contents),
286///             css_class: RUMString::from(css_class),
287///             custom_css_enabled
288///         })
289///     }
290///
291///     fn my_form (builder: FormElementBuilder) -> FormElements {
292///         vec![
293///             builder("input", "", InputProps::default(), "default")
294///         ]
295///     }
296///
297///     fn my_api_handler(path: APIPath, params: RUMWebData, form: FormData, state: SharedAppState) -> HTMLResult {
298///         Err(rumtk_format!(
299///             "No handler registered for API endpoint => {}",
300///             path
301///         ))
302///     }
303///
304///     //Requesting to immediately exit instead of indefinitely serving pages so this example can be used as a unit test.
305///     let skip_serve = true;
306///     let skip_default_css = false;
307///
308///     let result = rumtk_web_run_app!(
309///         vec![("about", about)],
310///         vec![("my_div", my_div)], //Optional, can be omitted alongside the skip_serve flag
311///         vec![("my_form", my_form)], //Optional, can be omitted alongside the skip_serve flag
312///         vec![("v2/add", my_api_handler)], //Optional, can be omitted alongside the skip_serve flag
313///         skip_serve, //Omit in production code. This is used so that this example can work as a unit test.
314///         skip_default_css //Omit in production code. This is used so that this example can work as a unit test.
315///     );
316/// ```
317///
318#[macro_export]
319macro_rules! rumtk_web_run_app {
320    (  ) => {{
321        use $crate::utils::app::app_main;
322
323        app_main(&vec![], &vec![], &vec![], &vec![], false, false)
324    }};
325    ( $pages:expr ) => {{
326        use $crate::utils::app::app_main;
327
328        app_main(&$pages, &vec![], &vec![], &vec![], false, false)
329    }};
330    ( $pages:expr, $components:expr ) => {{
331        use $crate::utils::app::app_main;
332
333        app_main(&$pages, &$components, &vec![], &vec![], false, false)
334    }};
335    ( $pages:expr, $components:expr, $forms:expr ) => {{
336        use $crate::utils::app::app_main;
337
338        app_main(&$pages, &$components, &$forms, &vec![], false, false)
339    }};
340    ( $pages:expr, $components:expr, $forms:expr, $apis:expr ) => {{
341        use $crate::utils::app::app_main;
342
343        app_main(&$pages, &$components, &$forms, &$apis, false, false)
344    }};
345    ( $pages:expr, $components:expr, $forms:expr, $apis:expr, $skip_serve:expr, $skip_default_css:expr ) => {{
346        use $crate::utils::app::app_main;
347
348        app_main(
349            &$pages,
350            &$components,
351            &$forms,
352            &$apis,
353            $skip_serve,
354            $skip_default_css,
355        )
356    }};
357}