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