rumtk_web/utils/conf.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::jobs::{Job, JobID};
22use crate::utils::defaults::DEFAULT_TEXT_ITEM;
23use crate::utils::types::RUMString;
24use axum::extract::State;
25use phf::OrderedMap;
26pub use phf_macros::phf_ordered_map as rumtk_create_const_ordered_map;
27use rumtk_core::net::tcp::SafeLock;
28use rumtk_core::strings::RUMStringConversions;
29use rumtk_core::types::{RUMDeserialize, RUMDeserializer, RUMSerialize, RUMSerializer, RUMID};
30use rumtk_core::types::{RUMHashMap, RUMOrderedMap};
31use rumtk_core::{rumtk_critical_section_read, rumtk_generate_id, rumtk_new_lock};
32
33pub type TextMap = RUMOrderedMap<RUMString, RUMString>;
34pub type NestedTextMap = RUMOrderedMap<RUMString, TextMap>;
35pub type NestedNestedTextMap = RUMOrderedMap<RUMString, NestedTextMap>;
36pub type RootNestedNestedTextMap = RUMOrderedMap<RUMString, NestedNestedTextMap>;
37
38pub type ConstTextMap = OrderedMap<&'static str, &'static str>;
39pub type ConstNestedTextMap = OrderedMap<&'static str, &'static ConstTextMap>;
40pub type ConstNestedNestedTextMap = OrderedMap<&'static str, &'static ConstNestedTextMap>;
41
42#[derive(RUMSerialize, RUMDeserialize, PartialEq, Debug, Clone, Default)]
43pub struct HeaderConf {
44 pub logo_size: RUMString,
45 pub disable_navlinks: bool,
46 pub disable_logo: bool,
47}
48
49#[derive(RUMSerialize, RUMDeserialize, PartialEq, Debug, Clone, Default)]
50pub struct FooterConf {
51 pub socials_list: RUMString,
52 pub disable_contact_button: bool,
53}
54
55///
56/// This is a core structure in a web project using the RUMTK framework. This structure contains
57/// a series of fields that represent the web app initial state or configuration. The idea is that
58/// the web app can come bundled with a JSON config file following this structure which we can load
59/// at runtime. The settings will dictate a few key project behaviors such as properly labeling
60/// some components with the company name or use the correct language text.
61///
62#[derive(RUMSerialize, RUMDeserialize, PartialEq, Debug, Clone, Default)]
63pub struct AppConf {
64 pub title: RUMString,
65 pub description: RUMString,
66 pub company: RUMString,
67 pub copyright: RUMString,
68 pub lang: RUMString,
69 pub theme: RUMString,
70 pub custom_css: bool,
71 pub header_conf: HeaderConf,
72 pub footer_conf: FooterConf,
73
74 strings: RootNestedNestedTextMap,
75 config: NestedNestedTextMap,
76 //pub opts: TextMap,
77}
78
79impl AppConf {
80 pub fn update_site_info(
81 &mut self,
82 title: RUMString,
83 description: RUMString,
84 company: RUMString,
85 copyright: RUMString,
86 ) {
87 if !title.is_empty() {
88 self.title = title;
89 }
90 if !company.is_empty() {
91 self.company = company;
92 }
93 if !description.is_empty() {
94 self.description = description;
95 }
96 if !copyright.is_empty() {
97 self.copyright = copyright;
98 }
99 }
100
101 pub fn get_text(&self, item: &str) -> NestedTextMap {
102 match self.strings.get(&self.lang) {
103 Some(l) => match l.get(item) {
104 Some(i) => i.clone(),
105 None => NestedTextMap::default(),
106 },
107 None => NestedTextMap::default(),
108 }
109 }
110
111 pub fn get_section(&self, section: &str) -> TextMap {
112 match self.config.get(&self.lang) {
113 Some(l) => match l.get(section) {
114 Some(i) => i.clone(),
115 None => match self.config.get(DEFAULT_TEXT_ITEM) {
116 Some(l) => match l.get(section) {
117 Some(i) => i.clone(),
118 None => TextMap::default(),
119 },
120 None => TextMap::default(),
121 },
122 },
123 None => match self.config.get(DEFAULT_TEXT_ITEM) {
124 Some(l) => match l.get(section) {
125 Some(i) => i.clone(),
126 None => TextMap::default(),
127 },
128 None => TextMap::default(),
129 },
130 }
131 }
132}
133
134pub type ClipboardID = RUMString;
135///
136/// Main internal structure for holding the initial app configuration ([AppConf](crate::utils::AppConf)),
137/// the `clipboard` containing dynamically generated state ([NestedTextMap](crate::utils::NestedTextMap)),
138/// and the `jobs` field containing
139///
140#[derive(Default, Debug, Clone)]
141pub struct AppState {
142 config: AppConf,
143 clipboard: NestedTextMap,
144 jobs: RUMHashMap<RUMID, Job>,
145}
146
147pub type SharedAppState = SafeLock<AppState>;
148
149impl AppState {
150 pub fn new() -> AppState {
151 AppState {
152 config: AppConf::default(),
153 clipboard: NestedTextMap::default(),
154 jobs: RUMHashMap::default(),
155 }
156 }
157
158 pub fn new_safe() -> SharedAppState {
159 rumtk_new_lock!(AppState::new())
160 }
161
162 pub fn from_safe(conf: AppConf) -> SharedAppState {
163 rumtk_new_lock!(AppState::from(conf))
164 }
165
166 pub fn get_config(&self) -> &AppConf {
167 &self.config
168 }
169
170 pub fn get_config_mut(&mut self) -> &mut AppConf {
171 &mut self.config
172 }
173
174 pub fn has_clipboard(&self, id: &ClipboardID) -> bool {
175 self.clipboard.contains_key(id)
176 }
177
178 pub fn has_job(&self, id: &JobID) -> bool {
179 self.jobs.contains_key(id)
180 }
181
182 pub fn push_job_result(&mut self, id: &JobID, job: Job) {
183 self.jobs.insert(id.clone(), job);
184 }
185
186 pub fn push_to_clipboard(&mut self, data: TextMap) -> ClipboardID {
187 let clipboard_id = rumtk_generate_id!().to_rumstring();
188 self.clipboard.insert(clipboard_id.clone(), data);
189 clipboard_id
190 }
191
192 pub fn request_clipboard_slice(&mut self) -> ClipboardID {
193 let clipboard_id = rumtk_generate_id!().to_rumstring();
194 self.clipboard
195 .insert(clipboard_id.clone(), TextMap::default());
196 clipboard_id
197 }
198
199 pub fn pop_job(&mut self, id: &RUMID) -> Option<Job> {
200 self.jobs.remove(id)
201 }
202
203 pub fn pop_clipboard(&mut self, id: &ClipboardID) -> Option<TextMap> {
204 self.clipboard.shift_remove(id)
205 }
206}
207
208impl From<AppConf> for AppState {
209 fn from(config: AppConf) -> Self {
210 AppState {
211 config,
212 clipboard: NestedTextMap::default(),
213 jobs: RUMHashMap::default(),
214 }
215 }
216}
217
218pub type RouterAppState = State<SharedAppState>;
219
220///
221/// Load the configuration for this app at the specified path. By default, we look into `./app.json`
222/// as the location of the configuration.
223///
224/// ## Example
225/// ```
226/// use std::fs;
227/// use rumtk_core::rumtk_new_lock;
228/// use rumtk_web::{rumtk_web_save_conf, rumtk_web_load_conf, rumtk_web_get_config};
229/// use rumtk_web::{AppConf};
230/// use rumtk_core::strings::RUMString;
231///
232/// #[derive(Default)]
233/// struct Args {
234/// title: RUMString,
235/// description: RUMString,
236/// company: RUMString,
237/// copyright: RUMString,
238/// css_source_dir: RUMString,
239/// ip: RUMString,
240/// upload_limit: usize,
241/// threads: usize,
242/// skip_default_css: bool,
243/// }
244///
245/// let path = "./test_conf.json";
246///
247/// rumtk_web_save_conf!(&path);
248/// let app_state = rumtk_web_load_conf!(Args::default());
249/// let config = rumtk_web_get_config!(app_state).clone();
250///
251/// if fs::exists(&path).unwrap() {
252/// fs::remove_file(&path).unwrap();
253/// }
254///
255/// assert_eq!(config, AppConf::default(), "Configuration was not loaded properly!");
256/// ```
257///
258#[macro_export]
259macro_rules! rumtk_web_load_conf {
260 ( $args:expr ) => {{
261 rumtk_web_load_conf!($args, "./app.json")
262 }};
263 ( $args:expr, $path:expr ) => {{
264 use rumtk_core::rumtk_deserialize;
265 use rumtk_core::strings::RUMStringConversions;
266 use rumtk_core::types::RUMHashMap;
267 use $crate::AppConf;
268 use std::fs;
269
270 use $crate::rumtk_web_save_conf;
271 use $crate::utils::{AppState, TextMap};
272
273 let json = match fs::read_to_string($path) {
274 Ok(json) => json,
275 Err(err) => rumtk_web_save_conf!($path),
276 };
277
278 let mut conf: AppConf = match rumtk_deserialize!(json) {
279 Ok(conf) => conf,
280 Err(err) => panic!(
281 "The App config file in {} does not meet the expected structure. \
282 See the documentation for more information. Error: {}\n{}",
283 $path, err, json
284 ),
285 };
286 conf.update_site_info(
287 $args.title.clone(),
288 $args.description.clone(),
289 $args.company.clone(),
290 $args.copyright.clone(),
291 );
292 AppState::from_safe(conf)
293 }};
294}
295
296///
297/// Serializes [AppConf] default contents and saves it to a file on disk at a specified path or relative to
298/// the current working directory. This is done to pre-craft a default configuration skeleton so
299/// a consumer of the framework can simply update that file before testing and shipping to production.
300///
301/// By default, we generate the skeleton in `./app.json`.
302///
303/// ## Example
304/// ```
305/// use std::fs;
306/// use rumtk_core::rumtk_new_lock;
307/// use rumtk_web::rumtk_web_save_conf;
308/// use rumtk_core::strings::RUMString;
309///
310/// let path = "./test_conf.json";
311///
312/// if fs::exists(&path).unwrap() {
313/// fs::remove_file(&path).unwrap();
314/// }
315///
316/// assert!(!fs::exists(&path).unwrap(), "File was not deleted as expected!");
317///
318/// rumtk_web_save_conf!(&path);
319///
320/// assert!(fs::exists(&path).unwrap(), "File was not created as expected!");
321///
322/// if fs::exists(&path).unwrap() {
323/// fs::remove_file(&path).unwrap();
324/// }
325/// ```
326///
327#[macro_export]
328macro_rules! rumtk_web_save_conf {
329 ( ) => {{
330 rumtk_web_save_conf!("./app.json")
331 }};
332 ( $path:expr ) => {{
333 use rumtk_core::rumtk_serialize;
334 use rumtk_core::strings::RUMStringConversions;
335 use std::fs;
336 use $crate::utils::AppConf;
337
338 let json = rumtk_serialize!(AppConf::default(), true).unwrap_or_default();
339 fs::write($path, &json);
340 json
341 }};
342}
343
344///
345/// Retrieve a configuration ([AppConf]) static string. These are strings driven by the app designer's
346/// generated configuration.
347///
348#[macro_export]
349macro_rules! rumtk_web_get_config_string {
350 ( $conf:expr, $item:expr ) => {{
351 use $crate::rumtk_web_get_config;
352 use $crate::AppConf;
353 rumtk_web_get_config!($conf).get_text($item)
354 }};
355}
356
357///
358/// Retrieve a configuration ([AppConf]) item. These are strings driven by the app designer's
359/// generated configuration. Unlike [rumtk_web_get_config_string](crate::rumtk_web_get_config_string), the item
360/// retrieved here is separate from the strings section.
361///
362#[macro_export]
363macro_rules! rumtk_web_get_config_section {
364 ( $conf:expr, $item:expr ) => {{
365 use $crate::rumtk_web_get_config;
366 use $crate::AppConf;
367 rumtk_web_get_config!($conf).get_section($item)
368 }};
369}
370
371///
372/// Get field state from the configuration section of the [SharedAppState] object. The configuration
373/// is of type [AppConf].
374///
375/// ## Example
376/// ```
377/// use rumtk_core::rumtk_new_lock;
378/// use rumtk_core::strings::RUMString;
379/// use rumtk_web::{AppState, ClipboardID, SharedAppState, AppConf};
380/// use rumtk_web::{rumtk_web_set_config, rumtk_web_get_config};
381///
382/// let state = rumtk_new_lock!(AppState::new());
383///
384/// let new_lang = rumtk_web_get_config!(state).lang.clone();
385///
386/// assert_eq!(new_lang, "", "Language field in the configuration was not empty!");
387/// ```
388///
389#[macro_export]
390macro_rules! rumtk_web_get_config {
391 ( $state:expr ) => {{
392 use rumtk_core::{rumtk_lock_read};
393 rumtk_lock_read!($state.clone()).get_config()
394 }};
395}
396
397///
398/// Set field or state in the configuration section of the [SharedAppState] object. The configuration
399/// is of type [AppConf].
400///
401/// ## Example
402/// ```
403/// use rumtk_core::rumtk_new_lock;
404/// use rumtk_core::strings::RUMString;
405/// use rumtk_web::{AppState, ClipboardID, SharedAppState, AppConf};
406/// use rumtk_web::{rumtk_web_set_config, rumtk_web_get_config};
407///
408/// let state = rumtk_new_lock!(AppState::new());
409/// let lang = RUMString::from("en");
410///
411/// rumtk_web_set_config!(state).lang = RUMString::from(lang.clone());
412///
413/// let new_lang = rumtk_web_get_config!(state).lang.clone();
414///
415/// assert_eq!(new_lang, lang, "Changing the language field in the configuration was not successful!");
416/// ```
417///
418#[macro_export]
419macro_rules! rumtk_web_set_config {
420 ( $state:expr ) => {{
421 use rumtk_core::rumtk_lock_write;
422 rumtk_lock_write!($state.clone()).get_config_mut()
423 }};
424}
425
426///
427/// Facility for modifying the state in an instance of [SharedAppState].
428///
429/// ## Example
430/// ```
431/// use rumtk_core::rumtk_new_lock;
432/// use rumtk_core::strings::RUMString;
433/// use rumtk_web::{AppState, ClipboardID, SharedAppState};
434/// use rumtk_web::rumtk_web_modify_state;
435///
436/// let state = rumtk_new_lock!(AppState::new());
437/// let clipboard_id = ClipboardID::new("");
438///
439/// let item_list = rumtk_web_modify_state!(state).pop_clipboard(&clipboard_id);
440///
441/// assert_eq!(item_list, None, "A non empty item list was retrieved from the app state.");
442/// ```
443///
444#[macro_export]
445macro_rules! rumtk_web_modify_state {
446 ( $state:expr ) => {{
447 use rumtk_core::rumtk_lock_write;
448 rumtk_lock_write!($state.clone())
449 }};
450}
451
452/*
453 Default non static data to minimize allocations.
454*/
455pub const DEFAULT_TEXT: fn() -> RUMString = || RUMString::default();
456pub const DEFAULT_TEXTMAP: fn() -> TextMap = || TextMap::default();
457pub const DEFAULT_NESTEDTEXTMAP: fn() -> NestedTextMap = || NestedTextMap::default();
458pub const DEFAULT_NESTEDNESTEDTEXTMAP: fn() -> NestedNestedTextMap =
459 || NestedNestedTextMap::default();