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::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::{rumtk_web_compile_css_bundle, rumtk_web_init_components, rumtk_web_init_pages};
29use crate::{rumtk_web_fetch, rumtk_web_load_conf};
30
31use rumtk_core::core::RUMResult;
32use rumtk_core::dependencies::clap;
33use rumtk_core::strings::RUMString;
34use rumtk_core::threading::threading_functions::get_default_system_thread_count;
35use rumtk_core::types::{RUMCLIParser, RUMTcpListener};
36use rumtk_core::{rumtk_init_threads, rumtk_resolve_task};
37
38use axum::routing::get;
39use axum::Router;
40use tower_http::compression::{CompressionLayer, DefaultPredicate};
41use tower_http::services::ServeDir;
42use tracing::error;
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 /// How many threads to use to serve the website. By default, we use
94 /// ```get_default_system_thread_count()``` from ```rumtk-core``` to detect the total count of
95 /// cpus available. We use the system's total count of cpus by default.
96 ///
97 #[arg(long, default_value_t = get_default_system_thread_count())]
98 pub threads: usize,
99}
100
101async fn run_app(args: &Args, skip_serve: bool) -> RUMResult<()> {
102 let state = rumtk_web_load_conf!(&args);
103 let comression_layer: CompressionLayer = CompressionLayer::new()
104 .br(true)
105 .deflate(true)
106 .gzip(true)
107 .zstd(true)
108 .compress_when(DefaultPredicate::new());
109 let app = Router::new()
110 /* Robots.txt */
111 .route("/robots.txt", get(rumtk_web_fetch!(default_robots_matcher)))
112 /* Components */
113 .route(
114 "/component/{*name}",
115 get(rumtk_web_fetch!(default_component_matcher)),
116 )
117 /* Pages */
118 .route("/", get(rumtk_web_fetch!(default_page_matcher)))
119 .route("/{*page}", get(rumtk_web_fetch!(default_page_matcher)))
120 /* Services */
121 .nest_service("/static", ServeDir::new("static"))
122 .with_state(state)
123 .layer(comression_layer);
124
125 let listener = RUMTcpListener::bind(&args.ip.as_str())
126 .await
127 .expect("There was an issue biding the listener.");
128 println!("listening on {}", listener.local_addr().unwrap());
129
130 if !skip_serve {
131 axum::serve(listener, app)
132 .await
133 .expect("There was an issue with the server.");
134 }
135
136 Ok(())
137}
138
139pub fn app_main(pages: &UserPages, components: &UserComponents, skip_serve: bool) {
140 let args = Args::parse();
141
142 rumtk_web_init_components!(components);
143 rumtk_web_init_pages!(pages);
144 rumtk_web_compile_css_bundle!(&args.css_source_dir);
145
146 let rt = rumtk_init_threads!(&args.threads);
147 let task = run_app(&args, skip_serve);
148 rumtk_resolve_task!(rt, task);
149}
150
151///
152/// This is the main macro for defining your applet and launching it.
153/// Usage is very simple and the only decision from a user is whether to pass a list of
154/// [UserPages](crate::pages::UserPages) or a list of [UserPages](crate::pages::UserPages) and a list
155/// of [UserComponents](crate::components::UserComponents).
156///
157/// These lists are used to automatically register your pages
158/// (e.g. `/index => ('index', my_index_function)`) and your custom components
159/// (e.g. `button => ('button', my_button_function)`
160///
161/// This macro will load CSS from predefined sources, concatenate their contents with predefined CSS,
162/// minified the concatenated results, and generate a bundle css file containing the minified results.
163/// The CSS bundle is written to file `./static/css/bundle.min.css`.
164///
165/// ***Note: anything in ./static will be considered static assets that need to be served.***
166///
167/// This macro will also parse the command line automatically with a few predefined options and
168/// use that information to override the config defaults.
169///
170/// By default, the app is launched to `127.0.0.1:3000` which is the loopback address.
171///
172/// App is served with the best compression algorithm allowed by the client browser.
173///
174/// For testing purposes, the function
175///
176/// ## Example Usage
177///
178/// ### With Page and Component definition
179/// ```
180/// use rumtk_web::{
181/// rumtk_web_run_app,
182/// rumtk_web_render_component,
183/// rumtk_web_render_html,
184/// rumtk_web_get_text_item
185/// };
186/// use rumtk_web::{SharedAppConf, RenderedPageComponents};
187/// use rumtk_web::{URLPath, URLParams, HTMLResult, RUMString};
188/// use rumtk_web::defaults::{DEFAULT_TEXT_ITEM, PARAMS_CONTENTS, PARAMS_CSS_CLASS};
189///
190/// use askama::Template;
191///
192/// // About page
193/// pub fn about(app_state: SharedAppConf) -> RenderedPageComponents {
194/// let title_coop = rumtk_web_render_component!("title", [("type", "coop_values")], app_state.clone());
195/// let title_team = rumtk_web_render_component!("title", [("type", "meet_the_team")], app_state.clone());
196///
197/// let text_card_story = rumtk_web_render_component!("text_card", [("type", "story")], app_state.clone());
198/// let text_card_coop = rumtk_web_render_component!("text_card", [("type", "coop_values")], app_state.clone());
199///
200/// let portrait_card = rumtk_web_render_component!("portrait_card", [("section", "company"), ("type", "personnel")], app_state.clone());
201///
202/// let spacer_5 = rumtk_web_render_component!("spacer", [("size", "5")], app_state.clone());
203///
204/// vec![
205/// text_card_story,
206/// spacer_5.clone(),
207/// title_coop,
208/// text_card_coop,
209/// spacer_5,
210/// title_team,
211/// portrait_card
212/// ]
213/// }
214///
215/// //Custom component
216/// #[derive(Template, Debug)]
217/// #[template(
218/// source = "
219/// <style>
220///
221/// </style>
222/// {% if custom_css_enabled %}
223/// <link href='/static/components/div.css' rel='stylesheet'>
224/// {% endif %}
225/// <div class='div-{{css_class}}'>{{contents|safe}}</div>
226/// ",
227/// ext = "html"
228/// )]
229/// struct MyDiv {
230/// contents: RUMString,
231/// css_class: RUMString,
232/// custom_css_enabled: bool,
233/// }
234///
235/// fn my_div(path_components: URLPath, params: URLParams, state: SharedAppConf) -> HTMLResult {
236/// let contents = rumtk_web_get_text_item!(params, PARAMS_CONTENTS, DEFAULT_TEXT_ITEM);
237/// let css_class = rumtk_web_get_text_item!(params, PARAMS_CSS_CLASS, DEFAULT_TEXT_ITEM);
238///
239/// let custom_css_enabled = state.read().expect("Lock failure").custom_css;
240///
241/// rumtk_web_render_html!(MyDiv {
242/// contents: RUMString::from(contents),
243/// css_class: RUMString::from(css_class),
244/// custom_css_enabled
245/// })
246/// }
247///
248/// //Requesting to immediately exit instead of indefinitely serving pages so this example can be used as a unit test.
249/// let skip_serve = true;
250///
251/// let result = rumtk_web_run_app!(
252/// vec![("about", about)],
253/// vec![("my_div", my_div)], //Optional, can be omitted alongside the skip_serve flag
254/// skip_serve //Omit in production code. This is used so that this example can work as a unit test.
255/// );
256/// ```
257///
258#[macro_export]
259macro_rules! rumtk_web_run_app {
260 ( ) => {{
261 use $crate::utils::app::app_main;
262
263 app_main(&vec![], &vec![], false)
264 }};
265 ( $pages:expr ) => {{
266 use $crate::utils::app::app_main;
267
268 app_main(&$pages, &vec![], false)
269 }};
270 ( $pages:expr, $components:expr ) => {{
271 use $crate::utils::app::app_main;
272
273 app_main(&$pages, &$components, false)
274 }};
275 ( $pages:expr, $components:expr, $skip_serve:expr ) => {{
276 use $crate::utils::app::app_main;
277
278 app_main(&$pages, &$components, $skip_serve)
279 }};
280}