dncurrency/nano/lib/jsonconfig.hpp
Wesley Shillingford f8158fc03c
Remove compiler warnings (incl from third party headers) (#2072)
* Remove compiler warnings

* Fix new warnings in test

* Remove new msvc warnings
2019-07-12 17:28:21 +01:00

504 lines
14 KiB
C++

#pragma once
#include <nano/boost/asio.hpp>
#include <nano/lib/errors.hpp>
#include <nano/lib/utility.hpp>
#include <boost/filesystem.hpp>
#include <boost/lexical_cast.hpp>
#include <boost/optional.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <fstream>
namespace nano
{
/** Type trait to determine if T is compatible with boost's lexical_cast */
template <class T>
struct is_lexical_castable : std::integral_constant<bool,
(std::is_default_constructible<T>::value && (boost::has_right_shift<std::basic_istream<wchar_t>, T>::value || boost::has_right_shift<std::basic_istream<char>, T>::value))>
{
};
/* Type descriptions are used to automatically construct configuration error messages */
// clang-format off
template <typename T> inline std::string type_desc (void) { return "an unknown type"; }
template <> inline std::string type_desc<int8_t> (void) { return "an integer between -128 and 127"; }
template <> inline std::string type_desc<uint8_t> (void) { return "an integer between 0 and 255"; }
template <> inline std::string type_desc<int16_t> (void) { return "an integer between -32768 and 32767"; }
template <> inline std::string type_desc<uint16_t> (void) { return "an integer between 0 and 65535"; }
template <> inline std::string type_desc<int32_t> (void) { return "a 32-bit signed integer"; }
template <> inline std::string type_desc<uint32_t> (void) { return "a 32-bit unsigned integer"; }
template <> inline std::string type_desc<int64_t> (void) { return "a 64-bit signed integer"; }
template <> inline std::string type_desc<uint64_t> (void) { return "a 64-bit unsigned integer"; }
template <> inline std::string type_desc<float> (void) { return "a single precision floating point number"; }
template <> inline std::string type_desc<double> (void) { return "a double precison floating point number"; }
template <> inline std::string type_desc<char> (void) { return "a character"; }
template <> inline std::string type_desc<std::string> (void) { return "a string"; }
template <> inline std::string type_desc<bool> (void) { return "a boolean"; }
template <> inline std::string type_desc<boost::asio::ip::address_v6> (void) { return "an IP address"; }
// clang-format on
/** Manages a node in a boost configuration tree. */
class jsonconfig : public nano::error_aware<>
{
public:
jsonconfig () :
tree (tree_default)
{
error = std::make_shared<nano::error> ();
}
jsonconfig (boost::property_tree::ptree & tree_a, std::shared_ptr<nano::error> error_a = nullptr) :
tree (tree_a), error (error_a)
{
if (!error)
{
error = std::make_shared<nano::error> ();
}
}
/**
* Reads a json object from the stream
* @return nano::error&, including a descriptive error message if the config file is malformed.
*/
nano::error & read (boost::filesystem::path const & path_a)
{
std::fstream stream;
open_or_create (stream, path_a.string ());
if (!stream.fail ())
{
try
{
boost::property_tree::read_json (stream, tree);
}
catch (std::runtime_error const & ex)
{
auto pos (stream.tellg ());
if (pos != std::streampos (0))
{
*error = ex;
}
}
stream.close ();
}
return *error;
}
/**
* Reads a json object from the stream and if it was changed, write the object back to the stream.
* @return nano::error&, including a descriptive error message if the config file is malformed.
*/
template <typename T>
nano::error & read_and_update (T & object, boost::filesystem::path const & path_a)
{
auto file_exists (boost::filesystem::exists (path_a));
read (path_a);
if (!*error)
{
std::fstream stream;
auto updated (false);
*error = object.deserialize_json (updated, *this);
if (!*error && updated)
{
// Before updating the config file during an upgrade make a backup first
if (file_exists)
{
create_backup_file (path_a);
}
stream.open (path_a.string (), std::ios_base::out | std::ios_base::trunc);
try
{
boost::property_tree::write_json (stream, tree);
}
catch (std::runtime_error const & ex)
{
*error = ex;
}
stream.close ();
}
}
return *error;
}
void write (boost::filesystem::path const & path_a)
{
std::fstream stream;
open_or_create (stream, path_a.string ());
write (stream);
}
void write (std::ostream & stream_a) const
{
boost::property_tree::write_json (stream_a, tree);
}
void read (std::istream & stream_a)
{
boost::property_tree::read_json (stream_a, tree);
}
/** Open configuration file, create if necessary */
void open_or_create (std::fstream & stream_a, std::string const & path_a)
{
if (!boost::filesystem::exists (path_a))
{
// Create temp stream to first create the file
std::ofstream stream (path_a);
// Set permissions before opening otherwise Windows only has read permissions
nano::set_secure_perm_file (path_a);
}
stream_a.open (path_a);
}
/** Takes a filepath, appends '_backup_<timestamp>' to the end (but before any extension) and saves that file in the same directory */
void create_backup_file (boost::filesystem::path const & filepath_a)
{
auto extension = filepath_a.extension ();
auto filename_without_extension = filepath_a.filename ().replace_extension ("");
auto orig_filepath = filepath_a;
auto & backup_path = orig_filepath.remove_filename ();
auto backup_filename = filename_without_extension;
backup_filename += "_backup_";
backup_filename += std::to_string (std::chrono::system_clock::now ().time_since_epoch ().count ());
backup_filename += extension;
auto backup_filepath = backup_path / backup_filename;
boost::filesystem::copy_file (filepath_a, backup_filepath);
}
/** Returns the boost property node managed by this instance */
boost::property_tree::ptree const & get_tree ()
{
return tree;
}
/** Returns true if the property tree node is empty */
bool empty () const
{
return tree.empty ();
}
boost::optional<jsonconfig> get_optional_child (std::string const & key_a)
{
boost::optional<jsonconfig> child_config;
auto child = tree.get_child_optional (key_a);
if (child)
{
return jsonconfig (child.get (), error);
}
return child_config;
}
jsonconfig get_required_child (std::string const & key_a)
{
auto child = tree.get_child_optional (key_a);
if (!child)
{
*error = nano::error_config::missing_value;
error->set_message ("Missing configuration node: " + key_a);
}
return child ? jsonconfig (child.get (), error) : *this;
}
jsonconfig & put_child (std::string const & key_a, nano::jsonconfig & conf_a)
{
tree.add_child (key_a, conf_a.get_tree ());
return *this;
}
jsonconfig & replace_child (std::string const & key_a, nano::jsonconfig & conf_a)
{
tree.erase (key_a);
put_child (key_a, conf_a);
return *this;
}
/** Set value for the given key. Any existing value will be overwritten. */
template <typename T>
jsonconfig & put (std::string const & key, T const & value)
{
tree.put (key, value);
return *this;
}
/** Push array element */
template <typename T>
jsonconfig & push (T const & value)
{
boost::property_tree::ptree entry;
entry.put ("", value);
tree.push_back (std::make_pair ("", entry));
return *this;
}
/** Iterate array entries */
template <typename T>
jsonconfig & array_entries (std::function<void(T)> callback)
{
for (auto & entry : tree)
{
callback (entry.second.get<T> (""));
}
return *this;
}
/** Returns true if \p key_a is present */
bool has_key (std::string const & key_a)
{
return tree.find (key_a) != tree.not_found ();
}
/** Erase the property of given key */
jsonconfig & erase (std::string const & key_a)
{
tree.erase (key_a);
return *this;
}
/** Get optional, using \p default_value if \p key is missing. */
template <typename T>
jsonconfig & get_optional (std::string const & key, T & target, T default_value)
{
get_config<T> (true, key, target, default_value);
return *this;
}
/**
* Get optional value, using the current value of \p target as the default if \p key is missing.
* @return May return nano::error_config::invalid_value
*/
template <typename T>
jsonconfig & get_optional (std::string const & key, T & target)
{
get_config<T> (true, key, target, target);
return *this;
}
/** Return a boost::optional<T> for the given key */
template <typename T>
boost::optional<T> get_optional (std::string const & key)
{
boost::optional<T> res;
if (has_key (key))
{
T target{};
get_config<T> (true, key, target, target);
res = target;
}
return res;
}
/** Get value, using the current value of \p target as the default if \p key is missing. */
template <typename T>
jsonconfig & get (std::string const & key, T & target)
{
get_config<T> (true, key, target, target);
return *this;
}
/**
* Get value of required key
* @note May set nano::error_config::missing_value if \p key is missing, nano::error_config::invalid_value if value is invalid.
*/
template <typename T>
T get (std::string const & key)
{
T target{};
get_config<T> (true, key, target, target);
return target;
}
/**
* Get required value.
* @note May set nano::error_config::missing_value if \p key is missing, nano::error_config::invalid_value if value is invalid.
*/
template <typename T>
jsonconfig & get_required (std::string const & key, T & target)
{
get_config<T> (false, key, target);
return *this;
}
/** Turn on or off automatic error message generation */
void set_auto_error_message (bool auto_a)
{
auto_error_message = auto_a;
}
nano::error & get_error () override
{
return *error;
}
protected:
template <typename T>
void construct_error_message (bool optional, std::string const & key)
{
if (auto_error_message && *error)
{
if (optional)
{
error->set_message (key + " is not " + type_desc<T> ());
}
else
{
error->set_message (key + " is required and must be " + type_desc<T> ());
}
}
}
/** Set error if not already set. That is, first error remains until get_error().clear() is called. */
template <typename T, typename V>
void conditionally_set_error (V error_a, bool optional, std::string const & key)
{
if (!*error)
{
*error = error_a;
construct_error_message<T> (optional, key);
}
}
template <typename T, typename = std::enable_if_t<nano::is_lexical_castable<T>::value>>
jsonconfig & get_config (bool optional, std::string key, T & target, T default_value = T ())
{
try
{
auto val (tree.get<std::string> (key));
if (!boost::conversion::try_lexical_convert<T> (val, target))
{
conditionally_set_error<T> (nano::error_config::invalid_value, optional, key);
}
}
catch (boost::property_tree::ptree_bad_path const &)
{
if (!optional)
{
conditionally_set_error<T> (nano::error_config::missing_value, optional, key);
}
else
{
target = default_value;
}
}
catch (std::runtime_error & ex)
{
conditionally_set_error<T> (ex, optional, key);
}
return *this;
}
// boost's lexical cast doesn't handle (u)int8_t
template <typename T, typename = std::enable_if_t<std::is_same<T, uint8_t>::value>>
jsonconfig & get_config (bool optional, std::string key, uint8_t & target, uint8_t default_value = T ())
{
int64_t tmp;
try
{
auto val (tree.get<std::string> (key));
if (!boost::conversion::try_lexical_convert<int64_t> (val, tmp) || tmp < 0 || tmp > 255)
{
conditionally_set_error<T> (nano::error_config::invalid_value, optional, key);
}
else
{
target = static_cast<uint8_t> (tmp);
}
}
catch (boost::property_tree::ptree_bad_path const &)
{
if (!optional)
{
conditionally_set_error<T> (nano::error_config::missing_value, optional, key);
}
else
{
target = default_value;
}
}
catch (std::runtime_error & ex)
{
conditionally_set_error<T> (ex, optional, key);
}
return *this;
}
template <typename T, typename = std::enable_if_t<std::is_same<T, bool>::value>>
jsonconfig & get_config (bool optional, std::string key, bool & target, bool default_value = false)
{
auto bool_conv = [this, &target, &key, optional](std::string val) {
if (val == "true")
{
target = true;
}
else if (val == "false")
{
target = false;
}
else if (!*error)
{
conditionally_set_error<T> (nano::error_config::invalid_value, optional, key);
}
};
try
{
auto val (tree.get<std::string> (key));
bool_conv (val);
}
catch (boost::property_tree::ptree_bad_path const &)
{
if (!optional)
{
conditionally_set_error<T> (nano::error_config::missing_value, optional, key);
}
else
{
target = default_value;
}
}
catch (std::runtime_error & ex)
{
conditionally_set_error<T> (ex, optional, key);
}
return *this;
}
template <typename T, typename = std::enable_if_t<std::is_same<T, boost::asio::ip::address_v6>::value>>
jsonconfig & get_config (bool optional, std::string key, boost::asio::ip::address_v6 & target, boost::asio::ip::address_v6 default_value = T ())
{
try
{
auto address_l (tree.get<std::string> (key));
boost::system::error_code bec;
target = boost::asio::ip::address_v6::from_string (address_l, bec);
if (bec)
{
conditionally_set_error<T> (nano::error_config::invalid_value, optional, key);
}
}
catch (boost::property_tree::ptree_bad_path const &)
{
if (!optional)
{
conditionally_set_error<T> (nano::error_config::missing_value, optional, key);
}
else
{
target = default_value;
}
}
return *this;
}
private:
/** The property node being managed */
boost::property_tree::ptree & tree;
boost::property_tree::ptree tree_default;
/** If set, automatically construct error messages based on parameters and type information. */
bool auto_error_message{ true };
/** We're a nano::error_aware type. Child nodes share the error state. */
std::shared_ptr<nano::error> error;
};
}