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}