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