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_rumstring())))
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_rumstring();
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_rumstring();
769/// let title_team = rumtk_web_render_component!("title", [(PARAMS_TYPE, "meet_the_team")], app_state)?.to_rumstring();
770///
771/// let text_card_story = rumtk_web_render_component!("text_card", [(PARAMS_TYPE, "story")], app_state)?.to_rumstring();
772/// let text_card_coop = rumtk_web_render_component!("text_card", [(PARAMS_TYPE, "coop_values")], app_state)?.to_rumstring();
773///
774/// let portrait_card = rumtk_web_render_component!("portrait_card", [("section", "company"), (PARAMS_TYPE, "personnel")], app_state)?.to_rumstring();
775///
776/// let spacer_5 = rumtk_web_render_component!("spacer", [("size", "5")], app_state)?.to_rumstring();
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}