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