Skip to content

Commit

Permalink
Improve documentation and unit tests of INI parser
Browse files Browse the repository at this point in the history
  • Loading branch information
Neverlord committed Oct 9, 2015
1 parent decfb96 commit 42d39ac
Show file tree
Hide file tree
Showing 5 changed files with 137 additions and 62 deletions.
7 changes: 3 additions & 4 deletions libcaf_core/caf/detail/parse_ini.hpp
Expand Up @@ -25,14 +25,13 @@
#include "caf/parse_config.hpp"

namespace caf {

namespace detail {

/// Parse the given input stream as INI formatted data and calls the consumer
/// with every key-value pair.
/// @param raw_data the INI formatted input stream
/// @param errors a stream of all errors which occure while parsing
/// @param consumer a function that consums the key-value pairs
/// @param raw_data Input stream of INI formatted text.
/// @param errors Output stream for parser errors.
/// @param consumer Callback consuming generated key-value pairs.
void parse_ini(std::istream& raw_data, std::ostream& errors,
config_consumer consumer);

Expand Down
9 changes: 5 additions & 4 deletions libcaf_core/caf/parse_config.hpp
Expand Up @@ -20,25 +20,26 @@
#ifndef CAF_PARSER_CONFIG_HPP
#define CAF_PARSER_CONFIG_HPP

#include <algorithm>
#include <string>
#include <algorithm>

#include "caf/variant.hpp"

namespace caf {

/// Denotes the format of a configuration file.
enum class config_format {
auto_detect,
ini
};

/// Denotes a configuration value.
using config_value = variant<std::string, double, int64_t, bool>;

/// Denotes a callback for config parser implementations.
using config_consumer = std::function<void (std::string, config_value)>;

/// parse_config
/// @param file_name
/// @param cf
/// Parse `file_name` using given file format `cf`.
void parse_config(const std::string& file_name,
config_format cf = config_format::auto_detect);

Expand Down
2 changes: 1 addition & 1 deletion libcaf_core/src/parse_config.cpp
Expand Up @@ -58,7 +58,7 @@ void parse_config(const std::string& file_name,

consumer(const consumer&) = default;
consumer& operator=(const consumer&) = default;
void operator()(std::string key, config_value value) const {
void operator()(const std::string&, config_value) const {
// send message to config server
}
};
Expand Down
49 changes: 31 additions & 18 deletions libcaf_core/src/parse_ini.cpp
Expand Up @@ -19,10 +19,10 @@

#include "caf/detail/parse_ini.hpp"

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
#include <iostream>
#include <algorithm>

#include "caf/string_algorithms.hpp"

Expand All @@ -43,12 +43,11 @@ void detail::parse_ini(std::istream& raw_data, std::ostream& errors,
continue;
// do we read a group name?
if (*bol == '[') {
if (*(eol - 1) != ']') {
if (*(eol - 1) != ']')
errors << "error in line " << ln << ": missing ] at end of line"
<< std::endl;
} else {
else
group.assign(bol + 1, eol - 1);
}
// skip further processing of this line
continue;
}
Expand Down Expand Up @@ -78,28 +77,43 @@ void detail::parse_ini(std::istream& raw_data, std::ostream& errors,
key += '.';
// ignore any whitespace between config-name and equal sign
key.insert(key.end(), bol, find_if(bol, eqs, ::isspace));
// begin-of-value, ignoreing whitespaces after '='
// begin-of-value, ignoring whitespaces after '='
auto bov = find_if_not(eqs + 1, eol, ::isspace);
// auto-detect what we are dealing with
const char* true_str = "true";
const char* false_str = "false";
if (std::equal(bov, eol, true_str)) {
constexpr const char* true_str = "true";
constexpr const char* false_str = "false";
auto icase_eq = [](char x, char y) { return tolower(x) == tolower(y); };
if (std::equal(bov, eol, true_str, icase_eq)) {
consumer(std::move(key), true);
} else if (std::equal(bov, eol, false_str)) {
} else if (std::equal(bov, eol, false_str, icase_eq)) {
consumer(std::move(key), false);
} else if (*bov == '"') {
// found a string, remove first and last char from string, start escaping
// string sequence
// end-of-string iterator
auto eos = eol - 1;
if (bov == eos) {
errors << "error in line " << ln << ": stray '\"'" << std::endl;
continue;
}
if (*eos != '"') {
errors << "error in line " << ln
<< ": string not terminated by '\"'" << std::endl;
continue;
}
// found a string, remove first and last char from string,
// start escaping string sequence
auto last_char_backslash = false;
std::string result = "";
std::string result;
// skip leading " and iterate up to the trailing "
++bov;
for (; bov+1 != eol; ++bov) {
for (; bov != eos; ++bov) {
if (last_char_backslash) {
switch (*bov) {
case 'n':
result += '\n';
break;
case 't':
result += '\t';
break;
default:
result += *bov;
}
Expand All @@ -111,8 +125,8 @@ void detail::parse_ini(std::istream& raw_data, std::ostream& errors,
}
}
if (last_char_backslash) {
errors << "error in line " << ln
<< ": trailing quotation mark was escaped" << std::endl;
errors << "warning in line " << ln
<< ": trailing quotation mark escaped" << std::endl;
}
consumer(std::move(key), std::move(result));
} else {
Expand Down Expand Up @@ -158,8 +172,7 @@ void detail::parse_ini(std::istream& raw_data, std::ostream& errors,
if (e == &*eol) {
consumer(std::move(key), is_neg ? -res : res);
} else {
errors << "error in line " << ln << ": can't parse value to double"
<< std::endl;
errors << "error in line " << ln << ": invalid value" << std::endl;
}
}
}
Expand Down
132 changes: 97 additions & 35 deletions libcaf_core/test/parse_ini.cpp
Expand Up @@ -19,25 +19,21 @@

#include "caf/config.hpp"

#define CAF_SUITE ini_parser
#define CAF_SUITE parse_ini
#include "caf/test/unit_test.hpp"

#include <iostream>
#include <sstream>

#include "caf/all.hpp"

#include "caf/detail/parse_ini.hpp"
#include "caf/detail/safe_equal.hpp"

using namespace caf;

namespace {

template <class T>
bool value_is(const config_value& cv, const T& what) {
const T* ptr = get<T>(&cv);
return ptr != nullptr && *ptr == what;
}

constexpr const char* case1 = R"__(
[scheduler]
policy="work-sharing"
Expand All @@ -63,40 +59,106 @@ buzz=1E-34
)__";

constexpr const char* case3 = R"__("
[whoops
foo="bar"
[test]
; provoke some more errors
foo bar
=42
baz=
foo="
bar="foo
some-int=42
some-string="hi there!\"
neg=-
wtf=0x3733T
hu=0779
hop=--"hiho"
)__";

struct fixture {
void load(const char* str) {
auto f = [&](std::string key, config_value value) {
values.emplace(std::move(key), std::move(value));
};
std::stringstream ss;
std::stringstream err;
ss << str;
detail::parse_ini(ss, err, f);
split(errors, err.str(), is_any_of("\n"), token_compress_on);
}

bool has_error(const char* err) {
return std::any_of(errors.begin(), errors.end(),
[=](const std::string& str) { return str == err; });
}

template <class T>
bool value_is(const char* key, const T& what) {
auto& cv = values[key];
using type =
typename std::conditional<
std::is_convertible<T, std::string>::value,
std::string,
typename std::conditional<
std::is_integral<T>::value && ! std::is_same<T, bool>::value,
int64_t,
T
>::type
>::type;
auto ptr = get<type>(&cv);
return ptr != nullptr && detail::safe_equal(*ptr, what);
}

std::map<std::string, config_value> values;
std::vector<std::string> errors;
};

} // namespace <anonymous>

CAF_TEST_FIXTURE_SCOPE(parse_ini_tests, fixture)

CAF_TEST(simple_ini) {
std::map<std::string, config_value> values;
auto f = [&](std::string key, config_value value) {
values.emplace(std::move(key), std::move(value));
};
std::stringstream ss;
std::stringstream err;
ss << case1;
detail::parse_ini(ss, err, f);
load(case1);
CAF_CHECK(errors.empty());
CAF_CHECK(values.count("nexus.port") > 0);
CAF_CHECK(value_is(values["nexus.port"], int64_t{4242}));
CAF_CHECK(value_is(values["nexus.host"], std::string{"127.0.0.1"}));
CAF_CHECK(value_is(values["scheduler.policy"], std::string{"work-sharing"}));
CAF_CHECK(value_is(values["scheduler.max-threads"], int64_t{2}));
CAF_CHECK(value_is(values["middleman.automatic-connections"], bool{true}));
CAF_CHECK(value_is("nexus.port", 4242));
CAF_CHECK(value_is("nexus.host", "127.0.0.1"));
CAF_CHECK(value_is("scheduler.policy", "work-sharing"));
CAF_CHECK(value_is("scheduler.max-threads", 2));
CAF_CHECK(value_is("middleman.automatic-connections", true));
CAF_CHECK(values.count("cash.greeting") > 0);
CAF_CHECK(value_is(values["cash.greeting"],std::string{
"Hi there, this is \"CASH!\"\n ~\\~ use at your own risk ~\\~"
}));
CAF_CHECK(value_is("cash.greeting",
"Hi there, this is \"CASH!\"\n ~\\~ use at your own risk ~\\~"));
}

CAF_TEST(numbers) {
std::map<std::string, config_value> values;
auto f = [&](std::string key, config_value value) {
values.emplace(std::move(key), std::move(value));
};
std::stringstream ss;
std::stringstream err;
ss << case2;
detail::parse_ini(ss, err, f);
CAF_CHECK(value_is(values["test.foo"], int64_t{-0xff}));
CAF_CHECK(value_is(values["test.bar"], int64_t{034}));
CAF_CHECK(value_is(values["test.baz"], double{-0.23}));
CAF_CHECK(value_is(values["test.buzz"], double{1E-34}));
load(case2);
CAF_CHECK(errors.empty());
CAF_CHECK(value_is("test.foo", -0xff));
CAF_CHECK(value_is("test.bar", 034));
CAF_CHECK(value_is("test.baz", -0.23));
CAF_CHECK(value_is("test.buzz", 1E-34));
}

CAF_TEST(errors) {
load(case3);
CAF_CHECK(has_error("error in line 2: missing ] at end of line"));
CAF_CHECK(has_error("error in line 3: value outside of a group"));
CAF_CHECK(has_error("error in line 6: no '=' found"));
CAF_CHECK(has_error("error in line 7: line starting with '='"));
CAF_CHECK(has_error("error in line 8: line ends with '='"));
CAF_CHECK(has_error("error in line 9: stray '\"'"));
CAF_CHECK(has_error("error in line 10: string not terminated by '\"'"));
CAF_CHECK(has_error("warning in line 12: trailing quotation mark escaped"));
CAF_CHECK(has_error("error in line 13: '-' is not a number"));
CAF_CHECK(has_error("error in line 14: invalid hex value"));
CAF_CHECK(has_error("error in line 15: invalid oct value"));
CAF_CHECK(has_error("error in line 16: invalid value"));
CAF_CHECK(values.size() == 2);
CAF_CHECK(value_is("test.some-int", 42));
CAF_CHECK(value_is("test.some-string", "hi there!"));
}

CAF_TEST_FIXTURE_SCOPE_END()

0 comments on commit 42d39ac

Please sign in to comment.