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