♻️ Integra config-rs sólo para archivos TOML
This commit is contained in:
parent
4daab33499
commit
104efd116f
10 changed files with 2185 additions and 0 deletions
215
pagetop/src/config/data.rs
Normal file
215
pagetop/src/config/data.rs
Normal file
|
|
@ -0,0 +1,215 @@
|
||||||
|
use crate::config::error::*;
|
||||||
|
use crate::config::path;
|
||||||
|
use crate::config::source::Source;
|
||||||
|
use crate::config::value::{Table, Value};
|
||||||
|
|
||||||
|
use serde::de::Deserialize;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum ConfigKind {
|
||||||
|
// A mutable configuration. This is the default.
|
||||||
|
Mutable {
|
||||||
|
defaults: HashMap<path::Expression, Value>,
|
||||||
|
overrides: HashMap<path::Expression, Value>,
|
||||||
|
sources: Vec<Box<dyn Source + Send + Sync>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConfigKind {
|
||||||
|
fn default() -> Self {
|
||||||
|
ConfigKind::Mutable {
|
||||||
|
defaults: HashMap::new(),
|
||||||
|
overrides: HashMap::new(),
|
||||||
|
sources: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A prioritized configuration repository. It maintains a set of configuration sources, fetches
|
||||||
|
/// values to populate those, and provides them according to the source's priority.
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub struct ConfigData {
|
||||||
|
kind: ConfigKind,
|
||||||
|
/// Root of the cached configuration.
|
||||||
|
pub cache: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigData {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
kind: ConfigKind::default(),
|
||||||
|
// Config root should be instantiated as an empty table to avoid deserialization errors.
|
||||||
|
cache: Value::new(None, Table::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge in a configuration property source.
|
||||||
|
pub fn merge<T>(&mut self, source: T) -> Result<&mut ConfigData>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
T: Source + Send + Sync,
|
||||||
|
{
|
||||||
|
match self.kind {
|
||||||
|
ConfigKind::Mutable {
|
||||||
|
ref mut sources, ..
|
||||||
|
} => {
|
||||||
|
sources.push(Box::new(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Merge in a configuration property source.
|
||||||
|
pub fn with_merged<T>(mut self, source: T) -> Result<Self>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
T: Source + Send + Sync,
|
||||||
|
{
|
||||||
|
match self.kind {
|
||||||
|
ConfigKind::Mutable {
|
||||||
|
ref mut sources, ..
|
||||||
|
} => {
|
||||||
|
sources.push(Box::new(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.refresh()?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh the configuration cache with fresh data from added sources.
|
||||||
|
///
|
||||||
|
/// Configuration is automatically refreshed after a mutation operation (`set`, `merge`,
|
||||||
|
/// `set_default`, etc.).
|
||||||
|
pub fn refresh(&mut self) -> Result<&mut ConfigData> {
|
||||||
|
self.cache = match self.kind {
|
||||||
|
// TODO: We need to actually merge in all the stuff.
|
||||||
|
ConfigKind::Mutable {
|
||||||
|
ref overrides,
|
||||||
|
ref sources,
|
||||||
|
ref defaults,
|
||||||
|
} => {
|
||||||
|
let mut cache: Value = HashMap::<String, Value>::new().into();
|
||||||
|
|
||||||
|
// Add defaults.
|
||||||
|
for (key, val) in defaults {
|
||||||
|
key.set(&mut cache, val.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sources.
|
||||||
|
sources.collect_to(&mut cache)?;
|
||||||
|
|
||||||
|
// Add overrides.
|
||||||
|
for (key, val) in overrides {
|
||||||
|
key.set(&mut cache, val.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
cache
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_default<T>(&mut self, key: &str, value: T) -> Result<&mut ConfigData>
|
||||||
|
where
|
||||||
|
T: Into<Value>,
|
||||||
|
{
|
||||||
|
match self.kind {
|
||||||
|
ConfigKind::Mutable {
|
||||||
|
ref mut defaults, ..
|
||||||
|
} => {
|
||||||
|
defaults.insert(key.parse()?, value.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set<T>(&mut self, key: &str, value: T) -> Result<&mut ConfigData>
|
||||||
|
where
|
||||||
|
T: Into<Value>,
|
||||||
|
{
|
||||||
|
match self.kind {
|
||||||
|
ConfigKind::Mutable {
|
||||||
|
ref mut overrides, ..
|
||||||
|
} => {
|
||||||
|
overrides.insert(key.parse()?, value.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_once(&mut self, key: &str, value: Value) -> Result<()> {
|
||||||
|
let expr: path::Expression = key.parse()?;
|
||||||
|
|
||||||
|
// Traverse the cache using the path to (possibly) retrieve a value.
|
||||||
|
if let Some(ref mut val) = expr.get_mut(&mut self.cache) {
|
||||||
|
**val = value;
|
||||||
|
} else {
|
||||||
|
expr.set(&mut self.cache, value);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get<'de, T: Deserialize<'de>>(&self, key: &str) -> Result<T> {
|
||||||
|
// Parse the key into a path expression.
|
||||||
|
let expr: path::Expression = key.parse()?;
|
||||||
|
|
||||||
|
// Traverse the cache using the path to (possibly) retrieve a value.
|
||||||
|
let value = expr.get(&self.cache).cloned();
|
||||||
|
|
||||||
|
match value {
|
||||||
|
Some(value) => {
|
||||||
|
// Deserialize the received value into the requested type.
|
||||||
|
T::deserialize(value).map_err(|e| e.extend_with_key(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
None => Err(ConfigError::NotFound(key.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_str(&self, key: &str) -> Result<String> {
|
||||||
|
self.get(key).and_then(Value::into_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_int(&self, key: &str) -> Result<i64> {
|
||||||
|
self.get(key).and_then(Value::into_int)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_float(&self, key: &str) -> Result<f64> {
|
||||||
|
self.get(key).and_then(Value::into_float)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_bool(&self, key: &str) -> Result<bool> {
|
||||||
|
self.get(key).and_then(Value::into_bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_table(&self, key: &str) -> Result<HashMap<String, Value>> {
|
||||||
|
self.get(key).and_then(Value::into_table)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_array(&self, key: &str) -> Result<Vec<Value>> {
|
||||||
|
self.get(key).and_then(Value::into_array)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to deserialize the entire configuration into the requested type.
|
||||||
|
pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result<T> {
|
||||||
|
T::deserialize(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for ConfigData {
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
Box::new((*self).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(&self) -> Result<HashMap<String, Value>> {
|
||||||
|
self.cache.clone().into_table()
|
||||||
|
}
|
||||||
|
}
|
||||||
462
pagetop/src/config/de.rs
Normal file
462
pagetop/src/config/de.rs
Normal file
|
|
@ -0,0 +1,462 @@
|
||||||
|
use crate::config::data::ConfigData;
|
||||||
|
use crate::config::error::*;
|
||||||
|
use crate::config::value::{Table, Value, ValueKind};
|
||||||
|
|
||||||
|
use serde::de;
|
||||||
|
use serde::forward_to_deserialize_any;
|
||||||
|
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use std::iter::Enumerate;
|
||||||
|
|
||||||
|
impl<'de> de::Deserializer<'de> for Value {
|
||||||
|
type Error = ConfigError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
// Deserialize based on the underlying type.
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Nil => visitor.visit_unit(),
|
||||||
|
ValueKind::Integer(i) => visitor.visit_i64(i),
|
||||||
|
ValueKind::Boolean(b) => visitor.visit_bool(b),
|
||||||
|
ValueKind::Float(f) => visitor.visit_f64(f),
|
||||||
|
ValueKind::String(s) => visitor.visit_string(s),
|
||||||
|
ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)),
|
||||||
|
ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_bool(self.into_bool()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_i8(self.into_int()? as i8)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_i16(self.into_int()? as i16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_i32(self.into_int()? as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_i64(self.into_int()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u8(self.into_int()? as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u16(self.into_int()? as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u32(self.into_int()? as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u64(self.into_int()? as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_f32(self.into_float()? as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_f64(self.into_float()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_string(self.into_str()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_string(self.into_str()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
// Match an explicit nil as None and everything else as Some.
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Nil => visitor.visit_none(),
|
||||||
|
_ => visitor.visit_some(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_newtype_struct<V>(self, _name: &'static str, visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_newtype_struct(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_enum<V>(
|
||||||
|
self,
|
||||||
|
name: &'static str,
|
||||||
|
variants: &'static [&'static str],
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_enum(EnumAccess {
|
||||||
|
value: self,
|
||||||
|
name,
|
||||||
|
variants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_to_deserialize_any! {
|
||||||
|
char seq
|
||||||
|
bytes byte_buf map struct unit
|
||||||
|
identifier ignored_any unit_struct tuple_struct tuple
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StrDeserializer<'a>(&'a str);
|
||||||
|
|
||||||
|
impl<'de, 'a> de::Deserializer<'de> for StrDeserializer<'a> {
|
||||||
|
type Error = ConfigError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_any<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_str(self.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_to_deserialize_any! {
|
||||||
|
bool u8 u16 u32 u64 i8 i16 i32 i64 f32 f64 char str string seq
|
||||||
|
bytes byte_buf map struct unit enum newtype_struct
|
||||||
|
identifier ignored_any unit_struct tuple_struct tuple option
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SeqAccess {
|
||||||
|
elements: Enumerate<::std::vec::IntoIter<Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SeqAccess {
|
||||||
|
fn new(elements: Vec<Value>) -> Self {
|
||||||
|
SeqAccess {
|
||||||
|
elements: elements.into_iter().enumerate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> de::SeqAccess<'de> for SeqAccess {
|
||||||
|
type Error = ConfigError;
|
||||||
|
|
||||||
|
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>>
|
||||||
|
where
|
||||||
|
T: de::DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
match self.elements.next() {
|
||||||
|
Some((idx, value)) => seed
|
||||||
|
.deserialize(value)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| e.prepend_index(idx)),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size_hint(&self) -> Option<usize> {
|
||||||
|
match self.elements.size_hint() {
|
||||||
|
(lower, Some(upper)) if lower == upper => Some(upper),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MapAccess {
|
||||||
|
elements: VecDeque<(String, Value)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MapAccess {
|
||||||
|
fn new(table: HashMap<String, Value>) -> Self {
|
||||||
|
MapAccess {
|
||||||
|
elements: table.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> de::MapAccess<'de> for MapAccess {
|
||||||
|
type Error = ConfigError;
|
||||||
|
|
||||||
|
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>>
|
||||||
|
where
|
||||||
|
K: de::DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
if let Some(&(ref key_s, _)) = self.elements.front() {
|
||||||
|
let key_de = Value::new(None, key_s as &str);
|
||||||
|
let key = de::DeserializeSeed::deserialize(seed, key_de)?;
|
||||||
|
|
||||||
|
Ok(Some(key))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
let (key, value) = self.elements.pop_front().unwrap();
|
||||||
|
de::DeserializeSeed::deserialize(seed, value).map_err(|e| e.prepend_key(key))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EnumAccess {
|
||||||
|
value: Value,
|
||||||
|
name: &'static str,
|
||||||
|
variants: &'static [&'static str],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnumAccess {
|
||||||
|
fn variant_deserializer(&self, name: &str) -> Result<StrDeserializer> {
|
||||||
|
self.variants
|
||||||
|
.iter()
|
||||||
|
.find(|&&s| s == name)
|
||||||
|
.map(|&s| StrDeserializer(s))
|
||||||
|
.ok_or_else(|| self.no_constructor_error(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_deserializer(&self, table: &Table) -> Result<StrDeserializer> {
|
||||||
|
if table.len() == 1 {
|
||||||
|
self.variant_deserializer(table.iter().next().unwrap().0)
|
||||||
|
} else {
|
||||||
|
Err(self.structural_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn no_constructor_error(&self, supposed_variant: &str) -> ConfigError {
|
||||||
|
ConfigError::Message(format!(
|
||||||
|
"enum {} does not have variant constructor {}",
|
||||||
|
self.name, supposed_variant
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn structural_error(&self) -> ConfigError {
|
||||||
|
ConfigError::Message(format!(
|
||||||
|
"value of enum {} should be represented by either string or table with exactly one key",
|
||||||
|
self.name
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> de::EnumAccess<'de> for EnumAccess {
|
||||||
|
type Error = ConfigError;
|
||||||
|
type Variant = Self;
|
||||||
|
|
||||||
|
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant)>
|
||||||
|
where
|
||||||
|
V: de::DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
let value = {
|
||||||
|
let deserializer = match self.value.kind {
|
||||||
|
ValueKind::String(ref s) => self.variant_deserializer(s),
|
||||||
|
ValueKind::Table(ref t) => self.table_deserializer(&t),
|
||||||
|
_ => Err(self.structural_error()),
|
||||||
|
}?;
|
||||||
|
seed.deserialize(deserializer)?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((value, self))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> de::VariantAccess<'de> for EnumAccess {
|
||||||
|
type Error = ConfigError;
|
||||||
|
|
||||||
|
fn unit_variant(self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value>
|
||||||
|
where
|
||||||
|
T: de::DeserializeSeed<'de>,
|
||||||
|
{
|
||||||
|
match self.value.kind {
|
||||||
|
ValueKind::Table(t) => seed.deserialize(t.into_iter().next().unwrap().1),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tuple_variant<V>(self, _len: usize, visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
match self.value.kind {
|
||||||
|
ValueKind::Table(t) => {
|
||||||
|
de::Deserializer::deserialize_seq(t.into_iter().next().unwrap().1, visitor)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn struct_variant<V>(self, _fields: &'static [&'static str], visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
match self.value.kind {
|
||||||
|
ValueKind::Table(t) => {
|
||||||
|
de::Deserializer::deserialize_map(t.into_iter().next().unwrap().1, visitor)
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> de::Deserializer<'de> for ConfigData {
|
||||||
|
type Error = ConfigError;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
// Deserialize based on the underlying type.
|
||||||
|
match self.cache.kind {
|
||||||
|
ValueKind::Nil => visitor.visit_unit(),
|
||||||
|
ValueKind::Integer(i) => visitor.visit_i64(i),
|
||||||
|
ValueKind::Boolean(b) => visitor.visit_bool(b),
|
||||||
|
ValueKind::Float(f) => visitor.visit_f64(f),
|
||||||
|
ValueKind::String(s) => visitor.visit_string(s),
|
||||||
|
ValueKind::Array(values) => visitor.visit_seq(SeqAccess::new(values)),
|
||||||
|
ValueKind::Table(map) => visitor.visit_map(MapAccess::new(map)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_bool<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_bool(self.cache.into_bool()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_i8(self.cache.into_int()? as i8)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_i16(self.cache.into_int()? as i16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_i32(self.cache.into_int()? as i32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_i64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_i64(self.cache.into_int()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u8<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u8(self.cache.into_int()? as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u16<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u16(self.cache.into_int()? as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u32(self.cache.into_int()? as u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_u64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
// FIXME: This should *fail* if the value does not fit in the requets integer type.
|
||||||
|
visitor.visit_u64(self.cache.into_int()? as u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_f32<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_f32(self.cache.into_float()? as f32)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_f64<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_f64(self.cache.into_float()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_str<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_string(self.cache.into_str()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_string<V: de::Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
|
visitor.visit_string(self.cache.into_str()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
// Match an explicit nil as None and everything else as Some.
|
||||||
|
match self.cache.kind {
|
||||||
|
ValueKind::Nil => visitor.visit_none(),
|
||||||
|
_ => visitor.visit_some(self),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_enum<V>(
|
||||||
|
self,
|
||||||
|
name: &'static str,
|
||||||
|
variants: &'static [&'static str],
|
||||||
|
visitor: V,
|
||||||
|
) -> Result<V::Value>
|
||||||
|
where
|
||||||
|
V: de::Visitor<'de>,
|
||||||
|
{
|
||||||
|
visitor.visit_enum(EnumAccess {
|
||||||
|
value: self.cache,
|
||||||
|
name,
|
||||||
|
variants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
forward_to_deserialize_any! {
|
||||||
|
char seq
|
||||||
|
bytes byte_buf map struct unit newtype_struct
|
||||||
|
identifier ignored_any unit_struct tuple_struct tuple
|
||||||
|
}
|
||||||
|
}
|
||||||
222
pagetop/src/config/error.rs
Normal file
222
pagetop/src/config/error.rs
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
use nom;
|
||||||
|
use serde::de;
|
||||||
|
use serde::ser;
|
||||||
|
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt;
|
||||||
|
use std::result;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Unexpected {
|
||||||
|
Bool(bool),
|
||||||
|
Integer(i64),
|
||||||
|
Float(f64),
|
||||||
|
Str(String),
|
||||||
|
Unit,
|
||||||
|
Seq,
|
||||||
|
Map,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Unexpected {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> result::Result<(), fmt::Error> {
|
||||||
|
match *self {
|
||||||
|
Unexpected::Bool(b) => write!(f, "boolean `{}`", b),
|
||||||
|
Unexpected::Integer(i) => write!(f, "integer `{}`", i),
|
||||||
|
Unexpected::Float(v) => write!(f, "floating point `{}`", v),
|
||||||
|
Unexpected::Str(ref s) => write!(f, "string {:?}", s),
|
||||||
|
Unexpected::Unit => write!(f, "unit value"),
|
||||||
|
Unexpected::Seq => write!(f, "sequence"),
|
||||||
|
Unexpected::Map => write!(f, "map"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents all possible errors that can occur when working with configuration.
|
||||||
|
pub enum ConfigError {
|
||||||
|
/// Configuration is frozen and no further mutations can be made.
|
||||||
|
Frozen,
|
||||||
|
|
||||||
|
/// Configuration property was not found.
|
||||||
|
NotFound(String),
|
||||||
|
|
||||||
|
/// Configuration path could not be parsed.
|
||||||
|
PathParse(nom::error::ErrorKind),
|
||||||
|
|
||||||
|
/// Configuration could not be parsed from file.
|
||||||
|
FileParse {
|
||||||
|
/// The URI used to access the file (if not loaded from a string).
|
||||||
|
/// Example: `/path/to/config.json`
|
||||||
|
uri: Option<String>,
|
||||||
|
|
||||||
|
/// The captured error from attempting to parse the file in its desired format.
|
||||||
|
/// This is the actual error object from the library used for the parsing.
|
||||||
|
cause: Box<dyn Error + Send + Sync>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Value could not be converted into the requested type.
|
||||||
|
Type {
|
||||||
|
/// The URI that references the source that the value came from.
|
||||||
|
/// Example: `/path/to/config.json` or `Environment` or `etcd://localhost`
|
||||||
|
// TODO: Why is this called Origin but FileParse has a uri field?
|
||||||
|
origin: Option<String>,
|
||||||
|
|
||||||
|
/// What we found when parsing the value.
|
||||||
|
unexpected: Unexpected,
|
||||||
|
|
||||||
|
/// What was expected when parsing the value.
|
||||||
|
expected: &'static str,
|
||||||
|
|
||||||
|
/// The key in the configuration hash of this value (if available where the error is
|
||||||
|
/// generated).
|
||||||
|
key: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Custom message.
|
||||||
|
Message(String),
|
||||||
|
|
||||||
|
/// Unadorned error from a foreign origin.
|
||||||
|
Foreign(Box<dyn Error + Send + Sync>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigError {
|
||||||
|
// FIXME: pub(crate).
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn invalid_type(
|
||||||
|
origin: Option<String>,
|
||||||
|
unexpected: Unexpected,
|
||||||
|
expected: &'static str,
|
||||||
|
) -> Self {
|
||||||
|
ConfigError::Type {
|
||||||
|
origin,
|
||||||
|
unexpected,
|
||||||
|
expected,
|
||||||
|
key: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: pub(crate).
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn extend_with_key(self, key: &str) -> Self {
|
||||||
|
match self {
|
||||||
|
ConfigError::Type {
|
||||||
|
origin,
|
||||||
|
unexpected,
|
||||||
|
expected,
|
||||||
|
..
|
||||||
|
} => ConfigError::Type {
|
||||||
|
origin,
|
||||||
|
unexpected,
|
||||||
|
expected,
|
||||||
|
key: Some(key.into()),
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepend(self, segment: String, add_dot: bool) -> Self {
|
||||||
|
let concat = |key: Option<String>| {
|
||||||
|
let key = key.unwrap_or_else(String::new);
|
||||||
|
let dot = if add_dot && key.as_bytes().get(0).unwrap_or(&b'[') != &b'[' {
|
||||||
|
"."
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
format!("{}{}{}", segment, dot, key)
|
||||||
|
};
|
||||||
|
match self {
|
||||||
|
ConfigError::Type {
|
||||||
|
origin,
|
||||||
|
unexpected,
|
||||||
|
expected,
|
||||||
|
key,
|
||||||
|
} => ConfigError::Type {
|
||||||
|
origin,
|
||||||
|
unexpected,
|
||||||
|
expected,
|
||||||
|
key: Some(concat(key)),
|
||||||
|
},
|
||||||
|
ConfigError::NotFound(key) => ConfigError::NotFound(concat(Some(key))),
|
||||||
|
_ => self,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepend_key(self, key: String) -> Self {
|
||||||
|
self.prepend(key, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepend_index(self, idx: usize) -> Self {
|
||||||
|
self.prepend(format!("[{}]", idx), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alias for a `Result` with the error type set to `ConfigError`.
|
||||||
|
pub type Result<T> = result::Result<T, ConfigError>;
|
||||||
|
|
||||||
|
// Forward Debug to Display for readable panic! messages.
|
||||||
|
impl fmt::Debug for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", *self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ConfigError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
ConfigError::Frozen => write!(f, "configuration is frozen"),
|
||||||
|
|
||||||
|
ConfigError::PathParse(ref kind) => write!(f, "{}", kind.description()),
|
||||||
|
|
||||||
|
ConfigError::Message(ref s) => write!(f, "{}", s),
|
||||||
|
|
||||||
|
ConfigError::Foreign(ref cause) => write!(f, "{}", cause),
|
||||||
|
|
||||||
|
ConfigError::NotFound(ref key) => {
|
||||||
|
write!(f, "configuration property {:?} not found", key)
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigError::Type {
|
||||||
|
ref origin,
|
||||||
|
ref unexpected,
|
||||||
|
expected,
|
||||||
|
ref key,
|
||||||
|
} => {
|
||||||
|
write!(f, "invalid type: {}, expected {}", unexpected, expected)?;
|
||||||
|
|
||||||
|
if let Some(ref key) = *key {
|
||||||
|
write!(f, " for key `{}`", key)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref origin) = *origin {
|
||||||
|
write!(f, " in {}", origin)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigError::FileParse { ref cause, ref uri } => {
|
||||||
|
write!(f, "{}", cause)?;
|
||||||
|
|
||||||
|
if let Some(ref uri) = *uri {
|
||||||
|
write!(f, " in {}", uri)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for ConfigError {}
|
||||||
|
|
||||||
|
impl de::Error for ConfigError {
|
||||||
|
fn custom<T: fmt::Display>(msg: T) -> Self {
|
||||||
|
ConfigError::Message(msg.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ser::Error for ConfigError {
|
||||||
|
fn custom<T: fmt::Display>(msg: T) -> Self {
|
||||||
|
ConfigError::Message(msg.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
90
pagetop/src/config/file.rs
Normal file
90
pagetop/src/config/file.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
mod source;
|
||||||
|
mod toml;
|
||||||
|
|
||||||
|
use crate::config::error::*;
|
||||||
|
use crate::config::source::Source;
|
||||||
|
use crate::config::value::Value;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use self::source::FileSource;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct File<T>
|
||||||
|
where
|
||||||
|
T: FileSource,
|
||||||
|
{
|
||||||
|
source: T,
|
||||||
|
|
||||||
|
/// A required File will error if it cannot be found.
|
||||||
|
required: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File<source::FileSourceFile> {
|
||||||
|
/// Given the basename of a file, will attempt to locate a file by setting its extension to a
|
||||||
|
/// registered format.
|
||||||
|
pub fn with_name(name: &str) -> Self {
|
||||||
|
File {
|
||||||
|
required: true,
|
||||||
|
source: source::FileSourceFile::new(name.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a Path> for File<source::FileSourceFile> {
|
||||||
|
fn from(path: &'a Path) -> Self {
|
||||||
|
File {
|
||||||
|
required: true,
|
||||||
|
source: source::FileSourceFile::new(path.to_path_buf()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for File<source::FileSourceFile> {
|
||||||
|
fn from(path: PathBuf) -> Self {
|
||||||
|
File {
|
||||||
|
required: true,
|
||||||
|
source: source::FileSourceFile::new(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FileSource> File<T> {
|
||||||
|
pub fn required(mut self, required: bool) -> Self {
|
||||||
|
self.required = required;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: FileSource> Source for File<T>
|
||||||
|
where
|
||||||
|
T: 'static,
|
||||||
|
T: Sync + Send,
|
||||||
|
{
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
Box::new((*self).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(&self) -> Result<HashMap<String, Value>> {
|
||||||
|
// Coerce the file contents to a string.
|
||||||
|
let (uri, contents) = match self
|
||||||
|
.source
|
||||||
|
.resolve()
|
||||||
|
.map_err(|err| ConfigError::Foreign(err))
|
||||||
|
{
|
||||||
|
Ok((uri, contents)) => (uri, contents),
|
||||||
|
|
||||||
|
Err(error) => {
|
||||||
|
if !self.required {
|
||||||
|
return Ok(HashMap::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the string using the given format.
|
||||||
|
toml::parse(uri.as_ref(), &contents).map_err(|cause| ConfigError::FileParse { uri, cause })
|
||||||
|
}
|
||||||
|
}
|
||||||
127
pagetop/src/config/file/source.rs
Normal file
127
pagetop/src/config/file/source.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::env;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{self, Read};
|
||||||
|
use std::iter::Iterator;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|
||||||
|
/// Describes where the file is sourced.
|
||||||
|
pub trait FileSource: Debug + Clone {
|
||||||
|
fn resolve(&self) -> Result<(Option<String>, String), Box<dyn Error + Send + Sync>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a file sourced from a file.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct FileSourceFile {
|
||||||
|
/// Path of configuration file.
|
||||||
|
name: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSourceFile {
|
||||||
|
pub fn new(name: PathBuf) -> FileSourceFile {
|
||||||
|
FileSourceFile { name }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_file(&self) -> Result<PathBuf, Box<dyn Error + Send + Sync>> {
|
||||||
|
// First check for an _exact_ match.
|
||||||
|
let mut filename = env::current_dir()?.as_path().join(self.name.clone());
|
||||||
|
if filename.is_file() {
|
||||||
|
if vec!["toml"].contains(
|
||||||
|
&filename
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.as_ref(),
|
||||||
|
) {
|
||||||
|
return Ok(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Box::new(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!(
|
||||||
|
"configuration file \"{}\" is not of a registered file format",
|
||||||
|
filename.to_string_lossy()
|
||||||
|
),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
filename.set_extension("toml");
|
||||||
|
|
||||||
|
if filename.is_file() {
|
||||||
|
return Ok(filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(Box::new(io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
format!(
|
||||||
|
"configuration file \"{}\" not found",
|
||||||
|
self.name.to_string_lossy()
|
||||||
|
),
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileSource for FileSourceFile {
|
||||||
|
fn resolve(&self) -> Result<(Option<String>, String), Box<dyn Error + Send + Sync>> {
|
||||||
|
// Find file.
|
||||||
|
let filename = self.find_file()?;
|
||||||
|
|
||||||
|
// Attempt to use a relative path for the URI.
|
||||||
|
let base = env::current_dir()?;
|
||||||
|
let uri = match path_relative_from(&filename, &base) {
|
||||||
|
Some(value) => value,
|
||||||
|
None => filename.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read contents from file.
|
||||||
|
let mut file = fs::File::open(filename)?;
|
||||||
|
let mut text = String::new();
|
||||||
|
file.read_to_string(&mut text)?;
|
||||||
|
|
||||||
|
Ok((Some(uri.to_string_lossy().into_owned()), text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This should probably be a crate.
|
||||||
|
// https://github.com/rust-lang/rust/blob/master/src/librustc_trans/back/rpath.rs#L128
|
||||||
|
fn path_relative_from(path: &Path, base: &Path) -> Option<PathBuf> {
|
||||||
|
use std::path::Component;
|
||||||
|
|
||||||
|
if path.is_absolute() != base.is_absolute() {
|
||||||
|
if path.is_absolute() {
|
||||||
|
Some(PathBuf::from(path))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut ita = path.components();
|
||||||
|
let mut itb = base.components();
|
||||||
|
let mut comps: Vec<Component> = vec![];
|
||||||
|
loop {
|
||||||
|
match (ita.next(), itb.next()) {
|
||||||
|
(None, None) => break,
|
||||||
|
(Some(a), None) => {
|
||||||
|
comps.push(a);
|
||||||
|
comps.extend(ita.by_ref());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(None, _) => comps.push(Component::ParentDir),
|
||||||
|
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
|
||||||
|
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
|
||||||
|
(Some(_), Some(b)) if b == Component::ParentDir => return None,
|
||||||
|
(Some(a), Some(_)) => {
|
||||||
|
comps.push(Component::ParentDir);
|
||||||
|
for _ in itb {
|
||||||
|
comps.push(Component::ParentDir);
|
||||||
|
}
|
||||||
|
comps.push(a);
|
||||||
|
comps.extend(ita.by_ref());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(comps.iter().map(|c| c.as_os_str()).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
51
pagetop/src/config/file/toml.rs
Normal file
51
pagetop/src/config/file/toml.rs
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
use crate::config::value::{Value, ValueKind};
|
||||||
|
|
||||||
|
use toml;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
pub fn parse(
|
||||||
|
uri: Option<&String>,
|
||||||
|
text: &str,
|
||||||
|
) -> Result<HashMap<String, Value>, Box<dyn Error + Send + Sync>> {
|
||||||
|
// Parse a TOML value from the provided text.
|
||||||
|
// TODO: Have a proper error fire if the root of a file is ever not a Table
|
||||||
|
let value = from_toml_value(uri, &toml::from_str(text)?);
|
||||||
|
match value.kind {
|
||||||
|
ValueKind::Table(map) => Ok(map),
|
||||||
|
|
||||||
|
_ => Ok(HashMap::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_toml_value(uri: Option<&String>, value: &toml::Value) -> Value {
|
||||||
|
match *value {
|
||||||
|
toml::Value::String(ref value) => Value::new(uri, value.to_string()),
|
||||||
|
toml::Value::Float(value) => Value::new(uri, value),
|
||||||
|
toml::Value::Integer(value) => Value::new(uri, value),
|
||||||
|
toml::Value::Boolean(value) => Value::new(uri, value),
|
||||||
|
|
||||||
|
toml::Value::Table(ref table) => {
|
||||||
|
let mut m = HashMap::new();
|
||||||
|
|
||||||
|
for (key, value) in table {
|
||||||
|
m.insert(key.clone(), from_toml_value(uri, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::new(uri, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::Value::Array(ref array) => {
|
||||||
|
let mut l = Vec::new();
|
||||||
|
|
||||||
|
for value in array {
|
||||||
|
l.push(from_toml_value(uri, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::new(uri, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::Value::Datetime(ref datetime) => Value::new(uri, datetime.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
254
pagetop/src/config/path.rs
Normal file
254
pagetop/src/config/path.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
use crate::config::error::*;
|
||||||
|
use crate::config::value::{Value, ValueKind};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
mod parser;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
|
||||||
|
pub enum Expression {
|
||||||
|
Identifier(String),
|
||||||
|
Child(Box<Expression>, String),
|
||||||
|
Subscript(Box<Expression>, isize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Expression {
|
||||||
|
type Err = ConfigError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Expression> {
|
||||||
|
parser::from_str(s).map_err(ConfigError::PathParse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sindex_to_uindex(index: isize, len: usize) -> usize {
|
||||||
|
if index >= 0 {
|
||||||
|
index as usize
|
||||||
|
} else {
|
||||||
|
len - (index.abs() as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Expression {
|
||||||
|
pub fn get(self, root: &Value) -> Option<&Value> {
|
||||||
|
match self {
|
||||||
|
Expression::Identifier(id) => {
|
||||||
|
match root.kind {
|
||||||
|
// `x` access on a table is equivalent to: map[x].
|
||||||
|
ValueKind::Table(ref map) => map.get(&id),
|
||||||
|
|
||||||
|
// All other variants return None.
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Child(expr, key) => {
|
||||||
|
match expr.get(root) {
|
||||||
|
Some(value) => {
|
||||||
|
match value.kind {
|
||||||
|
// Access on a table is identical to Identifier, it just forwards.
|
||||||
|
ValueKind::Table(ref map) => map.get(&key),
|
||||||
|
|
||||||
|
// All other variants return None.
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Subscript(expr, index) => match expr.get(root) {
|
||||||
|
Some(value) => match value.kind {
|
||||||
|
ValueKind::Array(ref array) => {
|
||||||
|
let index = sindex_to_uindex(index, array.len());
|
||||||
|
|
||||||
|
if index >= array.len() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&array[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> {
|
||||||
|
match *self {
|
||||||
|
Expression::Identifier(ref id) => match root.kind {
|
||||||
|
ValueKind::Table(ref mut map) => map.get_mut(id),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
Expression::Child(ref expr, ref key) => match expr.get_mut(root) {
|
||||||
|
Some(value) => match value.kind {
|
||||||
|
ValueKind::Table(ref mut map) => map.get_mut(key),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
Expression::Subscript(ref expr, index) => match expr.get_mut(root) {
|
||||||
|
Some(value) => match value.kind {
|
||||||
|
ValueKind::Array(ref mut array) => {
|
||||||
|
let index = sindex_to_uindex(index, array.len());
|
||||||
|
|
||||||
|
if index >= array.len() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(&mut array[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut_forcibly<'a>(&self, root: &'a mut Value) -> Option<&'a mut Value> {
|
||||||
|
match *self {
|
||||||
|
Expression::Identifier(ref id) => match root.kind {
|
||||||
|
ValueKind::Table(ref mut map) => Some(
|
||||||
|
map.entry(id.clone())
|
||||||
|
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
|
||||||
|
),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
Expression::Child(ref expr, ref key) => match expr.get_mut_forcibly(root) {
|
||||||
|
Some(value) => match value.kind {
|
||||||
|
ValueKind::Table(ref mut map) => Some(
|
||||||
|
map.entry(key.clone())
|
||||||
|
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
|
||||||
|
),
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
*value = HashMap::<String, Value>::new().into();
|
||||||
|
|
||||||
|
if let ValueKind::Table(ref mut map) = value.kind {
|
||||||
|
Some(
|
||||||
|
map.entry(key.clone())
|
||||||
|
.or_insert_with(|| Value::new(None, ValueKind::Nil)),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
|
||||||
|
Expression::Subscript(ref expr, index) => match expr.get_mut_forcibly(root) {
|
||||||
|
Some(value) => {
|
||||||
|
match value.kind {
|
||||||
|
ValueKind::Array(_) => (),
|
||||||
|
_ => *value = Vec::<Value>::new().into(),
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.kind {
|
||||||
|
ValueKind::Array(ref mut array) => {
|
||||||
|
let index = sindex_to_uindex(index, array.len());
|
||||||
|
|
||||||
|
if index >= array.len() {
|
||||||
|
array
|
||||||
|
.resize((index + 1) as usize, Value::new(None, ValueKind::Nil));
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(&mut array[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self, root: &mut Value, value: Value) {
|
||||||
|
match *self {
|
||||||
|
Expression::Identifier(ref id) => {
|
||||||
|
// Ensure that root is a table.
|
||||||
|
match root.kind {
|
||||||
|
ValueKind::Table(_) => {}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
*root = HashMap::<String, Value>::new().into();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match value.kind {
|
||||||
|
ValueKind::Table(ref incoming_map) => {
|
||||||
|
// Pull out another table.
|
||||||
|
let mut target = if let ValueKind::Table(ref mut map) = root.kind {
|
||||||
|
map.entry(id.clone())
|
||||||
|
.or_insert_with(|| HashMap::<String, Value>::new().into())
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Continue the deep merge.
|
||||||
|
for (key, val) in incoming_map {
|
||||||
|
Expression::Identifier(key.clone()).set(&mut target, val.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
if let ValueKind::Table(ref mut map) = root.kind {
|
||||||
|
// Just do a simple set.
|
||||||
|
map.insert(id.clone(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Child(ref expr, ref key) => {
|
||||||
|
if let Some(parent) = expr.get_mut_forcibly(root) {
|
||||||
|
match parent.kind {
|
||||||
|
ValueKind::Table(_) => {
|
||||||
|
Expression::Identifier(key.clone()).set(parent, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {
|
||||||
|
// Didn't find a table. Oh well. Make a table and do this anyway.
|
||||||
|
*parent = HashMap::<String, Value>::new().into();
|
||||||
|
|
||||||
|
Expression::Identifier(key.clone()).set(parent, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression::Subscript(ref expr, index) => {
|
||||||
|
if let Some(parent) = expr.get_mut_forcibly(root) {
|
||||||
|
match parent.kind {
|
||||||
|
ValueKind::Array(_) => (),
|
||||||
|
_ => *parent = Vec::<Value>::new().into(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ValueKind::Array(ref mut array) = parent.kind {
|
||||||
|
let uindex = sindex_to_uindex(index, array.len());
|
||||||
|
if uindex >= array.len() {
|
||||||
|
array.resize((uindex + 1) as usize, Value::new(None, ValueKind::Nil));
|
||||||
|
}
|
||||||
|
|
||||||
|
array[uindex] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
131
pagetop/src/config/path/parser.rs
Normal file
131
pagetop/src/config/path/parser.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
use super::Expression;
|
||||||
|
|
||||||
|
use nom::{
|
||||||
|
branch::alt,
|
||||||
|
bytes::complete::{is_a, tag},
|
||||||
|
character::complete::{char, digit1, space0},
|
||||||
|
combinator::{map, map_res, opt, recognize},
|
||||||
|
error::ErrorKind,
|
||||||
|
sequence::{delimited, pair, preceded},
|
||||||
|
Err, IResult,
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
fn raw_ident(i: &str) -> IResult<&str, String> {
|
||||||
|
map(
|
||||||
|
is_a(
|
||||||
|
"abcdefghijklmnopqrstuvwxyz \
|
||||||
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ \
|
||||||
|
0123456789 \
|
||||||
|
_-",
|
||||||
|
),
|
||||||
|
|s: &str| s.to_string(),
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn integer(i: &str) -> IResult<&str, isize> {
|
||||||
|
map_res(
|
||||||
|
delimited(space0, recognize(pair(opt(tag("-")), digit1)), space0),
|
||||||
|
FromStr::from_str,
|
||||||
|
)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ident(i: &str) -> IResult<&str, Expression> {
|
||||||
|
map(raw_ident, Expression::Identifier)(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn postfix<'a>(expr: Expression) -> impl FnMut(&'a str) -> IResult<&'a str, Expression> {
|
||||||
|
let e2 = expr.clone();
|
||||||
|
let child = map(preceded(tag("."), raw_ident), move |id| {
|
||||||
|
Expression::Child(Box::new(expr.clone()), id)
|
||||||
|
});
|
||||||
|
|
||||||
|
let subscript = map(delimited(char('['), integer, char(']')), move |num| {
|
||||||
|
Expression::Subscript(Box::new(e2.clone()), num)
|
||||||
|
});
|
||||||
|
|
||||||
|
alt((child, subscript))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(input: &str) -> Result<Expression, ErrorKind> {
|
||||||
|
match ident(input) {
|
||||||
|
Ok((mut rem, mut expr)) => {
|
||||||
|
while !rem.is_empty() {
|
||||||
|
match postfix(expr)(rem) {
|
||||||
|
Ok((rem_, expr_)) => {
|
||||||
|
rem = rem_;
|
||||||
|
expr = expr_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward Incomplete and Error
|
||||||
|
result => {
|
||||||
|
return result.map(|(_, o)| o).map_err(to_error_kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(expr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward Incomplete and Error
|
||||||
|
result => result.map(|(_, o)| o).map_err(to_error_kind),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_error_kind(e: Err<nom::error::Error<&str>>) -> ErrorKind {
|
||||||
|
match e {
|
||||||
|
Err::Incomplete(_) => ErrorKind::Complete,
|
||||||
|
Err::Failure(e) | Err::Error(e) => e.code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::Expression::*;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id() {
|
||||||
|
let parsed: Expression = from_str("abcd").unwrap();
|
||||||
|
assert_eq!(parsed, Identifier("abcd".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_id_dash() {
|
||||||
|
let parsed: Expression = from_str("abcd-efgh").unwrap();
|
||||||
|
assert_eq!(parsed, Identifier("abcd-efgh".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_child() {
|
||||||
|
let parsed: Expression = from_str("abcd.efgh").unwrap();
|
||||||
|
let expected = Child(Box::new(Identifier("abcd".into())), "efgh".into());
|
||||||
|
|
||||||
|
assert_eq!(parsed, expected);
|
||||||
|
|
||||||
|
let parsed: Expression = from_str("abcd.efgh.ijkl").unwrap();
|
||||||
|
let expected = Child(
|
||||||
|
Box::new(Child(Box::new(Identifier("abcd".into())), "efgh".into())),
|
||||||
|
"ijkl".into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(parsed, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_subscript() {
|
||||||
|
let parsed: Expression = from_str("abcd[12]").unwrap();
|
||||||
|
let expected = Subscript(Box::new(Identifier("abcd".into())), 12);
|
||||||
|
|
||||||
|
assert_eq!(parsed, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_subscript_neg() {
|
||||||
|
let parsed: Expression = from_str("abcd[-1]").unwrap();
|
||||||
|
let expected = Subscript(Box::new(Identifier("abcd".into())), -1);
|
||||||
|
|
||||||
|
assert_eq!(parsed, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
87
pagetop/src/config/source.rs
Normal file
87
pagetop/src/config/source.rs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
use crate::config::error::*;
|
||||||
|
use crate::config::path;
|
||||||
|
use crate::config::value::{Value, ValueKind};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// Describes a generic _source_ of configuration properties.
|
||||||
|
pub trait Source: Debug {
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync>;
|
||||||
|
|
||||||
|
/// Collect all configuration properties available from this source and return a HashMap.
|
||||||
|
fn collect(&self) -> Result<HashMap<String, Value>>;
|
||||||
|
|
||||||
|
fn collect_to(&self, cache: &mut Value) -> Result<()> {
|
||||||
|
let props = match self.collect() {
|
||||||
|
Ok(props) => props,
|
||||||
|
Err(error) => {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (key, val) in &props {
|
||||||
|
match path::Expression::from_str(key) {
|
||||||
|
// Set using the path.
|
||||||
|
Ok(expr) => expr.set(cache, val.clone()),
|
||||||
|
|
||||||
|
// Set diretly anyway.
|
||||||
|
_ => path::Expression::Identifier(key.clone()).set(cache, val.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Box<dyn Source + Send + Sync> {
|
||||||
|
fn clone(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
self.clone_into_box()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for Vec<Box<dyn Source + Send + Sync>> {
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
Box::new((*self).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(&self) -> Result<HashMap<String, Value>> {
|
||||||
|
let mut cache: Value = HashMap::<String, Value>::new().into();
|
||||||
|
|
||||||
|
for source in self {
|
||||||
|
source.collect_to(&mut cache)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ValueKind::Table(table) = cache.kind {
|
||||||
|
Ok(table)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Source for Vec<T>
|
||||||
|
where
|
||||||
|
T: Source + Sync + Send,
|
||||||
|
T: Clone,
|
||||||
|
T: 'static,
|
||||||
|
{
|
||||||
|
fn clone_into_box(&self) -> Box<dyn Source + Send + Sync> {
|
||||||
|
Box::new((*self).clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect(&self) -> Result<HashMap<String, Value>> {
|
||||||
|
let mut cache: Value = HashMap::<String, Value>::new().into();
|
||||||
|
|
||||||
|
for source in self {
|
||||||
|
source.collect_to(&mut cache)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let ValueKind::Table(table) = cache.kind {
|
||||||
|
Ok(table)
|
||||||
|
} else {
|
||||||
|
unreachable!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
546
pagetop/src/config/value.rs
Normal file
546
pagetop/src/config/value.rs
Normal file
|
|
@ -0,0 +1,546 @@
|
||||||
|
use crate::config::error::*;
|
||||||
|
|
||||||
|
use serde::de::{Deserialize, Deserializer, Visitor};
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
/// Underlying kind of the configuration value.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum ValueKind {
|
||||||
|
Nil,
|
||||||
|
Boolean(bool),
|
||||||
|
Integer(i64),
|
||||||
|
Float(f64),
|
||||||
|
String(String),
|
||||||
|
Table(Table),
|
||||||
|
Array(Array),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Array = Vec<Value>;
|
||||||
|
pub type Table = HashMap<String, Value>;
|
||||||
|
|
||||||
|
impl Default for ValueKind {
|
||||||
|
fn default() -> Self {
|
||||||
|
ValueKind::Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Option<T>> for ValueKind
|
||||||
|
where
|
||||||
|
T: Into<ValueKind>,
|
||||||
|
{
|
||||||
|
fn from(value: Option<T>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some(value) => value.into(),
|
||||||
|
None => ValueKind::Nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for ValueKind {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
ValueKind::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for ValueKind {
|
||||||
|
fn from(value: &'a str) -> Self {
|
||||||
|
ValueKind::String(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i64> for ValueKind {
|
||||||
|
fn from(value: i64) -> Self {
|
||||||
|
ValueKind::Integer(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<f64> for ValueKind {
|
||||||
|
fn from(value: f64) -> Self {
|
||||||
|
ValueKind::Float(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for ValueKind {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
ValueKind::Boolean(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<HashMap<String, T>> for ValueKind
|
||||||
|
where
|
||||||
|
T: Into<Value>,
|
||||||
|
{
|
||||||
|
fn from(values: HashMap<String, T>) -> Self {
|
||||||
|
let mut r = HashMap::new();
|
||||||
|
|
||||||
|
for (k, v) in values {
|
||||||
|
r.insert(k.clone(), v.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueKind::Table(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<Vec<T>> for ValueKind
|
||||||
|
where
|
||||||
|
T: Into<Value>,
|
||||||
|
{
|
||||||
|
fn from(values: Vec<T>) -> Self {
|
||||||
|
let mut l = Vec::new();
|
||||||
|
|
||||||
|
for v in values {
|
||||||
|
l.push(v.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueKind::Array(l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ValueKind {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
ValueKind::String(ref value) => write!(f, "{}", value),
|
||||||
|
ValueKind::Boolean(value) => write!(f, "{}", value),
|
||||||
|
ValueKind::Integer(value) => write!(f, "{}", value),
|
||||||
|
ValueKind::Float(value) => write!(f, "{}", value),
|
||||||
|
ValueKind::Nil => write!(f, "nil"),
|
||||||
|
|
||||||
|
// TODO: Figure out a nice Display for these
|
||||||
|
ValueKind::Table(ref table) => write!(f, "{:?}", table),
|
||||||
|
ValueKind::Array(ref array) => write!(f, "{:?}", array),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A configuration value.
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq)]
|
||||||
|
pub struct Value {
|
||||||
|
/// A description of the original location of the value.
|
||||||
|
///
|
||||||
|
/// A Value originating from a File might contain:
|
||||||
|
/// ```text
|
||||||
|
/// Settings.toml
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A Value originating from the environment would contain:
|
||||||
|
/// ```text
|
||||||
|
/// the envrionment
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A Value originating from a remote source might contain:
|
||||||
|
/// ```text
|
||||||
|
/// etcd+http://127.0.0.1:2379
|
||||||
|
/// ```
|
||||||
|
origin: Option<String>,
|
||||||
|
|
||||||
|
/// Underlying kind of the configuration value.
|
||||||
|
pub kind: ValueKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
/// Create a new value instance that will remember its source uri.
|
||||||
|
pub fn new<V>(origin: Option<&String>, kind: V) -> Self
|
||||||
|
where
|
||||||
|
V: Into<ValueKind>,
|
||||||
|
{
|
||||||
|
Value {
|
||||||
|
origin: origin.cloned(),
|
||||||
|
kind: kind.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempt to deserialize this value into the requested type.
|
||||||
|
pub fn try_into<'de, T: Deserialize<'de>>(self) -> Result<T> {
|
||||||
|
T::deserialize(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `self` as a bool, if possible.
|
||||||
|
// FIXME: Should this not be `try_into_*` ?
|
||||||
|
pub fn into_bool(self) -> Result<bool> {
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Boolean(value) => Ok(value),
|
||||||
|
ValueKind::Integer(value) => Ok(value != 0),
|
||||||
|
ValueKind::Float(value) => Ok(value != 0.0),
|
||||||
|
|
||||||
|
ValueKind::String(ref value) => {
|
||||||
|
match value.to_lowercase().as_ref() {
|
||||||
|
"1" | "true" | "on" | "yes" => Ok(true),
|
||||||
|
"0" | "false" | "off" | "no" => Ok(false),
|
||||||
|
|
||||||
|
// Unexpected string value
|
||||||
|
s => Err(ConfigError::invalid_type(
|
||||||
|
self.origin.clone(),
|
||||||
|
Unexpected::Str(s.into()),
|
||||||
|
"a boolean",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unexpected type
|
||||||
|
ValueKind::Nil => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Unit,
|
||||||
|
"a boolean",
|
||||||
|
)),
|
||||||
|
ValueKind::Table(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Map,
|
||||||
|
"a boolean",
|
||||||
|
)),
|
||||||
|
ValueKind::Array(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Seq,
|
||||||
|
"a boolean",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `self` into an i64, if possible.
|
||||||
|
// FIXME: Should this not be `try_into_*` ?
|
||||||
|
pub fn into_int(self) -> Result<i64> {
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Integer(value) => Ok(value),
|
||||||
|
|
||||||
|
ValueKind::String(ref s) => {
|
||||||
|
match s.to_lowercase().as_ref() {
|
||||||
|
"true" | "on" | "yes" => Ok(1),
|
||||||
|
"false" | "off" | "no" => Ok(0),
|
||||||
|
_ => {
|
||||||
|
s.parse().map_err(|_| {
|
||||||
|
// Unexpected string
|
||||||
|
ConfigError::invalid_type(
|
||||||
|
self.origin.clone(),
|
||||||
|
Unexpected::Str(s.clone()),
|
||||||
|
"an integer",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueKind::Boolean(value) => Ok(if value { 1 } else { 0 }),
|
||||||
|
ValueKind::Float(value) => Ok(value.round() as i64),
|
||||||
|
|
||||||
|
// Unexpected type
|
||||||
|
ValueKind::Nil => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Unit,
|
||||||
|
"an integer",
|
||||||
|
)),
|
||||||
|
ValueKind::Table(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Map,
|
||||||
|
"an integer",
|
||||||
|
)),
|
||||||
|
ValueKind::Array(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Seq,
|
||||||
|
"an integer",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `self` into a f64, if possible.
|
||||||
|
// FIXME: Should this not be `try_into_*` ?
|
||||||
|
pub fn into_float(self) -> Result<f64> {
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Float(value) => Ok(value),
|
||||||
|
|
||||||
|
ValueKind::String(ref s) => {
|
||||||
|
match s.to_lowercase().as_ref() {
|
||||||
|
"true" | "on" | "yes" => Ok(1.0),
|
||||||
|
"false" | "off" | "no" => Ok(0.0),
|
||||||
|
_ => {
|
||||||
|
s.parse().map_err(|_| {
|
||||||
|
// Unexpected string
|
||||||
|
ConfigError::invalid_type(
|
||||||
|
self.origin.clone(),
|
||||||
|
Unexpected::Str(s.clone()),
|
||||||
|
"a floating point",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueKind::Integer(value) => Ok(value as f64),
|
||||||
|
ValueKind::Boolean(value) => Ok(if value { 1.0 } else { 0.0 }),
|
||||||
|
|
||||||
|
// Unexpected type.
|
||||||
|
ValueKind::Nil => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Unit,
|
||||||
|
"a floating point",
|
||||||
|
)),
|
||||||
|
ValueKind::Table(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Map,
|
||||||
|
"a floating point",
|
||||||
|
)),
|
||||||
|
ValueKind::Array(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Seq,
|
||||||
|
"a floating point",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `self` into a str, if possible.
|
||||||
|
// FIXME: Should this not be `try_into_*` ?
|
||||||
|
pub fn into_str(self) -> Result<String> {
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::String(value) => Ok(value),
|
||||||
|
|
||||||
|
ValueKind::Boolean(value) => Ok(value.to_string()),
|
||||||
|
ValueKind::Integer(value) => Ok(value.to_string()),
|
||||||
|
ValueKind::Float(value) => Ok(value.to_string()),
|
||||||
|
|
||||||
|
// Cannot convert.
|
||||||
|
ValueKind::Nil => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Unit,
|
||||||
|
"a string",
|
||||||
|
)),
|
||||||
|
ValueKind::Table(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Map,
|
||||||
|
"a string",
|
||||||
|
)),
|
||||||
|
ValueKind::Array(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Seq,
|
||||||
|
"a string",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `self` into an array, if possible.
|
||||||
|
// FIXME: Should this not be `try_into_*` ?
|
||||||
|
pub fn into_array(self) -> Result<Vec<Value>> {
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Array(value) => Ok(value),
|
||||||
|
|
||||||
|
// Cannot convert.
|
||||||
|
ValueKind::Float(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Float(value),
|
||||||
|
"an array",
|
||||||
|
)),
|
||||||
|
ValueKind::String(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Str(value),
|
||||||
|
"an array",
|
||||||
|
)),
|
||||||
|
ValueKind::Integer(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Integer(value),
|
||||||
|
"an array",
|
||||||
|
)),
|
||||||
|
ValueKind::Boolean(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Bool(value),
|
||||||
|
"an array",
|
||||||
|
)),
|
||||||
|
ValueKind::Nil => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Unit,
|
||||||
|
"an array",
|
||||||
|
)),
|
||||||
|
ValueKind::Table(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Map,
|
||||||
|
"an array",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If the `Value` is a Table, returns the associated Map.
|
||||||
|
// FIXME: Should this not be `try_into_*` ?
|
||||||
|
pub fn into_table(self) -> Result<HashMap<String, Value>> {
|
||||||
|
match self.kind {
|
||||||
|
ValueKind::Table(value) => Ok(value),
|
||||||
|
|
||||||
|
// Cannot convert.
|
||||||
|
ValueKind::Float(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Float(value),
|
||||||
|
"a map",
|
||||||
|
)),
|
||||||
|
ValueKind::String(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Str(value),
|
||||||
|
"a map",
|
||||||
|
)),
|
||||||
|
ValueKind::Integer(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Integer(value),
|
||||||
|
"a map",
|
||||||
|
)),
|
||||||
|
ValueKind::Boolean(value) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Bool(value),
|
||||||
|
"a map",
|
||||||
|
)),
|
||||||
|
ValueKind::Nil => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Unit,
|
||||||
|
"a map",
|
||||||
|
)),
|
||||||
|
ValueKind::Array(_) => Err(ConfigError::invalid_type(
|
||||||
|
self.origin,
|
||||||
|
Unexpected::Seq,
|
||||||
|
"a map",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for Value {
|
||||||
|
#[inline]
|
||||||
|
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Value, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
struct ValueVisitor;
|
||||||
|
|
||||||
|
impl<'de> Visitor<'de> for ValueVisitor {
|
||||||
|
type Value = Value;
|
||||||
|
|
||||||
|
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
formatter.write_str("any valid configuration value")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_bool<E>(self, value: bool) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_i8<E>(self, value: i8) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_i16<E>(self, value: i16) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_i32<E>(self, value: i32) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_i64<E>(self, value: i64) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_u8<E>(self, value: u8) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_u16<E>(self, value: u16) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_u32<E>(self, value: u32) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_u64<E>(self, value: u64) -> ::std::result::Result<Value, E> {
|
||||||
|
// FIXME: This is bad
|
||||||
|
Ok((value as i64).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_f64<E>(self, value: f64) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_str<E>(self, value: &str) -> ::std::result::Result<Value, E>
|
||||||
|
where
|
||||||
|
E: ::serde::de::Error,
|
||||||
|
{
|
||||||
|
self.visit_string(String::from(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_string<E>(self, value: String) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_none<E>(self) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok(Value::new(None, ValueKind::Nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_some<D>(self, deserializer: D) -> ::std::result::Result<Value, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Deserialize::deserialize(deserializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_unit<E>(self) -> ::std::result::Result<Value, E> {
|
||||||
|
Ok(Value::new(None, ValueKind::Nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn visit_seq<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
|
||||||
|
where
|
||||||
|
V: ::serde::de::SeqAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut vec = Array::new();
|
||||||
|
|
||||||
|
while let Some(elem) = visitor.next_element()? {
|
||||||
|
vec.push(elem);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vec.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_map<V>(self, mut visitor: V) -> ::std::result::Result<Value, V::Error>
|
||||||
|
where
|
||||||
|
V: ::serde::de::MapAccess<'de>,
|
||||||
|
{
|
||||||
|
let mut values = Table::new();
|
||||||
|
|
||||||
|
while let Some((key, value)) = visitor.next_entry()? {
|
||||||
|
values.insert(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(values.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deserializer.deserialize_any(ValueVisitor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> From<T> for Value
|
||||||
|
where
|
||||||
|
T: Into<ValueKind>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Value {
|
||||||
|
origin: None,
|
||||||
|
kind: value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Value {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue