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::{rumtk_web_api_process, rumtk_web_compile_css_bundle, rumtk_web_init_api_endpoints, rumtk_web_init_components, rumtk_web_init_forms, rumtk_web_init_job_manager, rumtk_web_init_pages, SharedAppState};
27use crate::{rumtk_web_fetch, rumtk_web_load_conf};
28
29use rumtk_core::core::RUMResult;
30use rumtk_core::dependencies::clap;
31use rumtk_core::rumtk_resolve_task;
32use rumtk_core::strings::RUMString;
33use rumtk_core::threading::threading_functions::get_default_system_thread_count;
34use rumtk_core::types::{RUMCLIParser, RUMTcpListener};
35
36use crate::api::UserAPIEndpoints;
37use axum::routing::{get, post};
38use axum::Router;
39use tower_http::compression::{CompressionLayer, DefaultPredicate};
40use tower_http::services::ServeDir;
41
42const DEFAULT_UPLOAD_LIMIT: usize = 10240;
43
44///
45/// RUMTK WebApp CLI Args
46///
47#[derive(RUMCLIParser, Debug)]
48#[command(author, version, about, long_about = None)]
49struct Args {
50    ///
51    /// Website title to use internally. It can be omitted if defined in the app.json config file
52    /// bundled with your app.
53    ///
54    #[arg(long, default_value = "")]
55    pub title: RUMString,
56    ///
57    /// Website description string. 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 description: RUMString,
62    ///
63    /// Company to display in website.
64    ///
65    #[arg(long, default_value = "")]
66    pub company: RUMString,
67    ///
68    /// Copyright year to display in website.
69    ///
70    #[arg(short, long, default_value = "")]
71    pub copyright: RUMString,
72    ///
73    /// Directory to scan on startup to find custom CSS sources to bundle into a minified CSS file
74    /// that can be quickly pulled by the app client side.
75    ///
76    /// This option can provide an alternative to direct component retrieval of CSS fragments.
77    /// Meaning, you could bundle all of your fragments into the master bundle at startup and
78    /// turn off component level ```custom_css_enabled``` option in the ```app.json``` config.
79    ///
80    #[arg(long, default_value = DEFAULT_OUT_CSS_DIR)]
81    pub css_source_dir: RUMString,
82    ///
83    /// Is the interface meant to be bound to the loopback address and remain hidden from the
84    /// outside world.
85    ///
86    /// It follows the format ```IPv4:port``` and it is a string.
87    ///
88    /// If a NIC IP is defined via `--ip`, that value will override this flag.
89    ///
90    #[arg(short, long, default_value = DEFAULT_LOCAL_LISTENING_ADDRESS)]
91    pub ip: RUMString,
92    ///
93    /// Specify the size limit for a file upload post request.
94    ///
95    #[arg(long, default_value_t = DEFAULT_UPLOAD_LIMIT)]
96    pub upload_limit: usize,
97    ///
98    /// How many threads to use to serve the website. By default, we use
99    /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
100    /// cpus available. We use the system's total count of cpus by default.
101    ///
102    #[arg(long, default_value_t = get_default_system_thread_count())]
103    pub threads: 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 = false)]
110    pub skip_default_css: bool,
111}
112
113async fn run_app(args: Args, state: SharedAppState, skip_serve: bool) -> RUMResult<()> {
114    let comression_layer: CompressionLayer = CompressionLayer::new()
115        .br(true)
116        .deflate(true)
117        .gzip(true)
118        .zstd(true)
119        .compress_when(DefaultPredicate::new());
120    let app = Router::new()
121        /* Robots.txt */
122        .route("/robots.txt", get(rumtk_web_fetch!(default_robots_matcher)))
123        /* Components */
124        .route(
125            "/component/{*name}",
126            get(rumtk_web_fetch!(default_component_matcher)),
127        )
128        /* Pages */
129        .route("/", get(rumtk_web_fetch!(default_page_matcher)))
130        .route("/{*page}", get(rumtk_web_fetch!(default_page_matcher)))
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 components: Option<UserComponents<'a>>,
227    pub forms: Option<Forms<'a>>,
228    pub apis: Option<UserAPIEndpoints<'a>>,
229}
230
231///
232/// Struct for defining the global switches to drive the initialization of the web app. This struct
233/// works hand in hand with [AppComponents].
234///
235#[derive(Default, Debug, PartialEq)]
236pub struct AppSwitches {
237    pub skip_serve: bool,
238    pub skip_default_css: bool,
239}
240
241///
242/// Main API function for running and serving the web application.
243///
244/// It takes an [AppComponents] instance and a few switches to help preconfigure the framework to
245/// use custom-made components and to register API endpoints.
246///
247/// See [rumtk_web_run_app](crate::rumtk_web_run_app) for more details.
248///
249/// ## Example
250/// ```
251/// use rumtk_web::app_main;
252/// use rumtk_web::{rumtk_web_register_app_switches, rumtk_web_register_app_components};
253///
254/// // We pass true to the switches because we do not want the web server to actually serve the page
255/// // It would hang the test otherwise...
256/// app_main(
257///     rumtk_web_register_app_components!(),
258///     rumtk_web_register_app_switches!(true)
259/// ).expect("Issue occurred while running the app");
260/// ```
261///
262pub fn app_main(app_components: AppComponents<'_>, switches: AppSwitches) -> RUMResult<()> {
263    let args = Args::parse();
264    let state = rumtk_web_load_conf!(&args);
265
266    rumtk_web_init_components!(app_components.components);
267    rumtk_web_init_pages!(app_components.pages);
268    rumtk_web_init_forms!(app_components.forms, state);
269    rumtk_web_init_api_endpoints!(app_components.apis);
270    rumtk_web_compile_css_bundle!(
271        &args.css_source_dir,
272        &args.skip_default_css | switches.skip_default_css
273    );
274
275    rumtk_web_init_job_manager!(&args.threads);
276    let task = run_app(args, state, switches.skip_serve);
277    rumtk_resolve_task!(task)
278}
279
280///
281/// Convenience macro for quickly building the [AppComponents] object. Feel free to pass an instance of
282/// [AppComponents] directly to [run_app] or [rumtk_web_run_app](crate::rumtk_web_run_app).
283///
284/// Passing no parameters generates an "empty" instance, meaning you would be asking the framework that you only
285/// care about built-in components. This also implies you do not want to process API endpoints.
286///
287/// ## Examples
288///
289/// ### Without Parameters
290/// ```
291/// use crate::rumtk_web::AppComponents;
292/// use crate::rumtk_web::rumtk_web_register_app_components;
293///
294/// let expected = AppComponents::default();
295/// let result = rumtk_web_register_app_components!();
296///
297/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
298///
299/// ```
300///
301/// ### With Existing Page
302/// ```
303/// use rumtk_web::pages::UserPages;
304/// use crate::rumtk_web::AppComponents;
305/// use crate::rumtk_web::pages::index::index;
306/// use crate::rumtk_web::rumtk_web_register_app_components;
307///
308/// let my_pages: UserPages = vec![
309///     ("myindex", index)
310/// ];
311/// let expected = AppComponents {
312///     pages: Some(my_pages.clone()),
313///     components: None,
314///     forms: None,
315///     apis: None,
316///  };
317/// let result = rumtk_web_register_app_components!(my_pages);
318///
319/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
320///
321/// ```
322///
323/// ### With Existing Page and Component
324/// ```
325/// use rumtk_web::components::UserComponents;
326/// use rumtk_web::pages::UserPages;
327/// use rumtk_web::AppComponents;
328/// use rumtk_web::pages::index::index;
329/// use rumtk_web::components::div::div;
330/// use rumtk_web::rumtk_web_register_app_components;
331///
332/// let my_pages: UserPages = vec![
333///     ("myindex", index)
334/// ];
335/// let my_components: UserComponents = vec![
336///     ("mydiv", div)
337/// ];
338/// let expected = AppComponents {
339///     pages: Some(my_pages.clone()),
340///     components: Some(my_components.clone()),
341///     forms: None,
342///     apis: None,
343///  };
344/// let result = rumtk_web_register_app_components!(my_pages, my_components);
345///
346/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
347///
348/// ```
349///
350/// ### With Existing Page and Component and Form
351/// ```
352/// use rumtk_core::strings::RUMString;
353/// use rumtk_web::{AppComponents, SharedAppState};
354/// use rumtk_web::components::UserComponents;
355/// use rumtk_web::pages::UserPages;
356/// use rumtk_web::components::form::{FormElementBuilder, FormElements, Forms};
357/// use rumtk_web::pages::index::index;
358/// use rumtk_web::components::div::div;
359/// use rumtk_web::components::form::props::InputProps;
360/// use rumtk_web::rumtk_web_register_app_components;
361///
362/// fn upload_form(builder: FormElementBuilder, _state: &SharedAppState) -> FormElements {
363///     vec![
364///         builder(
365///             "input",
366///             "",
367///             InputProps {
368///                 id: Some("file"),
369///                 name: Some("file"),
370///                 for_element: None,
371///                 typ: Some("file"),
372///                 value: None,
373///                 max: None,
374///                 placeholder: Some("path/to/file"),
375///                 pattern: None,
376///                 accept: Some(".pdf,application/pdf"),
377///                 alt: None,
378///                 aria_label: Some("PDF File Picker"),
379///                 event_handlers: None,
380///                 max_length: None,
381///                 min_length: None,
382///                 autocapitalize: false,
383///                 autocomplete: false,
384///                 autocorrect: false,
385///                 autofocus: false,
386///                 disabled: false,
387///                 hidden: false,
388///                 required: true,
389///                 multiple: false,
390///             },
391///             ""
392///         ),
393///         builder(
394///             "input",
395///             "",
396///             InputProps {
397///                 id: Some("submit"),
398///                 name: None,
399///                 for_element: None,
400///                 typ: Some("submit"),
401///                 value: Some("Send"),
402///                 max: None,
403///                 placeholder: None,
404///                 pattern: None,
405///                 accept: None,
406///                 alt: None,
407///                 aria_label: Some("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///                 multiple: false,
419///             },
420///             "f18"
421///         ),
422///         builder(
423///             "progress",
424///             "",
425///             InputProps {
426///                 id: Some("progress"),
427///                 name: None,
428///                 for_element: None,
429///                 typ: None,
430///                 value: Some("0"),
431///                 max: Some("100"),
432///                 placeholder: None,
433///                 pattern: None,
434///                 accept: None,
435///                 alt: None,
436///                 aria_label: Some("PDF File Submit Progress Bar"),
437///                 event_handlers: None,
438///                 max_length: None,
439///                 min_length: None,
440///                 autocapitalize: false,
441///                 autocomplete: false,
442///                 autocorrect: false,
443///                 autofocus: false,
444///                 disabled: false,
445///                 hidden: true,
446///                 required: false,
447///                 multiple: false,
448///             },
449///             ""
450///         ),
451///     ]
452/// }
453///
454/// let my_pages: UserPages = vec![
455///     ("myindex", index)
456/// ];
457/// let my_components: UserComponents = vec![
458///     ("mydiv", div)
459/// ];
460/// let my_forms: Forms = vec![
461///     ("myform", upload_form)
462/// ];
463/// let expected = AppComponents {
464///     pages: Some(my_pages.clone()),
465///     components: Some(my_components.clone()),
466///     forms: Some(my_forms.clone()),
467///     apis: None,
468///  };
469/// let result = rumtk_web_register_app_components!(my_pages, my_components, my_forms);
470///
471/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
472///
473/// ```
474///
475/// ### With Existing Page and Component and Form and API Endpoint
476/// ```
477/// use rumtk_core::{rumtk_pipeline_run_async, rumtk_pipeline_command};
478/// use rumtk_core::strings::{RUMString, RUMStringConversions, RUMArrayConversions};
479/// use rumtk_web::{rumtk_web_post_process_html, APIPath, AppComponents, FormData, HTMLResult, RUMWebData, RUMWebResponse, SharedAppState};
480/// use rumtk_web::{rumtk_web_get_job_manager, rumtk_web_render_component, rumtk_web_render_page_contents};
481/// use rumtk_web::api::UserAPIEndpoints;
482/// use rumtk_web::components::UserComponents;
483/// use rumtk_web::pages::UserPages;
484/// use rumtk_web::components::form::{FormElementBuilder, FormElements, Forms};
485/// use rumtk_web::pages::index::index;
486/// use rumtk_web::components::div::div;
487/// use rumtk_web::components::form::props::InputProps;
488/// use rumtk_web::jobs::{JobResult};
489/// use rumtk_web::utils::defaults::{PARAMS_TARGET};
490/// use rumtk_web::rumtk_web_register_app_components;
491///
492/// async fn upload_processor(form: FormData) -> JobResult {
493///     let id = form.form.get("file").unwrap();
494///     let file = form.files.get(id).unwrap();
495///
496///     let result = rumtk_pipeline_run_async!(
497///         &vec![
498///             rumtk_pipeline_command!("cat"),
499///             rumtk_pipeline_command!("wc")
500///         ],
501///         &file.clone()
502///     ).await?;
503///
504///     Ok(Some(rumtk_web_post_process_html!(result.to_vec().to_string()?)))
505/// }
506///
507/// pub fn process_upload(path: APIPath, params: RUMWebData, form: FormData, state: SharedAppState) -> HTMLResult {
508///     let job_id = rumtk_web_get_job_manager!()?.spawn_task(upload_processor(form))?;
509///     let mydiv = rumtk_web_render_component!("mydiv", [(PARAMS_TARGET, job_id)], state)?.to_string();
510///
511///     rumtk_web_render_page_contents!(
512///         &vec![
513///             mydiv
514///         ]
515///     )
516/// }
517///
518/// fn upload_form(builder: FormElementBuilder, _state: &SharedAppState) -> FormElements {
519///     vec![
520///         builder(
521///             "input",
522///             "",
523///             InputProps {
524///                 id: Some("file"),
525///                 name: Some("file"),
526///                 for_element: None,
527///                 typ: Some("file"),
528///                 value: None,
529///                 max: None,
530///                 placeholder: Some("path/to/file"),
531///                 pattern: None,
532///                 accept: Some(".pdf,application/pdf"),
533///                 alt: None,
534///                 aria_label: Some("PDF File Picker"),
535///                 event_handlers: None,
536///                 max_length: None,
537///                 min_length: None,
538///                 autocapitalize: false,
539///                 autocomplete: false,
540///                 autocorrect: false,
541///                 autofocus: false,
542///                 disabled: false,
543///                 hidden: false,
544///                 required: true,
545///                 multiple: false,
546///             },
547///             ""
548///         ),
549///         builder(
550///             "input",
551///             "",
552///             InputProps {
553///                 id: Some("submit"),
554///                 name: None,
555///                 for_element: None,
556///                 typ: Some("submit"),
557///                 value: Some("Send"),
558///                 max: None,
559///                 placeholder: None,
560///                 pattern: None,
561///                 accept: None,
562///                 alt: None,
563///                 aria_label: Some("PDF File Submit Button"),
564///                 event_handlers: None,
565///                 max_length: None,
566///                 min_length: None,
567///                 autocapitalize: false,
568///                 autocomplete: false,
569///                 autocorrect: false,
570///                 autofocus: false,
571///                 disabled: false,
572///                 hidden: false,
573///                 required: false,
574///                 multiple: false,
575///             },
576///             "f18"
577///         ),
578///         builder(
579///             "progress",
580///             "",
581///             InputProps {
582///                 id: Some("progress"),
583///                 name: None,
584///                 for_element: None,
585///                 typ: None,
586///                 value: Some("0"),
587///                 max: Some("100"),
588///                 placeholder: None,
589///                 pattern: None,
590///                 accept: None,
591///                 alt: None,
592///                 aria_label: Some("PDF File Submit Progress Bar"),
593///                 event_handlers: None,
594///                 max_length: None,
595///                 min_length: None,
596///                 autocapitalize: false,
597///                 autocomplete: false,
598///                 autocorrect: false,
599///                 autofocus: false,
600///                 disabled: false,
601///                 hidden: true,
602///                 required: false,
603///                 multiple: false,
604///             },
605///             ""
606///         ),
607///     ]
608/// }
609///
610/// let my_pages: UserPages = vec![
611///     ("myindex", index)
612/// ];
613/// let my_components: UserComponents = vec![
614///     ("mydiv", div)
615/// ];
616/// let my_forms: Forms = vec![
617///     ("myform", upload_form)
618/// ];
619/// let my_endpoints: UserAPIEndpoints = vec![
620///     ("/api/upload", process_upload)
621/// ];
622/// let expected = AppComponents {
623///     pages: Some(my_pages.clone()),
624///     components: Some(my_components.clone()),
625///     forms: Some(my_forms.clone()),
626///     apis: Some(my_endpoints.clone()),
627///  };
628/// let result = rumtk_web_register_app_components!(my_pages, my_components, my_forms, my_endpoints);
629///
630/// assert_eq!(result, expected, "Default macro-generated instance of AppComponents are not the same!");
631///
632/// ```
633///
634#[macro_export]
635macro_rules! rumtk_web_register_app_components {
636    (  ) => {{
637        use $crate::utils::app::AppComponents;
638
639        AppComponents::default()
640    }};
641    ( $pages:expr ) => {{
642        use $crate::utils::app::AppComponents;
643
644        AppComponents {
645            pages: Some($pages),
646            components: None,
647            forms: None,
648            apis: None,
649        }
650    }};
651    ( $pages:expr, $components:expr ) => {{
652        use $crate::utils::app::AppComponents;
653
654        AppComponents {
655            pages: Some($pages),
656            components: Some($components),
657            forms: None,
658            apis: None,
659        }
660    }};
661    ( $pages:expr, $components:expr, $forms:expr ) => {{
662        use $crate::utils::app::AppComponents;
663
664        AppComponents {
665            pages: Some($pages),
666            components: Some($components),
667            forms: Some($forms),
668            apis: None,
669        }
670    }};
671    ( $pages:expr, $components:expr, $forms:expr, $apis:expr ) => {{
672        use $crate::utils::app::AppComponents;
673
674        AppComponents {
675            pages: Some($pages),
676            components: Some($components),
677            forms: Some($forms),
678            apis: Some($apis),
679        }
680    }};
681}
682
683///
684/// Convenience macro for generating a [AppSwitches] instance containing the boolean options a
685/// framework consumer would like to opt-in.
686///
687/// ## Examples
688/// ```
689/// use rumtk_web::AppSwitches;
690/// use rumtk_web::{rumtk_web_register_app_switches};
691///
692/// let expected = AppSwitches {
693///     skip_serve: true,
694///     skip_default_css: false
695/// };
696/// let switches = rumtk_web_register_app_switches!(true);
697///
698/// assert_eq!(switches, expected, "The switches constructed to config app does not match the expected.");
699/// ```
700///
701#[macro_export]
702macro_rules! rumtk_web_register_app_switches {
703    (  ) => {{
704        use $crate::utils::app::AppSwitches;
705
706        AppSwitches::default()
707    }};
708    ( $skip_serve:expr ) => {{
709        use $crate::utils::app::AppSwitches;
710
711        AppSwitches {
712            skip_serve: $skip_serve,
713            skip_default_css: false,
714        }
715    }};
716    ( $skip_serve:expr, $skip_default_css:expr ) => {{
717        use $crate::utils::app::AppSwitches;
718
719        AppSwitches {
720            skip_serve: $skip_serve,
721            skip_default_css: $skip_default_css,
722        }
723    }};
724}
725
726///
727/// This is the main macro for defining your applet and launching it.
728/// Usage is very simple and the only decision from a user is whether to pass a list of
729/// [UserPages](UserPages) or a list of [UserPages](UserPages) and a list
730/// of [UserComponents](UserComponents).
731///
732/// These lists are used to automatically register your pages
733/// (e.g. `/index => ('index', my_index_function)`) and your custom components
734/// (e.g. `button => ('button', my_button_function)`
735///
736/// This macro will load CSS from predefined sources, concatenate their contents with predefined CSS,
737/// minified the concatenated results, and generate a bundle css file containing the minified results.
738/// The CSS bundle is written to file `./static/css/bundle.min.css`.
739///
740/// ***Note: anything in ./static will be considered static assets that need to be served.***
741///
742/// This macro will also parse the command line automatically with a few predefined options and
743/// use that information to override the config defaults.
744///
745/// By default, the app is launched to `127.0.0.1:3000` which is the loopback address.
746///
747/// App is served with the best compression algorithm allowed by the client browser.
748///
749/// For testing purposes, the function
750///
751/// ## Example Usage
752///
753/// ### With Page and Component definition
754/// ```
755///     use rumtk_core::strings::{rumtk_format};
756///     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};
757///     use rumtk_web::components::form::{FormElementBuilder, props::InputProps, FormElements};
758///     use rumtk_web::{SharedAppState, RenderedPageComponentsResult};
759///     use rumtk_web::{APIPath, URLPath, URLParams, HTMLResult, RUMString, RouterForm, FormData, RUMWebData, AppConf};
760///     use rumtk_web::defaults::{DEFAULT_TEXT_ITEM, PARAMS_CONTENTS, PARAMS_CSS_CLASS, PARAMS_TYPE};
761///     use rumtk_web::utils::types::RUMWebTemplate;
762///
763///
764///
765///
766///     // About page
767///     pub fn about(app_state: SharedAppState) -> RenderedPageComponentsResult {
768///         let title_coop = rumtk_web_render_component!("title", [(PARAMS_TYPE, "coop_values")], app_state)?.to_string();
769///         let title_team = rumtk_web_render_component!("title", [(PARAMS_TYPE, "meet_the_team")], app_state)?.to_string();
770///     
771///         let text_card_story = rumtk_web_render_component!("text_card", [(PARAMS_TYPE, "story")], app_state)?.to_string();
772///         let text_card_coop = rumtk_web_render_component!("text_card", [(PARAMS_TYPE, "coop_values")], app_state)?.to_string();
773///     
774///         let portrait_card = rumtk_web_render_component!("portrait_card", [("section", "company"), (PARAMS_TYPE, "personnel")], app_state)?.to_string();
775///     
776///         let spacer_5 = rumtk_web_render_component!("spacer", [("size", "5")], app_state)?.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) -> HTMLResult {
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).custom_css;
811///
812///         rumtk_web_render_template!(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_div", my_div)], //Optional, can be omitted alongside the skip_serve flag
839///         vec![("my_form", my_form)], //Optional, can be omitted alongside the skip_serve flag
840///         vec![("v2/add", my_api_handler)] //Optional, can be omitted alongside the skip_serve flag
841///     );
842///     let app_switches = rumtk_web_register_app_switches!(
843///         skip_serve, //Omit in production code. This is used so that this example can work as a unit test.
844///         skip_default_css //Omit in production code. This is used so that this example can work as a unit test.
845///     );
846///     let result = rumtk_web_run_app!(
847///         app_components,
848///         app_switches
849///     );
850/// ```
851///
852#[macro_export]
853macro_rules! rumtk_web_run_app {
854    (  ) => {{
855        use $crate::utils::app::app_main;
856        use $crate::{rumtk_web_register_app_components, rumtk_web_register_app_switches};
857
858        app_main(
859            rumtk_web_register_app_components!(),
860            rumtk_web_register_app_switches!(),
861        )
862    }};
863    ( $app_components:expr ) => {{
864        use $crate::rumtk_web_register_app_switches;
865        use $crate::utils::app::app_main;
866
867        app_main($app_components, rumtk_web_register_app_switches!())
868    }};
869    ( $app_components:expr, $switches:expr ) => {{
870        use $crate::utils::app::app_main;
871
872        app_main($app_components, $switches)
873    }};
874}