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