Skip to main content

config/
json.rs

1use crate::{
2    util::*, ConfigurationBuilder, ConfigurationPath, ConfigurationProvider, ConfigurationSource, FileSource,
3    LoadError, LoadResult, Value,
4};
5use serde_json::{map::Map, Value as JsonValue};
6use std::collections::HashMap;
7use std::fs;
8use std::sync::{Arc, RwLock};
9use tokens::{ChangeToken, FileChangeToken, SharedChangeToken, SingleChangeToken, Subscription};
10
11#[derive(Default)]
12struct JsonVisitor {
13    data: HashMap<String, (String, String)>,
14    paths: Vec<String>,
15}
16
17impl JsonVisitor {
18    fn visit(mut self, root: &Map<String, JsonValue>) -> HashMap<String, (String, String)> {
19        self.visit_element(root);
20        self.data.shrink_to_fit();
21        self.data
22    }
23
24    fn visit_element(&mut self, element: &Map<String, JsonValue>) {
25        if element.is_empty() {
26            if let Some(key) = self.paths.last() {
27                self.data
28                    .insert(key.to_uppercase(), (to_pascal_case(key), String::new()));
29            }
30        } else {
31            for (name, value) in element {
32                self.enter_context(to_pascal_case(name));
33                self.visit_value(value);
34                self.exit_context();
35            }
36        }
37    }
38
39    fn visit_value(&mut self, value: &JsonValue) {
40        match value {
41            JsonValue::Object(ref element) => self.visit_element(element),
42            JsonValue::Array(array) => {
43                for (index, element) in array.iter().enumerate() {
44                    self.enter_context(index.to_string());
45                    self.visit_value(element);
46                    self.exit_context();
47                }
48            }
49            JsonValue::Bool(value) => self.add_value(value),
50            JsonValue::Null => self.add_value(String::new()),
51            JsonValue::Number(value) => self.add_value(value),
52            JsonValue::String(value) => self.add_value(value),
53        }
54    }
55
56    fn add_value<T: ToString>(&mut self, value: T) {
57        let key = self.paths.last().unwrap().to_string();
58        self.data.insert(key.to_uppercase(), (key, value.to_string()));
59    }
60
61    fn enter_context(&mut self, context: String) {
62        if self.paths.is_empty() {
63            self.paths.push(context);
64            return;
65        }
66
67        let path = ConfigurationPath::combine(&[&self.paths[self.paths.len() - 1], &context]);
68        self.paths.push(path);
69    }
70
71    fn exit_context(&mut self) {
72        self.paths.pop();
73    }
74}
75
76struct InnerProvider {
77    file: FileSource,
78    data: RwLock<HashMap<String, (String, String)>>,
79    token: RwLock<SharedChangeToken<SingleChangeToken>>,
80}
81
82impl InnerProvider {
83    fn new(file: FileSource) -> Self {
84        Self {
85            file,
86            data: RwLock::new(HashMap::with_capacity(0)),
87            token: Default::default(),
88        }
89    }
90
91    fn load(&self, reload: bool) -> LoadResult {
92        if !self.file.path.is_file() {
93            if self.file.optional || reload {
94                let mut data = self.data.write().unwrap();
95                if !data.is_empty() {
96                    *data = HashMap::with_capacity(0);
97                }
98
99                return Ok(());
100            } else {
101                return Err(LoadError::File {
102                    message: format!(
103                        "The configuration file '{}' was not found and is not optional.",
104                        self.file.path.display()
105                    ),
106                    path: self.file.path.clone(),
107                });
108            }
109        }
110
111        // REF: https://docs.serde.rs/serde_json/de/fn.from_reader.html
112        let content = fs::read(&self.file.path).unwrap();
113        let json: JsonValue = serde_json::from_slice(&content).unwrap();
114
115        if let Some(root) = json.as_object() {
116            let visitor = JsonVisitor::default();
117            let data = visitor.visit(root);
118            *self.data.write().unwrap() = data;
119        } else if reload {
120            *self.data.write().unwrap() = HashMap::with_capacity(0);
121        } else {
122            return Err(LoadError::File {
123                message: format!(
124                    "Top-level JSON element must be an object. Instead, '{}' was found.",
125                    match json {
126                        JsonValue::Array(_) => "array",
127                        JsonValue::Bool(_) => "Boolean",
128                        JsonValue::Null => "null",
129                        JsonValue::Number(_) => "number",
130                        JsonValue::String(_) => "string",
131                        _ => unreachable!(),
132                    }
133                ),
134                path: self.file.path.clone(),
135            });
136        }
137
138        let previous = std::mem::take(&mut *self.token.write().unwrap());
139
140        previous.notify();
141        Ok(())
142    }
143
144    fn get(&self, key: &str) -> Option<Value> {
145        self.data
146            .read()
147            .unwrap()
148            .get(&key.to_uppercase())
149            .map(|t| t.1.clone().into())
150    }
151
152    fn reload_token(&self) -> Box<dyn ChangeToken> {
153        Box::new(self.token.read().unwrap().clone())
154    }
155
156    fn child_keys(&self, earlier_keys: &mut Vec<String>, parent_path: Option<&str>) {
157        let data = self.data.read().unwrap();
158        accumulate_child_keys(&data, earlier_keys, parent_path)
159    }
160}
161
162/// Represents a [`ConfigurationProvider`](crate::ConfigurationProvider) for `*.json` files.
163pub struct JsonConfigurationProvider {
164    inner: Arc<InnerProvider>,
165    _subscription: Option<Box<dyn Subscription>>,
166}
167
168impl JsonConfigurationProvider {
169    /// Initializes a new `*.json` file configuration provider.
170    ///
171    /// # Arguments
172    ///
173    /// * `file` - The `*.json` [`FileSource`](crate::FileSource) information
174    pub fn new(file: FileSource) -> Self {
175        let path = file.path.clone();
176        let inner = Arc::new(InnerProvider::new(file));
177        let subscription: Option<Box<dyn Subscription>> = if inner.file.reload_on_change {
178            Some(Box::new(tokens::on_change(
179                move || FileChangeToken::new(path.clone()),
180                |state| {
181                    let provider = state.unwrap();
182                    std::thread::sleep(provider.file.reload_delay);
183                    provider.load(true).ok();
184                },
185                Some(inner.clone()),
186            )))
187        } else {
188            None
189        };
190
191        Self {
192            inner,
193            _subscription: subscription,
194        }
195    }
196}
197
198impl ConfigurationProvider for JsonConfigurationProvider {
199    fn get(&self, key: &str) -> Option<Value> {
200        self.inner.get(key)
201    }
202
203    fn reload_token(&self) -> Box<dyn ChangeToken> {
204        self.inner.reload_token()
205    }
206
207    fn load(&mut self) -> LoadResult {
208        self.inner.load(false)
209    }
210
211    fn child_keys(&self, earlier_keys: &mut Vec<String>, parent_path: Option<&str>) {
212        self.inner.child_keys(earlier_keys, parent_path)
213    }
214}
215
216/// Represents a [`ConfigurationSource`](crate::ConfigurationSource) for `*.json` files.
217pub struct JsonConfigurationSource {
218    file: FileSource,
219}
220
221impl JsonConfigurationSource {
222    /// Initializes a new `*.json` file configuration source.
223    ///
224    /// # Arguments
225    ///
226    /// * `file` - The `*.json` [`FileSource`](crate::FileSource) information
227    pub fn new(file: FileSource) -> Self {
228        Self { file }
229    }
230}
231
232impl ConfigurationSource for JsonConfigurationSource {
233    fn build(&self, _builder: &dyn ConfigurationBuilder) -> Box<dyn ConfigurationProvider> {
234        Box::new(JsonConfigurationProvider::new(self.file.clone()))
235    }
236}
237
238pub mod ext {
239
240    use super::*;
241
242    /// Defines extension methods for [`ConfigurationBuilder`](crate::ConfigurationBuilder).
243    pub trait JsonConfigurationExtensions {
244        /// Adds a `*.json` file as a configuration source.
245        ///
246        /// # Arguments
247        ///
248        /// * `file` - The `*.json` [`FileSource`](crate::FileSource) information
249        fn add_json_file<T: Into<FileSource>>(&mut self, file: T) -> &mut Self;
250    }
251
252    impl JsonConfigurationExtensions for dyn ConfigurationBuilder + '_ {
253        fn add_json_file<T: Into<FileSource>>(&mut self, file: T) -> &mut Self {
254            self.add(Box::new(JsonConfigurationSource::new(file.into())));
255            self
256        }
257    }
258
259    impl<T: ConfigurationBuilder> JsonConfigurationExtensions for T {
260        fn add_json_file<F: Into<FileSource>>(&mut self, file: F) -> &mut Self {
261            self.add(Box::new(JsonConfigurationSource::new(file.into())));
262            self
263        }
264    }
265}