Implement a simple config file parser
This commit is contained in:
parent
7744ac69f4
commit
a04dd647f6
20 changed files with 601 additions and 0 deletions
314
Garbage/Include/Garbage/SimpleConf.hpp
Normal file
314
Garbage/Include/Garbage/SimpleConf.hpp
Normal file
|
@ -0,0 +1,314 @@
|
|||
#ifndef GARBAGE_SIMPLE_CONF_HPP
|
||||
#define GARBAGE_SIMPLE_CONF_HPP
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Garbage
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
enum class SimpleConfErrorKind
|
||||
{
|
||||
ReadError,
|
||||
ConversionError,
|
||||
ParseError,
|
||||
LookupError,
|
||||
};
|
||||
|
||||
constexpr
|
||||
std::string AsString(SimpleConfErrorKind err)
|
||||
{
|
||||
switch (err) {
|
||||
case SimpleConfErrorKind::ReadError:
|
||||
return "Read Error";
|
||||
case SimpleConfErrorKind::ConversionError:
|
||||
return "Conversion Error";
|
||||
case SimpleConfErrorKind::ParseError:
|
||||
return "Parse Error";
|
||||
case SimpleConfErrorKind::LookupError:
|
||||
return "Lookup Error";
|
||||
default:
|
||||
return "Unknown Error";
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleConfError : public std::runtime_error
|
||||
{
|
||||
public:
|
||||
SimpleConfError(SimpleConfErrorKind kind, std::string_view message)
|
||||
: std::runtime_error(AsString(kind) + ": " + std::string(message))
|
||||
{}
|
||||
};
|
||||
|
||||
namespace SimpleConfImplementation
|
||||
{
|
||||
class ReadError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
ReadError(std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::ReadError, message)
|
||||
{}
|
||||
};
|
||||
|
||||
class ConversionError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
ConversionError(std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::ConversionError, message)
|
||||
{}
|
||||
};
|
||||
|
||||
class ParseError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
ParseError(std::size_t line, std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::ParseError,
|
||||
std::format("[Line {}] {}", line, message))
|
||||
{}
|
||||
};
|
||||
|
||||
class LookupError : public SimpleConfError
|
||||
{
|
||||
public:
|
||||
LookupError(std::string_view message)
|
||||
: SimpleConfError(SimpleConfErrorKind::LookupError, message)
|
||||
{}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
constexpr
|
||||
T Convert(std::string_view rawValue)
|
||||
{
|
||||
if constexpr (std::is_same_v<T, std::string>) {
|
||||
return std::string(rawValue);
|
||||
}
|
||||
|
||||
if constexpr (std::is_integral_v<T> || std::is_floating_point_v<T>) {
|
||||
T result{};
|
||||
auto [ptr, ec] = std::from_chars(rawValue.data(),
|
||||
rawValue.data() + rawValue.size(),
|
||||
result);
|
||||
|
||||
if (ptr != rawValue.data() + rawValue.size()) {
|
||||
throw ConversionError("Could not parse the whole value");
|
||||
}
|
||||
else if (ec == std::errc::invalid_argument) {
|
||||
throw ConversionError("The value is not a valid number");
|
||||
}
|
||||
else if (ec == std::errc::result_out_of_range) {
|
||||
throw ConversionError("The value is too large to store in the requested type");
|
||||
}
|
||||
else if (ec != std::errc()) {
|
||||
throw ConversionError("Unknown error");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
throw ConversionError("Can't convert the raw value to the requested type");
|
||||
}
|
||||
|
||||
using PairsType = std::unordered_map<std::string_view, std::string_view>;
|
||||
|
||||
constexpr
|
||||
void Parse(const std::string& rawConf, PairsType& pairs)
|
||||
{
|
||||
std::size_t current = 0;
|
||||
std::size_t currentLine = 1;
|
||||
|
||||
enum class State
|
||||
{
|
||||
Initial,
|
||||
ReadingKey,
|
||||
ReadingValue,
|
||||
CommentLine,
|
||||
} currentState = State::Initial;
|
||||
|
||||
std::size_t keyBegin = 0;
|
||||
std::size_t keyEnd = 0;
|
||||
std::size_t valueBegin = 0;
|
||||
std::size_t valueEnd = 0;
|
||||
|
||||
auto addPair = [&] {
|
||||
if (keyBegin == keyEnd) {
|
||||
throw ParseError(currentLine, "Empty key is not allowed");
|
||||
}
|
||||
// Allow empty values?
|
||||
|
||||
std::string_view key(rawConf.data() + keyBegin, keyEnd - keyBegin);
|
||||
std::string_view value(rawConf.data() + valueBegin, valueEnd - valueBegin);
|
||||
if (value.size() == 0) {
|
||||
value = std::string_view(nullptr, 0);
|
||||
}
|
||||
pairs[key] = value;
|
||||
|
||||
keyBegin = 0;
|
||||
keyEnd = 0;
|
||||
valueBegin = 0;
|
||||
valueEnd = 0;
|
||||
};
|
||||
|
||||
while (current < rawConf.size()) {
|
||||
switch (currentState) {
|
||||
break; case State::Initial:
|
||||
if (std::isspace(rawConf[current])) {
|
||||
if (rawConf[current] == '\n') {
|
||||
++currentLine;
|
||||
}
|
||||
++current;
|
||||
}
|
||||
else if (rawConf[current] == '#') {
|
||||
currentState = State::CommentLine;
|
||||
}
|
||||
else if (rawConf[current] == '=') {
|
||||
throw ParseError(currentLine, "Empty key is not allowed");
|
||||
}
|
||||
else {
|
||||
keyBegin = current;
|
||||
keyEnd = current + 1; // We know there is at least one char
|
||||
currentState = State::ReadingKey;
|
||||
}
|
||||
break; case State::ReadingKey:
|
||||
if (rawConf[current] == '=') {
|
||||
currentState = State::ReadingValue;
|
||||
}
|
||||
else if (rawConf[current] == '#') {
|
||||
throw ParseError(currentLine, "Unexpected comment start");
|
||||
}
|
||||
else if (!std::isspace(rawConf[current])) {
|
||||
keyEnd = current + 1;
|
||||
}
|
||||
else if (rawConf[current] == '\n') {
|
||||
throw ParseError(currentLine, "End of line while reading key");
|
||||
}
|
||||
++current;
|
||||
break; case State::ReadingValue:
|
||||
if (rawConf[current] == '\n') {
|
||||
addPair();
|
||||
|
||||
++currentLine;
|
||||
currentState = State::Initial;
|
||||
}
|
||||
else if (rawConf[current] == '=') {
|
||||
throw ParseError(currentLine, "Multiple key/value separators are illegal");
|
||||
}
|
||||
else if (rawConf[current] == '#') {
|
||||
addPair();
|
||||
currentState = State::CommentLine;
|
||||
}
|
||||
else {
|
||||
if (!std::isspace(rawConf[current])) {
|
||||
if (valueBegin == 0) {
|
||||
valueBegin = current;
|
||||
valueEnd = current + 1;
|
||||
}
|
||||
else {
|
||||
valueEnd = current + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
++current;
|
||||
break; case State::CommentLine:
|
||||
if (rawConf[current] == '\n') {
|
||||
++currentLine;
|
||||
currentState = State::Initial;
|
||||
}
|
||||
++current;
|
||||
break; default:
|
||||
throw ParseError(currentLine, "Invalid parser state (should be impossible)");
|
||||
}
|
||||
}
|
||||
|
||||
if (currentState == State::ReadingKey) {
|
||||
throw ParseError(currentLine, "End of config while reading the key");
|
||||
} else if (currentState == State::ReadingValue) {
|
||||
addPair();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleConf
|
||||
{
|
||||
public:
|
||||
struct Required {};
|
||||
|
||||
SimpleConf(const fs::path& path)
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::exists(path, ec) || ec) {
|
||||
throw ReadError("The specified file does not exist");
|
||||
}
|
||||
if (!fs::is_regular_file(path, ec) || ec) {
|
||||
throw ReadError("The the specified path is not a regular file");
|
||||
}
|
||||
|
||||
std::size_t fileSize = fs::file_size(path, ec);
|
||||
if (ec) {
|
||||
throw ReadError("Could not read the file size");
|
||||
}
|
||||
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
throw ReadError("Could not open the file for reading");
|
||||
}
|
||||
|
||||
mConfContent = std::string(fileSize, '\0');
|
||||
file.read(mConfContent.data(), fileSize);
|
||||
|
||||
Parse(mConfContent, mPairs);
|
||||
}
|
||||
|
||||
SimpleConf(std::string&& strConfig)
|
||||
: mConfContent(std::move(strConfig))
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
Parse(mConfContent, mPairs);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::optional<T> GetOptional(std::string_view key)
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
if (auto it = mPairs.find(key); it != mPairs.end()) {
|
||||
return Convert<T>(it->second);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <typename T, typename RequireValue = void>
|
||||
T Get(std::string_view key, const T& defaultValue = {})
|
||||
{
|
||||
using namespace SimpleConfImplementation;
|
||||
|
||||
if (auto it = mPairs.find(key); it != mPairs.end()) {
|
||||
return Convert<T>(it->second);
|
||||
}
|
||||
|
||||
if constexpr (std::is_same_v<RequireValue, Required>) {
|
||||
throw LookupError("Required config key not found");
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
std::string mConfContent;
|
||||
SimpleConfImplementation::PairsType mPairs;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // GARBAGE_SIMPLE_CONF_HPP
|
Loading…
Add table
Add a link
Reference in a new issue