imag/libimagstore/src/configuration.rs

699 lines
24 KiB
Rust
Raw Normal View History

//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; version
// 2.1 of the License.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
use toml::Value;
2016-03-05 10:37:06 +00:00
use libimagerror::into::IntoError;
use libimagutil::iter::FoldResult;
use store::Result;
2016-03-05 14:02:29 +00:00
/// Check whether the configuration is valid for the store
///
/// The passed `Value` _must be_ the `[store]` sub-tree of the configuration. Otherwise this will
/// fail.
///
/// It checks whether the configuration looks like the store wants it to be:
///
/// ```toml
/// [store]
/// pre-create-hook-aspects = [ "misc", "encryption", "version-control"]
///
2016-07-16 20:17:05 +00:00
/// [store.aspects.misc]
2016-03-05 14:02:29 +00:00
/// parallel = true
2016-07-16 20:17:05 +00:00
///
/// [store.aspects.encryption]
2016-03-05 14:02:29 +00:00
/// parallel = false
2016-07-16 20:17:05 +00:00
///
/// [store.aspects.version-control]
2016-03-05 14:02:29 +00:00
/// parallel = false
///
2016-07-16 20:17:05 +00:00
/// [store.hooks.gnupg]
2016-03-05 14:02:29 +00:00
/// aspect = "encryption"
/// key = "0x123456789"
///
2016-07-16 20:17:05 +00:00
/// [store.hooks.git]
2016-03-05 14:02:29 +00:00
/// aspect = "version-control"
/// ```
///
/// It checks:
/// * Whether all the maps are there (whether store, store.aspects, store.aspects.example are all
/// maps)
/// * Whether each aspect configuration has a "parallel = <Boolean>" setting
/// * Whether each hook congfiguration has a "aspect = <String>" setting
///
/// It does NOT check:
/// * Whether all aspects which are used in the hook configuration are also configured
///
2016-03-05 17:16:05 +00:00
/// No configuration is a valid configuration, as the store will use the most conservative settings
/// automatically. This has also performance impact, as all hooks run in no-parallel mode then.
/// You have been warned!
///
///
pub fn config_is_valid(config: &Option<Value>) -> Result<()> {
2016-03-05 14:02:29 +00:00
use std::collections::BTreeMap;
use error::StoreErrorKind as SEK;
2016-03-05 14:02:29 +00:00
2016-03-05 17:16:05 +00:00
if config.is_none() {
return Ok(());
2016-03-05 17:16:05 +00:00
}
/// Check whether the config has a key with a string array.
/// The `key` is the key which is checked
/// The `kind` is the error kind which is used as `cause` if there is an error, so we can
/// indicate via error type which key is missing
fn has_key_with_string_ary(v: &BTreeMap<String, Value>, key: &str,
kind: SEK) -> Result<()> {
2016-03-05 14:02:29 +00:00
v.get(key)
.ok_or_else(|| {
warn!("Required key '{}' is not in store config", key);
SEK::ConfigKeyMissingError.into_error_with_cause(Box::new(kind.into_error()))
})
.and_then(|t| match *t {
Value::Array(ref a) => {
a.iter().fold_result(|elem| if is_match!(*elem, Value::String(_)) {
Ok(())
} else {
let cause = Box::new(kind.into_error());
Err(SEK::ConfigTypeError.into_error_with_cause(cause))
})
},
_ => {
warn!("Key '{}' in store config should contain an array", key);
Err(SEK::ConfigTypeError.into_error_with_cause(Box::new(kind.into_error())))
}
})
2016-03-05 14:02:29 +00:00
}
/// Check that
/// * the top-level configuration
/// * is a table
/// * where all entries of a key `section` (eg. "hooks" or "aspects")
/// * Are maps
/// * where each has a key `key` (eg. "aspect" or "parallel")
/// * which fullfills constraint `f` (typecheck)
fn check_all_inner_maps_have_key_with<F>(store_config: &BTreeMap<String, Value>,
section: &str,
key: &str,
f: F)
-> Result<()>
2016-03-05 14:02:29 +00:00
where F: Fn(&Value) -> bool
{
store_config.get(section) // The store config has the section `section`
.ok_or_else(|| {
warn!("Store config expects section '{}' to be present, but isn't.", section);
SEK::ConfigKeyMissingError.into_error()
})
.and_then(|section_table| match *section_table { // which is
Value::Table(ref section_table) => // a table
section_table.iter().fold_result(|(inner_key, cfg)| {
match *cfg {
Value::Table(ref hook_config) => { // are tables
// with a key
let hook_aspect_is_valid = try!(hook_config.get(key)
.map(|hook_aspect| f(&hook_aspect))
.ok_or(SEK::ConfigKeyMissingError.into_error())
);
if !hook_aspect_is_valid {
Err(SEK::ConfigTypeError.into_error())
} else {
Ok(())
2016-03-05 14:02:29 +00:00
}
},
_ => {
warn!("Store config expects '{}' to be in '{}.{}', but isn't.",
key, section, inner_key);
Err(SEK::ConfigKeyMissingError.into_error())
}
}
}),
_ => {
warn!("Store config expects '{}' to be a Table, but isn't.", section);
Err(SEK::ConfigTypeError.into_error())
2016-03-05 14:02:29 +00:00
}
})
}
match *config {
2016-05-26 16:40:58 +00:00
Some(Value::Table(ref t)) => {
try!(has_key_with_string_ary(t, "store-unload-hook-aspects", SEK::ConfigKeyUnloadAspectsError));
2016-05-26 16:40:58 +00:00
try!(has_key_with_string_ary(t, "pre-create-hook-aspects", SEK::ConfigKeyPreCreateAspectsError));
try!(has_key_with_string_ary(t, "post-create-hook-aspects", SEK::ConfigKeyPostCreateAspectsError));
try!(has_key_with_string_ary(t, "pre-retrieve-hook-aspects", SEK::ConfigKeyPreRetrieveAspectsError));
try!(has_key_with_string_ary(t, "post-retrieve-hook-aspects", SEK::ConfigKeyPostRetrieveAspectsError));
try!(has_key_with_string_ary(t, "pre-update-hook-aspects", SEK::ConfigKeyPreUpdateAspectsError));
try!(has_key_with_string_ary(t, "post-update-hook-aspects", SEK::ConfigKeyPostUpdateAspectsError));
try!(has_key_with_string_ary(t, "pre-delete-hook-aspects", SEK::ConfigKeyPreDeleteAspectsError));
try!(has_key_with_string_ary(t, "post-delete-hook-aspects", SEK::ConfigKeyPostDeleteAspectsError));
2016-03-05 14:02:29 +00:00
// The section "hooks" has maps which have a key "aspect" which has a value of type
// String
try!(check_all_inner_maps_have_key_with(t, "hooks", "aspect",
|asp| is_match!(asp, &Value::String(_))));
2016-03-05 14:02:29 +00:00
// The section "aspects" has maps which have a key "parllel" which has a value of type
// Boolean
check_all_inner_maps_have_key_with(t, "aspects", "parallel",
|asp| is_match!(asp, &Value::Boolean(_)))
2016-03-05 14:02:29 +00:00
}
_ => {
warn!("Store config is no table");
Err(SEK::ConfigTypeError.into_error())
},
2016-03-05 14:02:29 +00:00
}
2016-03-05 10:37:06 +00:00
}
/// Checks whether the store configuration has a key "implicit-create" which maps to a boolean
/// value. If that key is present, the boolean is returned, otherwise false is returned.
pub fn config_implicit_store_create_allowed(config: Option<&Value>) -> bool {
config.map(|t| {
match *t {
Value::Table(ref t) => {
match t.get("implicit-create") {
Some(&Value::Boolean(b)) => b,
Some(_) => {
warn!("Key 'implicit-create' does not contain a Boolean value");
false
}
None => {
warn!("Key 'implicit-create' in store configuration missing");
false
},
}
}
_ => {
warn!("Store configuration seems to be no Table");
false
},
}
}).unwrap_or(false)
}
2016-05-26 16:40:58 +00:00
pub fn get_store_unload_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("store-unload-hook-aspects", value)
}
2016-03-05 17:16:05 +00:00
pub fn get_pre_create_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("pre-create-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_post_create_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("post-create-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_pre_retrieve_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("pre-retrieve-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_post_retrieve_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("post-retrieve-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_pre_update_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("pre-update-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_post_update_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("post-update-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_pre_delete_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("pre-delete-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 17:16:05 +00:00
pub fn get_post_delete_aspect_names(value: &Option<Value>) -> Vec<String> {
2016-03-05 15:05:31 +00:00
get_aspect_names_for_aspect_position("post-delete-hook-aspects", value)
2016-03-19 14:06:10 +00:00
}
pub fn get_pre_move_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("pre-move-hook-aspects", value)
}
pub fn get_post_move_aspect_names(value: &Option<Value>) -> Vec<String> {
get_aspect_names_for_aspect_position("post-move-hook-aspects", value)
2016-03-05 10:37:06 +00:00
}
2016-03-05 15:17:07 +00:00
#[derive(Debug)]
pub struct AspectConfig {
parallel: bool,
mutable_hooks: bool,
2016-03-05 15:17:07 +00:00
config: Value,
}
impl AspectConfig {
pub fn new(init: Value) -> AspectConfig {
debug!("Trying to parse AspectConfig from: {:?}", init);
2016-03-05 15:17:07 +00:00
let parallel = AspectConfig::is_parallel(&init);
let muthooks = AspectConfig::allows_mutable_hooks(&init);
2016-03-05 15:17:07 +00:00
AspectConfig {
config: init,
mutable_hooks: muthooks,
2016-03-05 15:17:07 +00:00
parallel: parallel,
}
}
fn is_parallel(init: &Value) -> bool {
match *init {
Value::Table(ref t) =>
2016-03-05 15:17:07 +00:00
t.get("parallel")
.map_or(false, |value| {
match *value {
Value::Boolean(b) => b,
2016-03-05 15:17:07 +00:00
_ => false,
}
}),
2016-03-05 15:17:07 +00:00
_ => false,
}
}
fn allows_mutable_hooks(init: &Value) -> bool {
match *init {
Value::Table(ref t) =>
t.get("mutable_hooks")
.map_or(false, |value| {
match *value {
Value::Boolean(b) => b,
_ => false,
}
}),
_ => false,
}
}
pub fn allow_mutable_hooks(&self) -> bool {
self.mutable_hooks
}
2016-03-05 15:17:07 +00:00
/// Get the aspect configuration for an aspect.
///
/// Pass the store configuration object, this searches in `[aspects][<aspect_name>]`.
///
/// Returns `None` if one of the keys in the chain is not available
2016-03-05 17:21:27 +00:00
pub fn get_for(v: &Option<Value>, a_name: String) -> Option<AspectConfig> {
debug!("Get aspect configuration for {:?} from {:?}", a_name, v);
let res = match *v {
Some(Value::Table(ref tabl)) => {
match tabl.get("aspects") {
Some(&Value::Table(ref tabl)) => {
tabl.get(&a_name[..]).map(|asp| AspectConfig::new(asp.clone()))
},
_ => None,
}
},
2016-03-05 17:21:27 +00:00
_ => None,
};
debug!("Found aspect configuration for {:?}: {:?}", a_name, res);
res
2016-03-05 15:17:07 +00:00
}
}
2016-03-05 17:16:05 +00:00
fn get_aspect_names_for_aspect_position(config_name: &'static str, value: &Option<Value>) -> Vec<String> {
2017-05-03 19:40:05 +00:00
use itertools::Itertools;
2016-03-05 15:05:31 +00:00
let mut v = vec![];
match *value {
Some(Value::Table(ref t)) => {
2016-03-05 15:05:31 +00:00
match t.get(config_name) {
Some(&Value::Array(ref a)) => {
for elem in a {
match *elem {
Value::String(ref s) => v.push(s.clone()),
2016-03-05 15:05:31 +00:00
_ => warn!("Non-String in configuration, inside '{}'", config_name),
}
}
},
_ => warn!("'{}' configuration key should contain Array, does not", config_name),
};
},
None => warn!("No store configuration, cannot get '{}'", config_name),
2016-03-05 15:05:31 +00:00
_ => warn!("Configuration is not a table"),
}
2017-05-03 19:40:05 +00:00
v.into_iter().unique().collect()
2016-03-05 10:37:06 +00:00
}
2016-03-05 15:05:31 +00:00
2017-05-03 15:33:52 +00:00
#[cfg(test)]
mod tests {
use toml::de::from_str as toml_from_str;
use configuration::*;
#[test]
fn test_implicit_store_create_allowed_no_toml() {
assert!(!config_implicit_store_create_allowed(None));
}
#[test]
fn test_implicit_store_create_allowed_toml_empty() {
let config = toml_from_str("").unwrap();
assert!(!config_implicit_store_create_allowed(Some(config).as_ref()));
}
#[test]
fn test_implicit_store_create_allowed_toml_false() {
let config = toml_from_str(r#"
implicit-create = false
"#).unwrap();
assert!(!config_implicit_store_create_allowed(Some(config).as_ref()));
}
#[test]
fn test_implicit_store_create_allowed_toml_true() {
let config = toml_from_str(r#"
implicit-create = true
"#).unwrap();
assert!(config_implicit_store_create_allowed(Some(config).as_ref()));
}
#[test]
fn test_get_store_unload_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
let names = get_store_unload_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_store_unload_aspect_names_empty() {
let config = toml_from_str(r#"
store-unload-hook-aspects = [ ]
"#).unwrap();
let names = get_store_unload_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_store_unload_aspect_names_one_elem() {
let config = toml_from_str(r#"
store-unload-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_store_unload_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_pre_create_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_pre_create_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_pre_create_aspect_names_empty() {
let config = toml_from_str(r#"
pre-create-hook-aspects = [ ]
"#).unwrap();
let names = get_pre_create_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_pre_create_aspect_names_one_elem() {
let config = toml_from_str(r#"
pre-create-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_pre_create_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_post_create_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_post_create_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_post_create_aspect_names_empty() {
let config = toml_from_str(r#"
post-create-hook-aspects = [ ]
"#).unwrap();
let names = get_post_create_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_post_create_aspect_names_one_elem() {
let config = toml_from_str(r#"
post-create-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_post_create_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_pre_retrieve_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_pre_retrieve_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_pre_retrieve_aspect_names_empty() {
let config = toml_from_str(r#"
pre-retrieve-hook-aspects = [ ]
"#).unwrap();
let names = get_pre_retrieve_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_pre_retrieve_aspect_names_one_elem() {
let config = toml_from_str(r#"
pre-retrieve-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_pre_retrieve_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_post_retrieve_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_post_retrieve_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_post_retrieve_aspect_names_empty() {
let config = toml_from_str(r#"
post-retrieve-hook-aspects = [ ]
"#).unwrap();
let names = get_post_retrieve_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_post_retrieve_aspect_names_one_elem() {
let config = toml_from_str(r#"
post-retrieve-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_post_retrieve_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_pre_update_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_pre_update_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_pre_update_aspect_names_empty() {
let config = toml_from_str(r#"
pre-update-hook-aspects = [ ]
"#).unwrap();
let names = get_pre_update_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_pre_update_aspect_names_one_elem() {
let config = toml_from_str(r#"
pre-update-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_pre_update_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_post_update_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_post_update_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_post_update_aspect_names_empty() {
let config = toml_from_str(r#"
post-update-hook-aspects = [ ]
"#).unwrap();
let names = get_post_update_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_post_update_aspect_names_one_elem() {
let config = toml_from_str(r#"
post-update-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_post_update_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_pre_delete_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_pre_delete_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_pre_delete_aspect_names_empty() {
let config = toml_from_str(r#"
pre-delete-hook-aspects = [ ]
"#).unwrap();
let names = get_pre_delete_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_pre_delete_aspect_names_one_elem() {
let config = toml_from_str(r#"
pre-delete-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_pre_delete_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_post_delete_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_post_delete_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_post_delete_aspect_names_empty() {
let config = toml_from_str(r#"
post-delete-hook-aspects = [ ]
"#).unwrap();
let names = get_post_delete_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_post_delete_aspect_names_one_elem() {
let config = toml_from_str(r#"
post-delete-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_post_delete_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_pre_move_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_pre_move_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_pre_move_aspect_names_empty() {
let config = toml_from_str(r#"
pre-move-hook-aspects = [ ]
"#).unwrap();
let names = get_pre_move_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_pre_move_aspect_names_one_elem() {
let config = toml_from_str(r#"
pre-move-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_pre_move_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_post_move_aspect_names_not_existent() {
let config = toml_from_str("").unwrap();
assert!(get_post_move_aspect_names(&Some(config)).is_empty());
}
#[test]
fn test_get_post_move_aspect_names_empty() {
let config = toml_from_str(r#"
post-move-hook-aspects = [ ]
"#).unwrap();
let names = get_post_move_aspect_names(&Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_post_move_aspect_names_one_elem() {
let config = toml_from_str(r#"
post-move-hook-aspects = [ "example" ]
"#).unwrap();
let names = get_post_move_aspect_names(&Some(config));
assert_eq!(1, names.len());
assert_eq!("example", names.iter().next().unwrap());
}
#[test]
fn test_get_aspect_names_for_aspect_position_arbitrary_empty() {
let config = toml_from_str(r#"
test-key = [ ]
"#).unwrap();
let names = get_aspect_names_for_aspect_position("test-key", &Some(config));
assert!(names.is_empty());
}
#[test]
fn test_get_aspect_names_for_aspect_position_arbitrary_one() {
let config = toml_from_str(r#"
test-key = [ "test-value" ]
"#).unwrap();
let names = get_aspect_names_for_aspect_position("test-key", &Some(config));
assert_eq!(1, names.len());
assert_eq!("test-value", names.iter().next().unwrap());
}
#[test]
fn test_get_aspect_names_for_aspect_position_arbitrary_duplicated() {
let config = toml_from_str(r#"
test-key = [ "test-value", "test-value" ]
"#).unwrap();
let names = get_aspect_names_for_aspect_position("test-key", &Some(config));
assert_eq!(1, names.len());
let mut iter = names.iter();
assert_eq!("test-value", iter.next().unwrap());
assert!(iter.next().is_none());
}
2017-05-03 15:33:52 +00:00
}
2016-03-05 15:05:31 +00:00