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 std::collections::BTreeMap;
use std::fs::File;
use std::io::Read;
use std::path::Path;

use toml::{Parser, Table, Value};

use data::Container;
use enums::{Error, ExtractResult};
use issues::Issues;
use item_def::ItemDef;
use notify::Notify;
use toml_builder::TomlBuilder;
use toml_data::TomlData;
use toml_iter::{Next, TomlIter};
use item_value::ItemValue;

//*************************************************************************************************
/// Defines a group in the TOML file.  Also the starting point for defining the contents of the
/// file, since the file is basically an unnamed group.
///
/// # Examples
///
/// ```
/// use valid_toml::{TomlDef, ItemDuration, ItemStr, ItemUsize};
///
/// # fn main() {
/// let file = r#"
///     password = "Abc"
///     count = 15
///
///     [child]
///     name = "test"
///     time = "30s"
///     "#;
///
/// let mut def = TomlDef::new()
///     .add(ItemStr::with_name("password").min(3).max(12))
///     .add(ItemUsize::with_name("count").optional())
///     .add(TomlDef::with_name("child")
///         .add(ItemStr::with_name("name"))
///         .add(ItemDuration::with_name("time").min("0s").max("1m")));
///
/// let file     = def.parse_toml(file).unwrap();
/// let password = file.get_str("password");
/// let count    = file.get_usize("count");
/// let name     = file.get_str("child.name");
/// let time     = file.get_duration("child.time");
///
/// assert_eq!(password, "Abc");
/// assert_eq!(count, 15);
/// assert_eq!(name, "test");
/// assert_eq!(time, 30000);
/// # }
/// ```
pub struct TomlDef
{
    //---------------------------------------------------------------------------------------------
    /// The name of the group.
    name : String,

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

    //---------------------------------------------------------------------------------------------
    /// The items in the group.
    items : BTreeMap<String, Box<ItemDef>>,

    //---------------------------------------------------------------------------------------------
    /// The items that are waiting for the data to be loaded.
    notify : BTreeMap<String, Box<Notify>>
}

impl TomlDef
{
    //*********************************************************************************************
    /// Create a new TOML definition.
    pub fn new() -> TomlDef
    {
        TomlDef {
            name     : String::from(""),
            optional : false,
            items    : BTreeMap::new(),
            notify   : BTreeMap::new()
        }
    }

    //*********************************************************************************************
    /// Create a child group inside an element.
    pub fn with_name<T:AsRef<str>>(
        name : T
        ) -> TomlDef
    {
        TomlDef {
            name     : String::from(name.as_ref()),
            optional : false,
            items    : BTreeMap::new(),
            notify   : BTreeMap::new()
        }
    }

    //*********************************************************************************************
    /// Marks the group as optional.
    pub fn optional(
        mut self,
        ) -> Self
    {
        self.optional = true;

        self
    }

    //*********************************************************************************************
    /// Adds an item or group to the definition.  This method is functionaly equivalent to the
    /// add_ref() method, except it takes self and then returns it.   This method would be used to
    /// define a TOML file using the following notation:
    ///
    /// # Examples
    ///
    /// ```
    /// use valid_toml::{TomlDef, ItemStr, ItemUsize};
    ///
    /// # fn main() {
    /// let file = r#"
    ///     password = "Abc"
    ///     count = 15
    ///     "#;
    ///
    /// let mut def = TomlDef::new()
    ///     .add(ItemStr::with_name("password").min(3).max(12))
    ///     .add(ItemUsize::with_name("count").optional());
    ///
    /// let file     = def.parse_toml(file).unwrap();
    /// let password = file.get_str("password");
    /// let count    = file.get_usize("count");
    ///
    /// assert_eq!(password, "Abc");
    /// assert_eq!(count, 15);
    ///
    /// # }
    /// ```
    pub fn add<T : ItemDef>(
        mut self,
        item : T
        ) -> Self
    {
        self.ref_add(item);

        self
    }

    //*********************************************************************************************
    /// Adds an item or group to the definition.  This method is functionaly equivalent to the
    /// add() method, except it takes a reference to self.   This method would be used to define
    /// a TOML file using the following notation:
    ///
    /// # Examples
    ///
    /// ```
    /// use valid_toml::{TomlDef, ItemStr, ItemUsize};
    ///
    /// # fn main() {
    /// let file = r#"
    ///     password = "Abc"
    ///     count = 15
    ///     "#;
    ///
    /// let mut def = TomlDef::new();
    ///
    /// def.ref_add(ItemStr::with_name("password").min(3).max(12));
    /// def.ref_add(ItemUsize::with_name("count").optional());
    ///
    /// let file     = def.parse_toml(file).unwrap();
    /// let password = file.get_str("password");
    /// let count    = file.get_usize("count");
    ///
    /// assert_eq!(password, "Abc");
    /// assert_eq!(count, 15);
    ///
    /// # }
    /// ```
    pub fn ref_add<T : ItemDef>(
        &mut self,
        item : T
        )
    {
        //-----------------------------------------------------------------------------------------
        // Make sure we're not overwriting anything.
        if self.items.contains_key(item.name())
        {
            panic!("Item [{}] already defined.", item.name());
        }

        //-----------------------------------------------------------------------------------------
        // Add the item to the map.
        let name = String::from(item.name());

        self.items.insert(name, Box::new(item));
    }

    //*********************************************************************************************
    /// Processes the TOML table element, extracting all the values and recording any errors or
    /// warnings.
    fn process<T:AsRef<str>>(
        &self,
        name  : T,
        input : &Table
        ) -> TomlBuilder
    {
        let     name    = name.as_ref();
        let mut builder = TomlBuilder::new(name);

        let mut iter = TomlIter::new(&input, &self.items, &self.notify);

        while let Some(next) = iter.next()
        {
            match next
            {
                Next::NextItem(name, value, definition, notify) => {
                    builder.add(
                        name,
                        definition.extract(value),
                        notify
                        );
                },
                Next::UnknownInput(name, _) => {
                    builder.undefined_item(name);
                },
                Next::MissingInput(name, definition, notify) => {
                    match definition.default()
                    {
                        Some(value) => builder.add(name, ExtractResult::Item(value), notify),
                        None => {
                            builder.missing_item(name, definition.is_optional())
                        }
                    }
                }
            }
        }

        builder
    }

    //*********************************************************************************************
    /// Load the TOML file from a string already loaded into memory.
    pub fn parse_toml<T:AsRef<str>>(
        &mut self,
        input : T
        ) -> Result<TomlData, Issues>
    {
        let mut parser = Parser::new(input.as_ref());

        //-----------------------------------------------------------------------------------------
        // Parse the TOML then process the data.
        if let Some(table) = parser.parse()
        {
            self.process("", &table).result()
        }
        //-----------------------------------------------------------------------------------------
        // TOML couldn't be parsed, generate the errors.
        else
        {
            let mut issues = Issues::new();

            for error in parser.errors.iter()
            {
                let (row, col) = parser.to_linecol(error.lo);

                issues.errors.push_back(
                    Error::Parse(row, col, error.desc.clone())
                    );
            }

            Err(issues)
        }
    }

    //*********************************************************************************************
    /// Load the TOML file.
    pub fn load_toml<P : AsRef<Path>>(
        &mut self,
        file : P
        ) -> Result<TomlData, Issues>
    {
        match File::open(file)
        {
            //-------------------------------------------------------------------------------------
            // The file was opened.
            Ok(mut file) => {
                let mut buf = String::with_capacity(8192);

                //---------------------------------------------------------------------------------
                // Read the file and extract the information.
                match file.read_to_string(&mut buf)
                {
                    //-----------------------------------------------------------------------------
                    // The contents of the file was read, parse it.
                    Ok(_) => {
                        self.parse_toml(buf)
                    },
                    //-----------------------------------------------------------------------------
                    // The content could not be read.
                    Err(err) => {
                        let mut issues = Issues::new();
                        issues.errors.push_back(Error::FileError(err));

                        Err(issues)
                    }
                }
            },
            //-------------------------------------------------------------------------------------
            // Error opening the file.
            Err(err) => {
                let mut issues = Issues::new();
                issues.errors.push_back(Error::FileError(err));

                Err(issues)
            }
        }
    }
}

impl Container for TomlDef
{
    //*********************************************************************************************
    /// Internal method to add a notification element to the group.
    fn add_notify(
        &mut self,
        name : String,
        notify : Box<Notify>
        )
    {
        self.notify.insert(name, notify);
    }
}

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

    //*********************************************************************************************
    /// Validate and extract the value from the TOML element.
    fn extract(
        &self,
        value : &Value
        ) -> ExtractResult
    {
        if let Some(table) = value.as_table()
        {
            ExtractResult::Group(self.process(&self.name, table))
        }
        else
        {
            ExtractResult::incorrect_type("group")
        }
    }

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

    //*********************************************************************************************
    /// Always return Err.
    fn default(&self) -> Option<ItemValue>
    {
        None
    }
}