valid_toml 0.0.2

Provides the ability to load a TOML file with validation.
Documentation
/* Copyright 2016 Joshua Gentry
 *
 * Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
 * http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
 * <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
 * option. This file may not be copied, modified, or distributed
 * except according to those terms.
 */
use toml::Value;

use data::Container;
use enums::{ExtractResult, FormatDesc};
use item_def::ItemDef;
use item_value::ItemValue;
use toml_def::TomlDef;
use data::duration_decoder;
use value::I64Value;

//*************************************************************************************************
/// A duration item allows a user to provide a time duration in the TOML file.
///
/// A duration is specified in the format `##d ##h ##m ##s ##ms`.  Where:
///
/// * `d` - Specifies the number of days.
/// * 'h' - Specifies the number of hours.
/// * 'm' - Specifies the number of minutes.
/// * 's' - Specifies the number of seconds.
/// * 'ms' - Specifies the number of milliseconds.
///
/// All units are optional so if you just want to specify 5 hours and 3 seconds you could write
/// `5h 3s`.  Durations can also be negative by specifying a dash at the start of the string.  So
/// `-3m 100ms` would be negative 3 minutes and 100 milliseconds.
///
/// When reading a duration it will return the number of milliseconds, so if the user specifies
/// `1d` the value read by the application will be 86,400,000 milliseconds.
pub struct ItemDuration
{
    //---------------------------------------------------------------------------------------------
    /// The name of the item.
    name : String,

    //---------------------------------------------------------------------------------------------
    /// The min value for the duration.
    min : Option<String>,

    //---------------------------------------------------------------------------------------------
    /// The max value for the duration.
    max : Option<String>,

    //---------------------------------------------------------------------------------------------
    /// Flag indicating if the item is optional.
    optional : bool,

    //---------------------------------------------------------------------------------------------
    /// The default value for the value.
    default : Option<i64>
}

impl ItemDuration
{
    //*********************************************************************************************
    /// Constructs a new instance of the Duration object.
    pub fn with_name<T:AsRef<str>>(name : T) -> ItemDuration
    {
        ItemDuration {
            name     : String::from(name.as_ref()),
            min      : None,
            max      : None,
            optional : false,
            default  : None
        }
    }

    //*********************************************************************************************
    /// Adds the item to a group and returns an option that will receive the item's value when
    /// the file is loaded.  Basically this allows a program to receive a value laoded from
    /// the file without getting the TomlData object that was created.
    ///
    /// # Examples
    ///
    /// ```
    /// use valid_toml::{TomlDef, ItemDuration, ItemUsize};
    ///
    /// # fn main() {
    /// let file = r#"
    ///     timeout = "1m"
    ///     count   = 15
    ///     "#;
    ///
    /// let mut def = TomlDef::new();
    ///
    /// let timeout = ItemDuration::with_name("timeout").add_to(&mut def);
    /// let count   = ItemUsize::with_name("count").optional().add_to(&mut def);
    ///
    /// let file     = def.parse_toml(file).unwrap();
    ///
    /// assert_eq!(timeout.get(), 60000);
    /// assert_eq!(count.get(), 15);
    /// # }
    /// ```
    pub fn add_to(
        self,
        group : &mut TomlDef
        ) -> I64Value
    {
        let result = I64Value::new();

        group.add_notify(self.name.clone(), Box::new(result.clone()));
        group.ref_add(self);

        result
    }

    //*********************************************************************************************
    /// Defines the minimum value (inclusive) of the duration read from the file.
    ///
    /// # Panics
    ///
    /// If the value provided is not a properly formated duration this method will panic.  If the
    /// max value is specified and is smaller than the min the method will panic.
    pub fn min(
        mut self,
        min : &str
        ) -> Self
    {
        if let Some(val) = duration_decoder::process(min)
        {
            if let Some(max) = self.max.as_ref().map(|x| duration_decoder::process(x).unwrap())
            {
                if max < val
                {
                    panic!("Mininum value [{}] is greater than the maximum value [{}]", min, self.max.as_ref().unwrap());
                }
            }

            self.min = Some(String::from(min));
        }
        else
        {
            panic!("Could not decode [{}]", min);
        }

        self
    }

    //*********************************************************************************************
    /// Defines the maximum value (inclusive) of the duration read from the file.
    ///
    /// # Panics
    ///
    /// If the value provided is not a properly formated duration this method will panic.
    pub fn max<T:AsRef<str>>(
        mut self,
        max : T
        ) -> Self
    {
        if let Some(val) = duration_decoder::process(max.as_ref())
        {
            if let Some(min) = self.min.as_ref().map(|x| duration_decoder::process(x).unwrap())
            {
                if min > val
                {
                    panic!("Maximum value [{}] is less than the minimum value [{}]", max.as_ref(), self.min.as_ref().unwrap());
                }
            }

            self.max = Some(String::from(max.as_ref()));
        }
        else
        {
            panic!("Could not decode [{}]", max.as_ref());
        }

        self
    }

    //*********************************************************************************************
    /// Marks the item as optional without providing a default value.
    pub fn optional(
        mut self,
        ) -> Self
    {
        self.optional = true;

        self
    }

    //*********************************************************************************************
    /// Defines the default value for the item.  This method makes the item optional.  The default
    /// value will only be used if the parent group exists.
    ///
    /// # Panics
    ///
    /// If the value provided is not a properly formated duration this method will panic.
    pub fn default<T:AsRef<str>>(
        mut self,
        default : T
        ) -> Self
    {
        if let Some(val) = duration_decoder::process(default.as_ref())
        {
            self.default = Some(val);
        }
        else
        {
            panic!("Could not decode [{}]", default.as_ref());
        }

        self
    }
}

impl ItemDef for ItemDuration
{
    //*********************************************************************************************
    /// Returns the name of the item.
    fn name(&self) -> &str
    {
        &self.name
    }

    //*********************************************************************************************
    /// Validates and extracts the value.
    fn extract(
        &self,
        value : &Value
        ) -> ExtractResult
    {
        //-----------------------------------------------------------------------------------------
        // Extract the string.
        if let Some(value) = value.as_str()
        {
            //-------------------------------------------------------------------------------------
            // Decode the value.
            if let Some(val) = duration_decoder::process(value)
            {
                //---------------------------------------------------------------------------------
                // If there is a min value, make sure we're over or equal to it.
                if let Some(ref min) = self.min
                {
                    if val < duration_decoder::process(min).unwrap()
                    {
                        return ExtractResult::duration_underrun(self.min.clone(), self.max.clone())
                    }
                }

                //---------------------------------------------------------------------------------
                // If there is a max value, make sure we're under or equal to it.
                if let Some(ref max) = self.max
                {
                    if val > duration_decoder::process(max).unwrap()
                    {
                        return ExtractResult::duration_overflow(self.min.clone(), self.max.clone())
                    }
                }
                //---------------------------------------------------------------------------------
                // The value looks good.
                ExtractResult::Item(ItemValue::Duration(val))
            }
            //-------------------------------------------------------------------------------------
            // The value cannot be parsed.
            else
            {
                ExtractResult::cannot_parse(Some(FormatDesc::Duration))
            }
        }
        //-----------------------------------------------------------------------------------------
        // The value is not a string.
        else
        {
            ExtractResult::incorrect_type("string")
        }
    }

    //*********************************************************************************************
    /// Returns true if the item is optional.
    fn is_optional(&self) -> bool
    {
        self.optional
    }

    //*********************************************************************************************
    /// Returns the default value for the item if it exists, otherwise return Err.
    fn default(&self) -> Option<ItemValue>
    {
        self.default.map(|x|ItemValue::Duration(x))
    }
}

#[cfg(test)]
mod tests
{
    use toml::Value;
    use item_def::ItemDef;
    use item_value::ItemValue;
    use enums::{ExtractResult, ValidationError};

    macro_rules! test {
        ($item:expr, $val:expr) => ($item.extract(&Value::String(String::from($val))))
    }

    //*********************************************************************************************
    /// Test setting the values don't panic.
    #[test]
    fn set()
    {
        super::ItemDuration::with_name("a").min("12m");
        super::ItemDuration::with_name("b").max("67h");
        super::ItemDuration::with_name("c").default("123ms");
    }

    //*********************************************************************************************
    /// Test that the min value is honored.
    #[test]
    fn min()
    {
        let test = super::ItemDuration::with_name("b").min("3h 5m");

        assert_duration_underrun!(test!(test, "-65m"), "3h 5m");
        assert_duration_underrun!(test!(test, "2h"),   "3h 5m");

        assert_duration!(test!(test, "3h 5m"),      3, 5, 0, 0);
        assert_duration!(test!(test, "3h 5m 20ms"), 3, 5, 0, 20);
    }

    //*********************************************************************************************
    /// Test that the max value is honored.
    #[test]
    fn max()
    {
        let test = super::ItemDuration::with_name("b").max("3h 5m");

        assert_duration!(test!(test, "-65m"), -65, 0, 0);
        assert_duration!(test!(test, "2h"),     2, 0, 0, 0);
        assert_duration!(test!(test, "3h 5m"),  3, 5, 0, 0);

        assert_duration_overflow!(test!(test, "3h 5m 20ms"), "3h 5m");
    }

    //*********************************************************************************************
    /// Test that the min and max values are honored.
    #[test]
    fn min_max()
    {
        let test = super::ItemDuration::with_name("b").min("2h 7m").max("3h 5m");

        assert_duration_underrun!(test!(test, "-65m"), "2h 7m", "3h 5m");
        assert_duration_underrun!(test!(test, "2h"),   "2h 7m", "3h 5m");

        assert_duration!(test!(test, "2h 7m"), 2, 7, 0, 0);
        assert_duration!(test!(test, "3h 1s"), 3, 0, 1, 0);
        assert_duration!(test!(test, "3h 5m"), 3, 5, 0, 0);

        assert_duration_overflow!(test!(test, "3h 5m 20ms"), "2h 7m", "3h 5m");
    }

    //*********************************************************************************************
    /// Test that straight bad values are errors.
    #[test]
    fn validate()
    {
        let test = super::ItemDuration::with_name("b");

        assert_incorrect_type!(test.extract(&Value::Integer(27)));
        assert_incorrect_type!(test.extract(&Value::Boolean(true)));
        assert_cannot_parse!(test.extract(&Value::String(String::from("65"))));
        assert_cannot_parse!(test.extract(&Value::String(String::from("abc"))));
    }
}