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. <lsantos@medicalmasses.com>
5 * Copyright (C) 2025 Ethan Dixon
6 * Copyright (C) 2025 MedicalMasses L.L.C. <contact@medicalmasses.com>
7 *
8 * This program is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21use crate::components::form::Forms;
22use crate::css::DEFAULT_OUT_CSS_DIR;
23use crate::pages::UserPages;
24use crate::utils::defaults::DEFAULT_LOCAL_LISTENING_ADDRESS;
25use crate::utils::matcher::*;
26use crate::{rumtk_web_api_process, rumtk_web_compile_css_bundle, rumtk_web_get_config, rumtk_web_init_api_endpoints, rumtk_web_init_forms, rumtk_web_init_job_manager, rumtk_web_init_pages, SharedAppState};
27use crate::{rumtk_web_fetch, rumtk_web_load_conf};
28use std::random;
29
30use rumtk_core::base::RUMResult;
31use rumtk_core::dependencies::clap;
32use rumtk_core::rumtk_resolve_task;
33use rumtk_core::strings::{rumtk_format, RUMString};
34use rumtk_core::threading::threading_functions::get_default_system_thread_count;
35use rumtk_core::types::{RUMCLIParser, RUMTcpListener};
36
37use crate::api::UserAPIEndpoints;
38use axum::routing::{get, post};
39use axum::Router;
40use tower_http::compression::{CompressionLayer, DefaultPredicate};
41use tower_http::services::ServeDir;
42
43const DEFAULT_UPLOAD_LIMIT: usize = 10240;
44
45///
46/// RUMTK WebApp CLI Args
47///
48#[derive(RUMCLIParser, Default, Debug)]
49#[command(author, version, about, long_about = None)]
50struct Args {
51 ///
52 /// Website title to use internally. It can be omitted if defined in the app.json config file
53 /// bundled with your app.
54 ///
55 #[arg(long, default_value = "")]
56 pub title: RUMString,
57 ///
58 /// Website description string. 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 description: RUMString,
63 ///
64 /// Company to display in website.
65 ///
66 #[arg(long, default_value = "")]
67 pub company: RUMString,
68 ///
69 /// Copyright year to display in website.
70 ///
71 #[arg(short, long, default_value = "")]
72 pub copyright: RUMString,
73 ///
74 /// Directory to scan on startup to find custom CSS sources to bundle into a minified CSS file
75 /// that can be quickly pulled by the app client side.
76 ///
77 /// This option can provide an alternative to direct component retrieval of CSS fragments.
78 /// Meaning, you could bundle all of your fragments into the master bundle at startup and
79 /// turn off component level ```custom_css_enabled``` option in the ```app.json``` config.
80 ///
81 #[arg(long, default_value = DEFAULT_OUT_CSS_DIR)]
82 pub css_source_dir: RUMString,
83 ///
84 /// Is the interface meant to be bound to the loopback address and remain hidden from the
85 /// outside world.
86 ///
87 /// It follows the format ```IPv4:port``` and it is a string.
88 ///
89 /// If a NIC IP is defined via `--ip`, that value will override this flag.
90 ///
91 #[arg(short, long, default_value = DEFAULT_LOCAL_LISTENING_ADDRESS)]
92 pub ip: RUMString,
93 ///
94 /// Specify the size limit for a file upload post request.
95 ///
96 #[arg(long, default_value_t = DEFAULT_UPLOAD_LIMIT)]
97 pub upload_limit: usize,
98 ///
99 /// How many threads to use to serve the website. By default, we use
100 /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
101 /// cpus available. We use the system's total count of cpus by default.
102 ///
103 #[arg(long, default_value_t = get_default_system_thread_count())]
104 pub threads: usize,
105 ///
106 /// How many threads to use to serve the website. By default, we use
107 /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
108 /// cpus available. We use the system's total count of cpus by default.
109 ///
110 #[arg(long, default_value_t = false)]
111 pub skip_default_css: bool,
112}
113
114async fn run_app(args: Args, state: SharedAppState, skip_serve: bool) -> RUMResult<()> {
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 /* Jobs Queue */
125 .route("/jobs/", get(rumtk_web_fetch!(default_job_matcher)))
126 .route("/jobs/{*page}", get(rumtk_web_fetch!(default_job_matcher)))
127 /* Pages */
128 .route("/", get(rumtk_web_fetch!(default_page_matcher)))
129 .route("/{*page}", get(rumtk_web_fetch!(default_page_matcher)))
130 .layer(rumtk_web_get_config!(state).cors.clone().unwrap_or_default().build_cors_layer())
131 /* Post Handling */
132 .route("/api/", post(rumtk_web_api_process!(default_api_matcher)))
133 //.layer(DefaultBodyLimit::max(args.upload_limit))
134 .route(
135 "/api/{*page}",
136 post(rumtk_web_api_process!(default_api_matcher)),
137 )
138 //.layer(DefaultBodyLimit::max(args.upload_limit))
139 /* Services */
140 .nest_service("/static", ServeDir::new("static"))
141 .with_state(state)
142 .layer(comression_layer);
143
144 println!("binding IP {}", &args.ip.as_str());
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
159///
160/// Struct encapsulating custom-made items to register with the framework.
161///
162/// ## Pages
163/// The `pages` field accepts an optional of [UserPages] which is a vector of [PageItem](crate::pages::PageItem).
164///
165/// ```text
166/// vec![
167/// ("my_page", my_page_function),
168/// ...
169/// ];
170/// ```
171///
172/// The page function is of type [PageFunction](crate::utils::types::PageFunction).
173///
174/// It is important to understand that a `page` is a function that simply list the series of components to be rendered.
175/// We rely on `CSS` for the actual layout in 2D space. Therefore, a page function should not prescribe the page layout per se.
176///
177/// ## Components
178/// The `components` field takes an optional of [UserComponents] which is a vector of [UserComponentItem](crate::components::UserComponentItem).
179///
180/// ```text
181/// vec![
182/// ("my_component", my_component_function),
183/// ...
184/// ];
185/// ```
186///
187/// The component function is of type [PageFunction](crate::utils::types::ComponentFunction).
188///
189/// ## Forms
190/// The `forms` field takes an optional of [Forms] which is a vector of [FormItem](crate::components::form::FormItem).
191///
192/// ```text
193/// vec![
194/// ("my_form", my_form_function),
195/// ...
196/// ];
197/// ```
198///
199/// The form function is of type [FormBuilderFunction](crate::components::form::FormBuilderFunction).
200///
201/// Although a `form` is treated as a type of component in the framework, its implementation and behavior is closer to
202/// a `page` in that its main role is to define the vector of [FormElementBuilder](crate::components::form::FormElementBuilder).
203/// These element builder functions further renders the actual form component to be inserted in linear order. Again, these functions
204/// say nothing about the layout of the form as that is handled via `CSS`.
205///
206/// ## APIs
207/// The `apis` field takes an optional of [UserAPIEndpoints] which is a vector of [APIItem](crate::api::APIItem).
208///
209/// ```text
210/// vec![
211/// ("/my/api/endpoint", my_api_handler),
212/// ...
213/// ];
214/// ```
215///
216/// The api function is of type [APIFunction](crate::utils::types::APIFunction).
217///
218/// These API functions are your handlers directly mapped to the REST route you wish to intercept. This enables a
219/// simple key-value pair approach to defining API endpoints in your web app. These handlers can queue asynchronous
220/// pipeline jobs, return HTML fragments, redirect the current page somewhere else, or a combination of these.
221/// It is a powerful interface for organizing your routing.
222///
223#[derive(Default, Debug, PartialEq)]
224pub struct AppComponents<'a> {
225 pub pages: Option<UserPages<'a>>,
226 pub forms: Option<Forms<'a>>,
227 pub apis: Option<UserAPIEndpoints<'a>>,
228}
229
230///
231/// Struct for defining the global switches to drive the initialization of the web app. This struct
232/// works hand in hand with [AppComponents].
233///
234#[derive(Default, Debug, PartialEq)]
235pub struct AppSwitches {
236 pub skip_serve: bool,
237 pub skip_default_css: bool,
238 pub skip_cli_args: bool,
239}
240
241impl AppSwitches {
242 pub fn from_slice(switches: &[bool]) -> Self {
243 match switches.len() {
244 0 => AppSwitches::default(),
245 1 => AppSwitches {
246 skip_serve: switches[0],
247 ..Default::default()
248 },
249 2 => AppSwitches {
250 skip_serve: switches[0],
251 skip_default_css: switches[1],
252 ..Default::default()
253 },
254 _ => AppSwitches {
255 skip_serve: switches[0],
256 skip_default_css: switches[1],
257 skip_cli_args: switches[2],
258 },
259 }
260 }
261}
262
263impl From<&[bool]> for AppSwitches {
264 fn from(switches: &[bool]) -> Self {
265 AppSwitches::from_slice(switches)
266 }
267}
268
269impl<const N: usize> From<[bool; N]> for AppSwitches {
270 fn from(switches: [bool; N]) -> Self {
271 AppSwitches::from_slice(&switches)
272 }
273}
274
275///
276/// Main API function for running and serving the web application.
277///
278/// It takes an [AppComponents] instance and a few switches to help preconfigure the framework to
279/// use custom-made components and to register API endpoints.
280///
281/// See [rumtk_web_run_app](crate::rumtk_web_run_app) for more details.
282///
283/// ## Example
284/// ```
285/// use rumtk_web::app_main;
286/// use rumtk_web::{rumtk_web_register_app_switches, rumtk_web_register_app_components};
287///
288/// // We pass true to the switches because we do not want the web server to actually serve the page
289/// // It would hang the test otherwise...
290/// app_main(
291/// rumtk_web_register_app_components!(),
292/// rumtk_web_register_app_switches!(true)
293/// ).expect("Issue occurred while running the app");
294/// ```
295///
296pub fn app_main(app_components: AppComponents<'_>, switches: AppSwitches, port: Option<u16>) -> RUMResult<()> {
297 let args = match switches.skip_cli_args {
298 true => {
299 let port = port.unwrap_or_else(|| random::random::<u16>(..));
300 let ip = rumtk_format!("127.0.0.1:{port}");
301 Args {
302 ip,
303 ..Default::default()
304 }
305 },
306 false => Args::parse()
307 };
308 let state = rumtk_web_load_conf!(&args);
309
310 rumtk_web_init_pages!(app_components.pages);
311 rumtk_web_init_forms!(app_components.forms, state);
312 rumtk_web_init_api_endpoints!(app_components.apis);
313 rumtk_web_compile_css_bundle!(
314 &args.css_source_dir,
315 &args.skip_default_css | switches.skip_default_css
316 );
317
318 rumtk_web_init_job_manager!(&args.threads);
319 let task = run_app(args, state, switches.skip_serve);
320 rumtk_resolve_task!(task)
321}
322
323///
324/// Convenience macro for quickly building the [AppComponents] object. Feel free to pass an instance of
325/// [AppComponents] directly to [run_app] or [rumtk_web_run_app](crate::rumtk_web_run_app).
326///
327/// Passing no parameters generates an "empty" instance, meaning you would be asking the framework that you only
328/// care about built-in components. This also implies you do not want to process API endpoints.
329///
330/// ## Examples
331///
332/// ### Without Parameters
333/// ```
334/// use crate::rumtk_web::AppComponents;
335/// use crate::rumtk_web::rumtk_web_register_app_components;
336///
337/// let expected = AppComponents::default();
338/// let result = rumtk_web_register_app_components!();
339///
340/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
341///
342/// ```
343///
344/// ### With Existing Page
345/// ```
346/// use rumtk_web::pages::UserPages;
347/// use crate::rumtk_web::AppComponents;
348/// use crate::rumtk_web::pages::index::index;
349/// use crate::rumtk_web::rumtk_web_register_app_components;
350///
351/// let my_pages: UserPages = vec![
352/// ("myindex", index)
353/// ];
354/// let expected = AppComponents {
355/// pages: Some(my_pages.clone()),
356/// forms: None,
357/// apis: None,
358/// };
359/// let result = rumtk_web_register_app_components!(my_pages);
360///
361/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
362///
363/// ```
364///
365/// ### With Existing Page and Component and Form
366/// ```
367/// use rumtk_core::strings::RUMString;
368/// use rumtk_web::{AppComponents, SharedAppState};
369/// use rumtk_web::pages::UserPages;
370/// use rumtk_web::components::form::{FormElementBuilder, FormElements, Forms};
371/// use rumtk_web::pages::index::index;
372/// use rumtk_web::components::form::props::InputProps;
373/// use rumtk_web::rumtk_web_register_app_components;
374///
375/// fn upload_form(builder: FormElementBuilder, _state: &SharedAppState) -> FormElements {
376/// vec![
377/// builder(
378/// "input",
379/// "",
380/// InputProps {
381/// id: Some("file"),
382/// name: Some("file"),
383/// for_element: None,
384/// typ: Some("file"),
385/// value: None,
386/// max: None,
387/// placeholder: Some("path/to/file"),
388/// pattern: None,
389/// accept: Some(".pdf,application/pdf"),
390/// alt: None,
391/// aria_label: Some("PDF File Picker"),
392/// event_handlers: None,
393/// max_length: None,
394/// min_length: None,
395/// autocapitalize: false,
396/// autocomplete: false,
397/// autocorrect: false,
398/// autofocus: false,
399/// disabled: false,
400/// hidden: false,
401/// required: true,
402/// multiple: false,
403/// },
404/// ""
405/// ),
406/// builder(
407/// "input",
408/// "",
409/// InputProps {
410/// id: Some("submit"),
411/// name: None,
412/// for_element: None,
413/// typ: Some("submit"),
414/// value: Some("Send"),
415/// max: None,
416/// placeholder: None,
417/// pattern: None,
418/// accept: None,
419/// alt: None,
420/// aria_label: Some("PDF File Submit Button"),
421/// event_handlers: None,
422/// max_length: None,
423/// min_length: None,
424/// autocapitalize: false,
425/// autocomplete: false,
426/// autocorrect: false,
427/// autofocus: false,
428/// disabled: false,
429/// hidden: false,
430/// required: false,
431/// multiple: false,
432/// },
433/// "f18"
434/// ),
435/// builder(
436/// "progress",
437/// "",
438/// InputProps {
439/// id: Some("progress"),
440/// name: None,
441/// for_element: None,
442/// typ: None,
443/// value: Some("0"),
444/// max: Some("100"),
445/// placeholder: None,
446/// pattern: None,
447/// accept: None,
448/// alt: None,
449/// aria_label: Some("PDF File Submit Progress Bar"),
450/// event_handlers: None,
451/// max_length: None,
452/// min_length: None,
453/// autocapitalize: false,
454/// autocomplete: false,
455/// autocorrect: false,
456/// autofocus: false,
457/// disabled: false,
458/// hidden: true,
459/// required: false,
460/// multiple: false,
461/// },
462/// ""
463/// ),
464/// ]
465/// }
466///
467/// let my_pages: UserPages = vec![
468/// ("myindex", index)
469/// ];
470/// let my_forms: Forms = vec![
471/// ("myform", upload_form)
472/// ];
473/// let expected = AppComponents {
474/// pages: Some(my_pages.clone()),
475/// forms: Some(my_forms.clone()),
476/// apis: None,
477/// };
478/// let result = rumtk_web_register_app_components!(my_pages, my_forms);
479///
480/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
481///
482/// ```
483///
484/// ### With Existing Page and Component and Form and API Endpoint
485/// ```
486/// use rumtk_core::{rumtk_pipeline_run_async, rumtk_pipeline_command};
487/// use rumtk_core::strings::{RUMString, RUMStringConversions, RUMArrayConversions};
488/// use rumtk_web::{rumtk_web_params_map, rumtk_web_post_process_html, APIPath, AppComponents, FormData, HTMLResult, RUMWebData, RUMWebResponse, SharedAppState};
489/// use rumtk_web::{rumtk_web_get_job_manager, rumtk_web_render_component, rumtk_web_render_page_contents};
490/// use rumtk_web::api::UserAPIEndpoints;
491/// use rumtk_web::pages::UserPages;
492/// use rumtk_web::components::form::{FormElementBuilder, FormElements, Forms};
493/// use rumtk_web::pages::index::index;
494/// use rumtk_web::components::form::props::InputProps;
495/// use rumtk_web::components::job_loader::{job_loader, JobLoader};
496/// use rumtk_web::jobs::{JobResult};
497/// use rumtk_web::utils::defaults::{PARAMS_TARGET, PARAMS_ID};
498/// use rumtk_web::ComponentResult;
499/// use rumtk_web::components::sanitize::sanitized;
500/// use rumtk_web::rumtk_web_register_app_components;
501///
502/// async fn upload_processor(form: FormData) -> JobResult {
503/// let id = form.form.get("file").unwrap();
504/// let file = form.files.get(id).unwrap();
505///
506/// let result = rumtk_pipeline_run_async!(
507/// &vec![
508/// rumtk_pipeline_command!("cat"),
509/// rumtk_pipeline_command!("wc")
510/// ],
511/// &file.clone()
512/// ).await?;
513///
514/// let sanitized = sanitized(result.to_vec().to_string()?);
515/// Ok(Some(sanitized?))
516/// }
517///
518/// pub fn process_upload(path: APIPath, params: RUMWebData, form: FormData, state: SharedAppState) -> HTMLResult {
519/// let job_id = rumtk_web_get_job_manager!()?.spawn_task(upload_processor(form))?;
520///
521/// let params = rumtk_web_params_map!([(PARAMS_ID, job_id)]);
522/// let viewer = job_loader(&[], params.get_inner(), state)?.to_string();
523///
524///
525/// rumtk_web_render_page_contents!(
526/// &vec![
527/// viewer
528/// ]
529/// )
530/// }
531///
532/// fn upload_form(builder: FormElementBuilder, _state: &SharedAppState) -> FormElements {
533/// vec![
534/// builder(
535/// "input",
536/// "",
537/// InputProps {
538/// id: Some("file"),
539/// name: Some("file"),
540/// for_element: None,
541/// typ: Some("file"),
542/// value: None,
543/// max: None,
544/// placeholder: Some("path/to/file"),
545/// pattern: None,
546/// accept: Some(".pdf,application/pdf"),
547/// alt: None,
548/// aria_label: Some("PDF File Picker"),
549/// event_handlers: None,
550/// max_length: None,
551/// min_length: None,
552/// autocapitalize: false,
553/// autocomplete: false,
554/// autocorrect: false,
555/// autofocus: false,
556/// disabled: false,
557/// hidden: false,
558/// required: true,
559/// multiple: false,
560/// },
561/// ""
562/// ),
563/// builder(
564/// "input",
565/// "",
566/// InputProps {
567/// id: Some("submit"),
568/// name: None,
569/// for_element: None,
570/// typ: Some("submit"),
571/// value: Some("Send"),
572/// max: None,
573/// placeholder: None,
574/// pattern: None,
575/// accept: None,
576/// alt: None,
577/// aria_label: Some("PDF File Submit Button"),
578/// event_handlers: None,
579/// max_length: None,
580/// min_length: None,
581/// autocapitalize: false,
582/// autocomplete: false,
583/// autocorrect: false,
584/// autofocus: false,
585/// disabled: false,
586/// hidden: false,
587/// required: false,
588/// multiple: false,
589/// },
590/// "f18"
591/// ),
592/// builder(
593/// "progress",
594/// "",
595/// InputProps {
596/// id: Some("progress"),
597/// name: None,
598/// for_element: None,
599/// typ: None,
600/// value: Some("0"),
601/// max: Some("100"),
602/// placeholder: None,
603/// pattern: None,
604/// accept: None,
605/// alt: None,
606/// aria_label: Some("PDF File Submit Progress Bar"),
607/// event_handlers: None,
608/// max_length: None,
609/// min_length: None,
610/// autocapitalize: false,
611/// autocomplete: false,
612/// autocorrect: false,
613/// autofocus: false,
614/// disabled: false,
615/// hidden: true,
616/// required: false,
617/// multiple: false,
618/// },
619/// ""
620/// ),
621/// ]
622/// }
623///
624/// let my_pages: UserPages = vec![
625/// ("myindex", index)
626/// ];
627/// let my_forms: Forms = vec![
628/// ("myform", upload_form)
629/// ];
630/// let my_endpoints: UserAPIEndpoints = vec![
631/// ("/api/upload", process_upload)
632/// ];
633/// let expected = AppComponents {
634/// pages: Some(my_pages.clone()),
635/// forms: Some(my_forms.clone()),
636/// apis: Some(my_endpoints.clone()),
637/// };
638/// let result = rumtk_web_register_app_components!(my_pages, my_forms, my_endpoints);
639///
640/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
641///
642/// ```
643///
644#[macro_export]
645macro_rules! rumtk_web_register_app_components {
646 ( ) => {{
647 use $crate::utils::app::AppComponents;
648
649 AppComponents::default()
650 }};
651 ( $pages:expr ) => {{
652 use $crate::utils::app::AppComponents;
653
654 AppComponents {
655 pages: Some($pages),
656 forms: None,
657 apis: None,
658 }
659 }};
660 ( $pages:expr, $forms:expr ) => {{
661 use $crate::utils::app::AppComponents;
662
663 AppComponents {
664 pages: Some($pages),
665 forms: Some($forms),
666 apis: None,
667 }
668 }};
669 ( $pages:expr, $forms:expr, $apis:expr ) => {{
670 use $crate::utils::app::AppComponents;
671
672 AppComponents {
673 pages: Some($pages),
674 forms: Some($forms),
675 apis: Some($apis),
676 }
677 }};
678}
679
680///
681/// Convenience macro for generating a [AppSwitches] instance containing the boolean options a
682/// framework consumer would like to opt-in.
683///
684/// ## Examples
685/// ```
686/// use rumtk_web::AppSwitches;
687/// use rumtk_web::{rumtk_web_register_app_switches};
688///
689/// let expected = AppSwitches {
690/// skip_serve: true,
691/// skip_default_css: false
692/// };
693/// let switches = rumtk_web_register_app_switches!(true);
694///
695/// assert_eq!(switches, expected, "The switches constructed to config app does not match the expected.");
696/// ```
697///
698#[macro_export]
699macro_rules! rumtk_web_register_app_switches {
700 ( ) => {{
701 use $crate::utils::app::AppSwitches;
702
703 AppSwitches::default()
704 }};
705 ( $($switch:expr),+ ) => {{
706 use $crate::utils::app::AppSwitches;
707
708 AppSwitches::from([$($switch),+])
709 }};
710}
711
712///
713/// This is the main macro for defining your applet and launching it.
714/// Usage is very simple and the only decision from a user is whether to pass a list of
715/// [UserPages](UserPages) or a list of [UserPages](UserPages) and a list
716/// of [UserComponents](UserComponents).
717///
718/// These lists are used to automatically register your pages
719/// (e.g. `/index => ('index', my_index_function)`) and your custom components
720/// (e.g. `button => ('button', my_button_function)`
721///
722/// This macro will load CSS from predefined sources, concatenate their contents with predefined CSS,
723/// minified the concatenated results, and generate a bundle css file containing the minified results.
724/// The CSS bundle is written to file `./static/css/bundle.min.css`.
725///
726/// ***Note: anything in ./static will be considered static assets that need to be served.***
727///
728/// This macro will also parse the command line automatically with a few predefined options and
729/// use that information to override the config defaults.
730///
731/// By default, the app is launched to `127.0.0.1:3000` which is the loopback address.
732///
733/// App is served with the best compression algorithm allowed by the client browser.
734///
735/// For testing purposes, the function
736///
737/// ## Example Usage
738///
739/// ### With Page and Component definition
740/// ```
741/// use rumtk_core::strings::{rumtk_format};
742/// use rumtk_web::{rumtk_web_run_app, rumtk_web_register_app_components, rumtk_web_render_component, rumtk_web_render_template, rumtk_web_get_text_item, rumtk_web_register_app_switches, rumtk_web_get_config, rumtk_web_params_map};
743/// use rumtk_web::components::form::{FormElementBuilder, props::InputProps, FormElements};
744/// use rumtk_web::{SharedAppState, RenderedPageComponentsResult};
745/// use rumtk_web::{APIPath, URLPath, URLParams, HTMLResult, RUMString, RouterForm, FormData, RUMWebData, AppConf};
746/// use rumtk_web::components::portrait_card::portrait_card;
747/// use rumtk_web::components::spacer::spacer;
748/// use rumtk_web::components::text_card::text_card;
749/// use rumtk_web::components::title::title;
750/// use rumtk_web::ComponentResult;
751/// use rumtk_web::components::job_loader::JobLoader;
752/// use rumtk_web::defaults::{DEFAULT_TEXT_ITEM, PARAMS_CONTENTS, PARAMS_CSS_CLASS, PARAMS_TYPE};
753/// use rumtk_web::utils::types::RUMWebTemplate;
754///
755///
756///
757///
758/// // About page
759/// pub fn about(app_state: SharedAppState) -> RenderedPageComponentsResult {
760/// let title_coop_params = rumtk_web_params_map!([(PARAMS_TYPE, "coop_values")]);
761/// let title_coop = title(&[], title_coop_params.get_inner(), app_state.clone())?.to_string();
762///
763/// let title_team_params = rumtk_web_params_map!([(PARAMS_TYPE, "meet_the_team")]);
764/// let title_team = title(&[], title_team_params.get_inner(), app_state.clone())?.to_string();
765///
766/// let text_card_story_params = rumtk_web_params_map!([(PARAMS_TYPE, "story")]);
767/// let text_card_story = text_card(&[], text_card_story_params.get_inner(), app_state.clone())?.to_string();
768///
769/// let text_card_coop_params = rumtk_web_params_map!([(PARAMS_TYPE, "coop_values")]);
770/// let text_card_coop = text_card(&[], text_card_coop_params.get_inner(), app_state.clone())?.to_string();
771///
772/// let portrait_card_params = rumtk_web_params_map!([("section", "company"), (PARAMS_TYPE, "personnel")]);
773/// let portrait_card = portrait_card(&[], portrait_card_params.get_inner(), app_state.clone())?.to_string();
774///
775/// let spacer_5_params = rumtk_web_params_map!([("size", "5")]);
776/// let spacer_5 = spacer(&[], spacer_5_params.get_inner(), app_state.clone())?.to_string();
777///
778/// Ok(vec![
779/// text_card_story,
780/// spacer_5.clone(),
781/// title_coop,
782/// text_card_coop,
783/// spacer_5,
784/// title_team,
785/// portrait_card
786/// ])
787/// }
788///
789/// //Custom component
790/// #[derive(RUMWebTemplate, Debug)]
791/// #[template(
792/// source = "
793/// {% if custom_css_enabled %}
794/// <link href='/static/components/div.css' rel='stylesheet'>
795/// {% endif %}
796/// <div class='div-{{css_class}}'>{{contents|safe}}</div>
797/// ",
798/// ext = "html"
799/// )]
800/// struct MyDiv {
801/// contents: RUMString,
802/// css_class: RUMString,
803/// custom_css_enabled: bool,
804/// }
805///
806/// fn my_div(path_components: URLPath, params: URLParams, state: SharedAppState) -> ComponentResult<MyDiv> {
807/// let contents = rumtk_web_get_text_item!(params, PARAMS_CONTENTS, DEFAULT_TEXT_ITEM);
808/// let css_class = rumtk_web_get_text_item!(params, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM);
809///
810/// let custom_css_enabled = rumtk_web_get_config!(state).flags.custom_css;
811///
812/// Ok(MyDiv {
813/// contents: RUMString::from(contents),
814/// css_class: RUMString::from(css_class),
815/// custom_css_enabled
816/// })
817/// }
818///
819/// fn my_form (builder: FormElementBuilder, _state: &SharedAppState) -> FormElements {
820/// vec![
821/// builder("input", "", InputProps::default(), "default")
822/// ]
823/// }
824///
825/// fn my_api_handler(path: APIPath, params: RUMWebData, form: FormData, state: SharedAppState) -> HTMLResult {
826/// Err(rumtk_format!(
827/// "No handler registered for API endpoint => {}",
828/// path
829/// ))
830/// }
831///
832/// //Requesting to immediately exit instead of indefinitely serving pages so this example can be used as a unit test.
833/// let skip_serve = true;
834/// let skip_default_css = false;
835///
836/// let app_components = rumtk_web_register_app_components!(
837/// vec![("about", about)],
838/// vec![("my_form", my_form)], //Optional, can be omitted alongside the skip_serve flag
839/// vec![("v2/add", my_api_handler)] //Optional, can be omitted alongside the skip_serve flag
840/// );
841/// let app_switches = rumtk_web_register_app_switches!(
842/// skip_serve, //Omit in production code. This is used so that this example can work as a unit test.
843/// skip_default_css //Omit in production code. This is used so that this example can work as a unit test.
844/// );
845/// let result = rumtk_web_run_app!(
846/// app_components,
847/// app_switches
848/// );
849/// ```
850///
851#[macro_export]
852macro_rules! rumtk_web_run_app {
853 ( ) => {{
854 use $crate::utils::app::app_main;
855 use $crate::{rumtk_web_register_app_components, rumtk_web_register_app_switches};
856
857 rumtk_web_run_app!(
858 rumtk_web_register_app_components!(),
859 rumtk_web_register_app_switches!(),
860 )
861 }};
862 ( $app_components:expr ) => {{
863 use $crate::rumtk_web_register_app_switches;
864 let switches = rumtk_web_register_app_switches!();
865 rumtk_web_run_app!($app_components, switches, None)
866 }};
867 ( $app_components:expr, $switches:expr ) => {{
868 rumtk_web_run_app!($app_components, $switches, None)
869 }};
870 ( $app_components:expr, $switches:expr, $port:expr ) => {{
871 use $crate::utils::app::app_main;
872
873 app_main($app_components, $switches, $port)
874 }};
875}