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;
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}