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.
5 * Copyright (C) 2025  Nick Stephenson
6 * Copyright (C) 2025  Ethan Dixon
7 * Copyright (C) 2025  MedicalMasses L.L.C.
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 */
23use crate::components::{form::Forms, UserComponents};
24use crate::css::DEFAULT_OUT_CSS_DIR;
25use crate::pages::UserPages;
26use crate::utils::defaults::DEFAULT_LOCAL_LISTENING_ADDRESS;
27use crate::utils::matcher::*;
28use crate::{
29    rumtk_web_compile_css_bundle, rumtk_web_init_components, rumtk_web_init_forms,
30    rumtk_web_init_pages,
31};
32use crate::{rumtk_web_fetch, rumtk_web_load_conf};
33
34use rumtk_core::core::RUMResult;
35use rumtk_core::dependencies::clap;
36use rumtk_core::strings::RUMString;
37use rumtk_core::threading::threading_functions::get_default_system_thread_count;
38use rumtk_core::types::{RUMCLIParser, RUMTcpListener};
39use rumtk_core::{rumtk_init_threads, rumtk_resolve_task};
40
41use axum::routing::get;
42use axum::Router;
43use tower_http::compression::{CompressionLayer, DefaultPredicate};
44use tower_http::services::ServeDir;
45use tracing::error;
46
47///
48/// RUMTK WebApp CLI Args
49///
50#[derive(RUMCLIParser, Debug)]
51#[command(author, version, about, long_about = None)]
52struct Args {
53    ///
54    /// Website title to use internally. It can be omitted if defined in the app.json config file
55    /// bundled with your app.
56    ///
57    #[arg(long, default_value = "")]
58    pub title: RUMString,
59    ///
60    /// Website description string. It can be omitted if defined in the app.json config file
61    /// bundled with your app.
62    ///
63    #[arg(long, default_value = "")]
64    pub description: RUMString,
65    ///
66    /// Company to display in website.
67    ///
68    #[arg(long, default_value = "")]
69    pub company: RUMString,
70    ///
71    /// Copyright year to display in website.
72    ///
73    #[arg(short, long, default_value = "")]
74    pub copyright: RUMString,
75    ///
76    /// Directory to scan on startup to find custom CSS sources to bundle into a minified CSS file
77    /// that can be quickly pulled by the app client side.
78    ///
79    /// This option can provide an alternative to direct component retrieval of CSS fragments.
80    /// Meaning, you could bundle all of your fragments into the master bundle at startup and
81    /// turn off component level ```custom_css_enabled``` option in the ```app.json``` config.
82    ///
83    #[arg(long, default_value = DEFAULT_OUT_CSS_DIR)]
84    pub css_source_dir: RUMString,
85    ///
86    /// Is the interface meant to be bound to the loopback address and remain hidden from the
87    /// outside world.
88    ///
89    /// It follows the format ```IPv4:port``` and it is a string.
90    ///
91    /// If a NIC IP is defined via `--ip`, that value will override this flag.
92    ///
93    #[arg(short, long, default_value = DEFAULT_LOCAL_LISTENING_ADDRESS)]
94    pub ip: RUMString,
95    ///
96    /// How many threads to use to serve the website. By default, we use
97    /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
98    /// cpus available. We use the system's total count of cpus by default.
99    ///
100    #[arg(long, default_value_t = get_default_system_thread_count())]
101    pub threads: usize,
102}
103
104async fn run_app(args: &Args, skip_serve: bool) -> RUMResult<()> {
105    let state = rumtk_web_load_conf!(&args);
106    let comression_layer: CompressionLayer = CompressionLayer::new()
107        .br(true)
108        .deflate(true)
109        .gzip(true)
110        .zstd(true)
111        .compress_when(DefaultPredicate::new());
112    let app = Router::new()
113        /* Robots.txt */
114        .route("/robots.txt", get(rumtk_web_fetch!(default_robots_matcher)))
115        /* Components */
116        .route(
117            "/component/{*name}",
118            get(rumtk_web_fetch!(default_component_matcher)),
119        )
120        /* Pages */
121        .route("/", get(rumtk_web_fetch!(default_page_matcher)))
122        .route("/{*page}", get(rumtk_web_fetch!(default_page_matcher)))
123        /* Services */
124        .nest_service("/static", ServeDir::new("static"))
125        .with_state(state)
126        .layer(comression_layer);
127
128    let listener = RUMTcpListener::bind(&args.ip.as_str())
129        .await
130        .expect("There was an issue biding the listener.");
131    println!("listening on {}", listener.local_addr().unwrap());
132
133    if !skip_serve {
134        axum::serve(listener, app)
135            .await
136            .expect("There was an issue with the server.");
137    }
138
139    Ok(())
140}
141
142pub fn app_main(pages: &UserPages, components: &UserComponents, forms: &Forms, skip_serve: bool) {
143    let args = Args::parse();
144
145    rumtk_web_init_components!(components);
146    rumtk_web_init_pages!(pages);
147    rumtk_web_init_forms!(forms);
148    rumtk_web_compile_css_bundle!(&args.css_source_dir);
149
150    let rt = rumtk_init_threads!(&args.threads);
151    let task = run_app(&args, skip_serve);
152    rumtk_resolve_task!(rt, task);
153}
154
155///
156/// This is the main macro for defining your applet and launching it.
157/// Usage is very simple and the only decision from a user is whether to pass a list of
158/// [UserPages](crate::pages::UserPages) or a list of [UserPages](crate::pages::UserPages) and a list
159/// of [UserComponents](crate::components::UserComponents).
160///
161/// These lists are used to automatically register your pages
162/// (e.g. `/index => ('index', my_index_function)`) and your custom components
163/// (e.g. `button => ('button', my_button_function)`
164///
165/// This macro will load CSS from predefined sources, concatenate their contents with predefined CSS,
166/// minified the concatenated results, and generate a bundle css file containing the minified results.
167/// The CSS bundle is written to file `./static/css/bundle.min.css`.
168///
169/// ***Note: anything in ./static will be considered static assets that need to be served.***
170///
171/// This macro will also parse the command line automatically with a few predefined options and
172/// use that information to override the config defaults.
173///
174/// By default, the app is launched to `127.0.0.1:3000` which is the loopback address.
175///
176/// App is served with the best compression algorithm allowed by the client browser.
177///
178/// For testing purposes, the function
179///
180/// ## Example Usage
181///
182/// ### With Page and Component definition
183/// ```
184///     use rumtk_web::{
185///         rumtk_web_run_app,
186///         rumtk_web_render_component,
187///         rumtk_web_render_html,
188///         rumtk_web_get_text_item
189///     };
190///     use rumtk_web::components::form::{FormElementBuilder, props::InputProps, FormElements};
191///     use rumtk_web::{SharedAppConf, RenderedPageComponents};
192///     use rumtk_web::{URLPath, URLParams, HTMLResult, RUMString};
193///     use rumtk_web::defaults::{DEFAULT_TEXT_ITEM, PARAMS_CONTENTS, PARAMS_CSS_CLASS};
194///
195///     use askama::Template;
196///
197///
198///
199///     // About page
200///     pub fn about(app_state: SharedAppConf) -> RenderedPageComponents {
201///         let title_coop = rumtk_web_render_component!("title", [("type", "coop_values")], app_state.clone());
202///         let title_team = rumtk_web_render_component!("title", [("type", "meet_the_team")], app_state.clone());
203///     
204///         let text_card_story = rumtk_web_render_component!("text_card", [("type", "story")], app_state.clone());
205///         let text_card_coop = rumtk_web_render_component!("text_card", [("type", "coop_values")], app_state.clone());
206///     
207///         let portrait_card = rumtk_web_render_component!("portrait_card", [("section", "company"), ("type", "personnel")], app_state.clone());
208///     
209///         let spacer_5 = rumtk_web_render_component!("spacer", [("size", "5")], app_state.clone());
210///     
211///         vec![
212///             text_card_story,
213///             spacer_5.clone(),
214///             title_coop,
215///             text_card_coop,
216///             spacer_5,
217///             title_team,
218///             portrait_card
219///         ]
220///     }
221///
222///     //Custom component
223///     #[derive(Template, Debug)]
224///     #[template(
225///             source = "
226///                <style>
227///
228///                </style>
229///                {% if custom_css_enabled %}
230///                    <link href='/static/components/div.css' rel='stylesheet'>
231///                {% endif %}
232///                <div class='div-{{css_class}}'>{{contents|safe}}</div>
233///            ",
234///            ext = "html"
235///     )]
236///     struct MyDiv {
237///         contents: RUMString,
238///         css_class: RUMString,
239///         custom_css_enabled: bool,
240///     }
241///
242///     fn my_div(path_components: URLPath, params: URLParams, state: SharedAppConf) -> HTMLResult {
243///         let contents = rumtk_web_get_text_item!(params, PARAMS_CONTENTS, DEFAULT_TEXT_ITEM);
244///         let css_class = rumtk_web_get_text_item!(params, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM);
245///
246///         let custom_css_enabled = state.read().expect("Lock failure").custom_css;
247///
248///         rumtk_web_render_html!(MyDiv {
249///             contents: RUMString::from(contents),
250///             css_class: RUMString::from(css_class),
251///             custom_css_enabled
252///         })
253///     }
254///
255///     fn my_form (builder: FormElementBuilder) -> FormElements {
256///         vec![
257///             builder("input", "", InputProps::default(), "default")
258///         ]
259///     }
260///
261///     //Requesting to immediately exit instead of indefinitely serving pages so this example can be used as a unit test.
262///     let skip_serve = true;
263///
264///     let result = rumtk_web_run_app!(
265///         vec![("about", about)],
266///         vec![("my_div", my_div)], //Optional, can be omitted alongside the skip_serve flag
267///         vec![("my_form", my_form)], //Optional, can be omitted alongside the skip_serve flag
268///         skip_serve //Omit in production code. This is used so that this example can work as a unit test.
269///     );
270/// ```
271///
272#[macro_export]
273macro_rules! rumtk_web_run_app {
274    (  ) => {{
275        use $crate::utils::app::app_main;
276
277        app_main(&vec![], &vec![], &vec![], false)
278    }};
279    ( $pages:expr ) => {{
280        use $crate::utils::app::app_main;
281
282        app_main(&$pages, &vec![], &vec![], false)
283    }};
284    ( $pages:expr, $components:expr ) => {{
285        use $crate::utils::app::app_main;
286
287        app_main(&$pages, &$components, &vec![], false)
288    }};
289    ( $pages:expr, $components:expr, $forms:expr ) => {{
290        use $crate::utils::app::app_main;
291
292        app_main(&$pages, &$components, &$forms, false)
293    }};
294    ( $pages:expr, $components:expr, $forms:expr, $skip_serve:expr ) => {{
295        use $crate::utils::app::app_main;
296
297        app_main(&$pages, &$components, &$forms, $skip_serve)
298    }};
299}