Skip to main content

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();