use crate::{ util::*, ConfigurationBuilder, ConfigurationPath, ConfigurationProvider, ConfigurationSource, FileSource, LoadError, LoadResult, Value, }; use serde_json::{map::Map, Value as JsonValue}; use std::collections::HashMap; use std::fs; use std::sync::{Arc, RwLock}; use tokens::{ChangeToken, FileChangeToken, SharedChangeToken, SingleChangeToken, Subscription}; #[derive(Default)] struct JsonVisitor { data: HashMap, paths: Vec, } impl JsonVisitor { fn visit(mut self, root: &Map) -> HashMap { self.visit_element(root); self.data.shrink_to_fit(); self.data } fn visit_element(&mut self, element: &Map) { if element.is_empty() { if let Some(key) = self.paths.last() { self.data .insert(key.to_uppercase(), (to_pascal_case(key), String::new().into())); } } else { for (name, value) in element { self.enter_context(to_pascal_case(name)); self.visit_value(value); self.exit_context(); } } } fn visit_value(&mut self, value: &JsonValue) { match value { JsonValue::Object(ref element) => self.visit_element(element), JsonValue::Array(array) => { for (index, element) in array.iter().enumerate() { self.enter_context(index.to_string()); self.visit_value(element); self.exit_context(); } } JsonValue::Bool(value) => self.add_value(value), JsonValue::Null => self.add_value(String::new()), JsonValue::Number(value) => self.add_value(value), JsonValue::String(value) => self.add_value(value), } } fn add_value(&mut self, value: T) { let key = self.paths.last().unwrap().to_string(); self.data .insert(key.to_uppercase(), (key, value.to_string().into())); } fn enter_context(&mut self, context: String) { if self.paths.is_empty() { self.paths.push(context); return; } let path = ConfigurationPath::combine(&[&self.paths[self.paths.len() - 1], &context]); self.paths.push(path); } fn exit_context(&mut self) { self.paths.pop(); } } struct InnerProvider { file: FileSource, data: RwLock>, token: RwLock>, } impl InnerProvider { fn new(file: FileSource) -> Self { Self { file, data: RwLock::new(HashMap::with_capacity(0)), token: Default::default(), } } fn load(&self, reload: bool) -> LoadResult { if !self.file.path.is_file() { if self.file.optional || reload { let mut data = self.data.write().unwrap(); if !data.is_empty() { *data = HashMap::with_capacity(0); } return Ok(()); } else { return Err(LoadError::File { message: format!( "The configuration file '{}' was not found and is not optional.", self.file.path.display() ), path: self.file.path.clone(), }); } } // REF: https://docs.serde.rs/serde_json/de/fn.from_reader.html let content = fs::read(&self.file.path).unwrap(); let json: JsonValue = serde_json::from_slice(&content).unwrap(); if let Some(root) = json.as_object() { let visitor = JsonVisitor::default(); let data = visitor.visit(root); *self.data.write().unwrap() = data; } else if reload { *self.data.write().unwrap() = HashMap::with_capacity(0); } else { return Err(LoadError::File { message: format!( "Top-level JSON element must be an object. Instead, '{}' was found.", match json { JsonValue::Array(_) => "array", JsonValue::Bool(_) => "Boolean", JsonValue::Null => "null", JsonValue::Number(_) => "number", JsonValue::String(_) => "string", _ => unreachable!(), } ), path: self.file.path.clone(), }); } let previous = std::mem::replace( &mut *self.token.write().unwrap(), SharedChangeToken::default(), ); previous.notify(); Ok(()) } fn get(&self, key: &str) -> Option { self.data .read() .unwrap() .get(&key.to_uppercase()) .map(|t| t.1.clone()) } fn reload_token(&self) -> Box { Box::new(self.token.read().unwrap().clone()) } fn child_keys(&self, earlier_keys: &mut Vec, parent_path: Option<&str>) { let data = self.data.read().unwrap(); accumulate_child_keys(&data, earlier_keys, parent_path) } } /// Represents a [`ConfigurationProvider`](crate::ConfigurationProvider) for `*.json` files. pub struct JsonConfigurationProvider { inner: Arc, _subscription: Option>, } impl JsonConfigurationProvider { /// Initializes a new `*.json` file configuration provider. /// /// # Arguments /// /// * `file` - The `*.json` [`FileSource`](crate::FileSource) information pub fn new(file: FileSource) -> Self { let path = file.path.clone(); let inner = Arc::new(InnerProvider::new(file)); let subscription: Option> = if inner.file.reload_on_change { Some(Box::new(tokens::on_change( move || FileChangeToken::new(path.clone()), |state| { let provider = state.unwrap(); std::thread::sleep(provider.file.reload_delay); provider.load(true).ok(); }, Some(inner.clone()) ))) } else { None }; Self { inner, _subscription: subscription, } } } impl ConfigurationProvider for JsonConfigurationProvider { fn get(&self, key: &str) -> Option { self.inner.get(key) } fn reload_token(&self) -> Box { self.inner.reload_token() } fn load(&mut self) -> LoadResult { self.inner.load(false) } fn child_keys(&self, earlier_keys: &mut Vec, parent_path: Option<&str>) { self.inner.child_keys(earlier_keys, parent_path) } } /// Represents a [`ConfigurationSource`](crate::ConfigurationSource) for `*.json` files. pub struct JsonConfigurationSource { file: FileSource, } impl JsonConfigurationSource { /// Initializes a new `*.json` file configuration source. /// /// # Arguments /// /// * `file` - The `*.json` [`FileSource`](crate::FileSource) information pub fn new(file: FileSource) -> Self { Self { file } } } impl ConfigurationSource for JsonConfigurationSource { fn build(&self, _builder: &dyn ConfigurationBuilder) -> Box { Box::new(JsonConfigurationProvider::new(self.file.clone())) } } pub mod ext { use super::*; /// Defines extension methods for [`ConfigurationBuilder`](crate::ConfigurationBuilder). pub trait JsonConfigurationExtensions { /// Adds a `*.json` file as a configuration source. /// /// # Arguments /// /// * `file` - The `*.json` [`FileSource`](crate::FileSource) information fn add_json_file>(&mut self, file: T) -> &mut Self; } impl JsonConfigurationExtensions for dyn ConfigurationBuilder + '_ { fn add_json_file>(&mut self, file: T) -> &mut Self { self.add(Box::new(JsonConfigurationSource::new(file.into()))); self } } impl JsonConfigurationExtensions for T { fn add_json_file>(&mut self, file: F) -> &mut Self { self.add(Box::new(JsonConfigurationSource::new(file.into()))); self } } }