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