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.
5 * Copyright (C) 2025  Nick Stephenson
6 * Copyright (C) 2025  Ethan Dixon
7 * Copyright (C) 2025  MedicalMasses L.L.C.
8 *
9 * This library is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public
11 * License as published by the Free Software Foundation; either
12 * version 2.1 of the License, or (at your option) any later version.
13 *
14 * This library is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17 * Lesser General Public License for more details.
18 *
19 * You should have received a copy of the GNU Lesser General Public
20 * License along with this library; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 */
23use crate::jobs::{Job, JobID};
24use crate::utils::defaults::DEFAULT_TEXT_ITEM;
25use crate::utils::types::RUMString;
26use axum::extract::State;
27use phf::OrderedMap;
28pub use phf_macros::phf_ordered_map as rumtk_create_const_ordered_map;
29use rumtk_core::rumtk_generate_id;
30use rumtk_core::strings::RUMStringConversions;
31use rumtk_core::types::{RUMDeserialize, RUMDeserializer, RUMSerialize, RUMSerializer, RUMID};
32use rumtk_core::types::{RUMHashMap, RUMOrderedMap};
33use std::sync::{Arc, RwLock};
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
44#[derive(RUMSerialize, RUMDeserialize, PartialEq, Debug, Clone, Default)]
45pub struct HeaderConf {
46    pub logo_size: RUMString,
47    pub disable_navlinks: bool,
48    pub disable_logo: bool,
49}
50
51#[derive(RUMSerialize, RUMDeserialize, PartialEq, Debug, Clone, Default)]
52pub struct FooterConf {
53    pub socials_list: RUMString,
54    pub disable_contact_button: bool,
55}
56
57///
58/// This is a core structure in a web project using the RUMTK framework. This structure contains
59/// a series of fields that represent the web app initial state or configuration. The idea is that
60/// the web app can come bundled with a JSON config file following this structure which we can load
61/// at runtime. The settings will dictate a few key project behaviors such as properly labeling
62/// some components with the company name or use the correct language text.
63///
64#[derive(RUMSerialize, RUMDeserialize, PartialEq, Debug, Clone, Default)]
65pub struct AppConf {
66    pub title: RUMString,
67    pub description: RUMString,
68    pub company: RUMString,
69    pub copyright: RUMString,
70    pub lang: RUMString,
71    pub theme: RUMString,
72    pub custom_css: bool,
73    pub header_conf: HeaderConf,
74    pub footer_conf: FooterConf,
75
76    strings: RootNestedNestedTextMap,
77    config: NestedNestedTextMap,
78    //pub opts: TextMap,
79}
80
81impl AppConf {
82    pub fn update_site_info(
83        &mut self,
84        title: RUMString,
85        description: RUMString,
86        company: RUMString,
87        copyright: RUMString,
88    ) {
89        if !title.is_empty() {
90            self.title = title;
91        }
92        if !company.is_empty() {
93            self.company = company;
94        }
95        if !description.is_empty() {
96            self.description = description;
97        }
98        if !copyright.is_empty() {
99            self.copyright = copyright;
100        }
101    }
102
103    pub fn get_text(&self, item: &str) -> NestedTextMap {
104        match self.strings.get(&self.lang) {
105            Some(l) => match l.get(item) {
106                Some(i) => i.clone(),
107                None => NestedTextMap::default(),
108            },
109            None => NestedTextMap::default(),
110        }
111    }
112
113    pub fn get_conf(&self, section: &str) -> TextMap {
114        match self.config.get(&self.lang) {
115            Some(l) => match l.get(section) {
116                Some(i) => i.clone(),
117                None => match self.config.get(DEFAULT_TEXT_ITEM) {
118                    Some(l) => match l.get(section) {
119                        Some(i) => i.clone(),
120                        None => TextMap::default(),
121                    },
122                    None => TextMap::default(),
123                },
124            },
125            None => match self.config.get(DEFAULT_TEXT_ITEM) {
126                Some(l) => match l.get(section) {
127                    Some(i) => i.clone(),
128                    None => TextMap::default(),
129                },
130                None => TextMap::default(),
131            },
132        }
133    }
134}
135
136pub type ClipboardID = RUMString;
137///
138/// Main internal structure for holding the initial app configuration ([AppConf](crate::utils::AppConf)),
139/// the `clipboard` containing dynamically generated state ([NestedTextMap](crate::utils::NestedTextMap)),
140/// and the `jobs` field containing
141///
142pub struct AppState {
143    config: AppConf,
144    clipboard: NestedTextMap,
145    jobs: RUMHashMap<RUMID, Job>,
146}
147
148pub type SafeAppState = Arc<RwLock<AppState>>;
149
150impl AppState {
151    pub fn new() -> AppState {
152        AppState {
153            config: AppConf::default(),
154            clipboard: NestedTextMap::default(),
155            jobs: RUMHashMap::default(),
156        }
157    }
158
159    pub fn new_safe() -> SafeAppState {
160        Arc::new(RwLock::new(AppState::new()))
161    }
162
163    pub fn from_safe(conf: AppConf) -> SafeAppState {
164        Arc::new(RwLock::new(AppState::from(conf)))
165    }
166
167    pub fn get_config(&self) -> &AppConf {
168        &self.config
169    }
170
171    pub fn get_config_mut(&mut self) -> &mut AppConf {
172        &mut self.config
173    }
174
175    pub fn has_clipboard(&self, id: &ClipboardID) -> bool {
176        self.clipboard.contains_key(id)
177    }
178
179    pub fn has_job(&self, id: &JobID) -> bool {
180        self.jobs.contains_key(id)
181    }
182
183    pub fn push_job_result(&mut self, id: &JobID, job: Job) {
184        self.jobs.insert(id.clone(), job);
185    }
186
187    pub fn push_to_clipboard(&mut self, data: TextMap) -> ClipboardID {
188        let clipboard_id = rumtk_generate_id!().to_rumstring();
189        self.clipboard.insert(clipboard_id.clone(), data);
190        clipboard_id
191    }
192
193    pub fn request_clipboard_slice(&mut self) -> ClipboardID {
194        let clipboard_id = rumtk_generate_id!().to_rumstring();
195        self.clipboard
196            .insert(clipboard_id.clone(), TextMap::default());
197        clipboard_id
198    }
199
200    pub fn pop_job(&mut self, id: &RUMID) -> Option<Job> {
201        self.jobs.remove(id)
202    }
203
204    pub fn pop_clipboard(&mut self, id: &ClipboardID) -> Option<TextMap> {
205        self.clipboard.shift_remove(id)
206    }
207}
208
209impl From<AppConf> for AppState {
210    fn from(config: AppConf) -> Self {
211        AppState {
212            config,
213            clipboard: NestedTextMap::default(),
214            jobs: RUMHashMap::default(),
215        }
216    }
217}
218
219pub type SharedAppState = Arc<RwLock<AppState>>;
220pub type RouterAppState = State<Arc<RwLock<AppState>>>;
221
222#[macro_export]
223macro_rules! rumtk_web_load_conf {
224    ( $args:expr ) => {{
225        rumtk_web_load_conf!($args, "./app.json")
226    }};
227    ( $args:expr, $path:expr ) => {{
228        use rumtk_core::rumtk_deserialize;
229        use rumtk_core::strings::RUMStringConversions;
230        use rumtk_core::types::RUMHashMap;
231        use std::fs;
232
233        use $crate::rumtk_web_save_conf;
234        use $crate::utils::{AppConf, AppState, TextMap};
235
236        let json = match fs::read_to_string($path) {
237            Ok(json) => json,
238            Err(err) => rumtk_web_save_conf!($path),
239        };
240
241        let mut conf: AppConf = match rumtk_deserialize!(json) {
242            Ok(conf) => conf,
243            Err(err) => panic!(
244                "The App config file in {} does not meet the expected structure. \
245                    See the documentation for more information. Error: {}\n{}",
246                $path, err, json
247            ),
248        };
249        conf.update_site_info(
250            $args.title.clone(),
251            $args.description.clone(),
252            $args.company.clone(),
253            $args.copyright.clone(),
254        );
255        AppState::from_safe(conf)
256    }};
257}
258
259#[macro_export]
260macro_rules! rumtk_web_save_conf {
261    ( $path:expr ) => {{
262        use rumtk_core::rumtk_serialize;
263        use rumtk_core::strings::RUMStringConversions;
264        use std::fs;
265        use $crate::utils::AppConf;
266
267        let json = rumtk_serialize!(AppConf::default(), true)?;
268        fs::write($path, &json);
269        json
270    }};
271}
272
273#[macro_export]
274macro_rules! rumtk_web_get_string {
275    ( $conf:expr, $item:expr ) => {{
276        let owned_state = $conf.read().expect("Lock failure");
277        owned_state.get_config().get_text($item)
278    }};
279}
280
281#[macro_export]
282macro_rules! rumtk_web_get_conf {
283    ( $conf:expr, $item:expr ) => {{
284        let owned_state = $conf.read().expect("Lock failure");
285        owned_state.get_config().get_conf($item)
286    }};
287}
288
289/*
290   Default non static data to minimize allocations.
291*/
292pub const DEFAULT_TEXT: fn() -> RUMString = || RUMString::default();
293pub const DEFAULT_TEXTMAP: fn() -> TextMap = || TextMap::default();
294pub const DEFAULT_NESTEDTEXTMAP: fn() -> NestedTextMap = || NestedTextMap::default();
295pub const DEFAULT_NESTEDNESTEDTEXTMAP: fn() -> NestedNestedTextMap =
296    || NestedNestedTextMap::default();