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