Files
mcc/include/mcc/mcc_keyvalue.h

616 lines
23 KiB
C++

#pragma once
/****************************************************************************************
* *
* MOUNT CONTROL COMPONENTS LIBRARY *
* *
* *
* IMPLEMENTATION OF KEY-VALUE PAIRS HOLDER *
* *
****************************************************************************************/
#include <unordered_map>
#include <unordered_set>
#include "mcc_deserializer.h"
#include "mcc_serializer.h"
#include "mcc_utils.h"
namespace mcc::impl
{
enum class MccKeyValueHolderErrorCode : int {
ERROR_OK,
ERROR_INVALID_KEY,
ERROR_INCOMPATIBLE_TYPE,
ERROR_DESERIAL,
ERROR_SERIAL
};
} // namespace mcc::impl
namespace std
{
template <>
class is_error_code_enum<mcc::impl::MccKeyValueHolderErrorCode> : public true_type
{
};
} // namespace std
namespace mcc::impl
{
// error category
struct MccKeyValueHolderCategory : public std::error_category {
MccKeyValueHolderCategory() : std::error_category() {}
const char* name() const noexcept override
{
return "MCC-KEYVALUEHOLDER-ERR-CATEGORY";
}
std::string message(int ec) const override
{
MccKeyValueHolderErrorCode err = static_cast<MccKeyValueHolderErrorCode>(ec);
switch (err) {
case MccKeyValueHolderErrorCode::ERROR_OK:
return "OK";
case MccKeyValueHolderErrorCode::ERROR_INVALID_KEY:
return "invalid key value";
case MccKeyValueHolderErrorCode::ERROR_INCOMPATIBLE_TYPE:
return "incompatible type";
case MccKeyValueHolderErrorCode::ERROR_DESERIAL:
return "deserialization error";
case MccKeyValueHolderErrorCode::ERROR_SERIAL:
return "serialization error";
default:
return "UNKNOWN";
}
}
static const MccKeyValueHolderCategory& get()
{
static const MccKeyValueHolderCategory constInst;
return constInst;
}
};
inline std::error_code make_error_code(MccKeyValueHolderErrorCode ec)
{
return std::error_code(static_cast<int>(ec), MccKeyValueHolderCategory::get());
}
// to follow std::variant requirements (not references, not array, not void)
template <typename T>
concept mcc_record_value_c = requires { !std::is_array_v<T> && !std::is_void_v<T> && !std::is_reference_v<T>; };
template <typename T>
concept mcc_keyvalue_record_c = requires(T t) {
requires std::same_as<decltype(t.key), const std::string_view>; // key name is immutable!!!
requires mcc_record_value_c<decltype(t.value)>; // value is mutable
requires mcc_record_value_c<decltype(t.default_value)>; // default value is mutable
requires mcc_serialization_params_c<decltype(t.serial_pars)>; // serialization parameters
};
// must be a std::tuple of mcc_keyvalue_record_c
template <typename T>
concept mcc_keyvalue_desc_c = requires(T t) { []<mcc_keyvalue_record_c... Ts>(std::tuple<Ts...>) {}(t); };
// use of 'inline' here to avoid '-Wsubobject-linkage' GCC-warning
inline constexpr char MCC_KV_COMMENT_SEQ_ARR[] = "#";
inline constexpr char MCC_KV_KEY_VALUE_DELIM_SEQ_ARR[] = "=";
inline constexpr char MCC_KV_COMPOSITE_VALUE_DELIM_SEQ_ARR[] = ",";
template <mcc_keyvalue_desc_c DESCR_T,
const char* COMM_SEQ = MCC_KV_COMMENT_SEQ_ARR, // comment char sequence
const char* KV_DELIM = MCC_KV_KEY_VALUE_DELIM_SEQ_ARR // key-value delimiter
>
class MccKeyValueHolder
{
public:
enum OutputPolicy {
OPOLICY_CHANGED_ONLY, // serialize only changed and previously deserialized (by fromCharRange method) key-value
// pairs
OPOLICY_FULL // serialize entire set of key-value pairs
};
static constexpr std::string_view DEFAULT_RECORD_DELIMITER{"\n"};
static constexpr std::string_view COMMENT_SEQ{COMM_SEQ == nullptr ? MCC_KV_COMMENT_SEQ_ARR
: COMM_SEQ[0] == '\0' ? MCC_KV_COMMENT_SEQ_ARR
: COMM_SEQ};
static constexpr std::string_view KEY_VALUE_DELIM{KV_DELIM == nullptr ? MCC_KV_KEY_VALUE_DELIM_SEQ_ARR
: KV_DELIM[0] == '\0' ? MCC_KV_KEY_VALUE_DELIM_SEQ_ARR
: KV_DELIM};
static constexpr size_t NUMBER_OF_RECORDS = std::tuple_size_v<DESCR_T>;
MccKeyValueHolder(DESCR_T desc) : _keyValue(desc)
{
[this]<size_t... I>(std::index_sequence<I...>) {
((_hashes[I] = utils::FNV1aHash(std::get<I>(_keyValue).key)), ...);
}(std::make_index_sequence<NUMBER_OF_RECORDS>());
_changedKey.insert_range(_hashes);
}
MccKeyValueHolder(MccKeyValueHolder const& other) : MccKeyValueHolder(other._keyValue)
{
copyInst(other);
};
MccKeyValueHolder(MccKeyValueHolder&& other)
{
moveInst(std::move(other));
};
MccKeyValueHolder& operator=(MccKeyValueHolder const& other)
{
copyInst(other);
return *this;
};
MccKeyValueHolder& operator=(MccKeyValueHolder&& other)
{
moveInst(std::move(other));
return *this;
};
constexpr std::array<std::string_view, NUMBER_OF_RECORDS> keys() const
{
return [this]<size_t... Is>(std::index_sequence<Is...>) {
return std::array<std::string_view, NUMBER_OF_RECORDS>{std::get<Is>(_keyValue).key...};
}(std::make_index_sequence<NUMBER_OF_RECORDS>{});
}
void setToDefaults()
{
[this]<size_t... Is>(std::index_sequence<Is...>) {
((std::get<Is>(_keyValue).value = std::get<Is>(_keyValue).default_value), ...);
}(std::make_index_sequence<NUMBER_OF_RECORDS>{});
}
template <typename T, std::ranges::contiguous_range R>
std::expected<T, std::error_code> getValue(R const& key) const
{
return getValue<T>(std::string_view{key});
}
template <typename T>
std::expected<T, std::error_code> getValue(std::string_view key) const
{
// ugly, but works for non-default constructible values
alignas(T) unsigned char buff[sizeof(T)];
auto err = forKey(key, [&buff](auto const& rec) -> std::error_code {
using VT = decltype(rec.value);
if constexpr (std::constructible_from<T, VT>) {
new (buff) T(rec.value);
} else {
return MccKeyValueHolderErrorCode::ERROR_INCOMPATIBLE_TYPE;
}
return MccKeyValueHolderErrorCode::ERROR_OK;
});
if (err) {
return std::unexpected(err);
} else {
auto ptr = reinterpret_cast<T*>(&buff[0]);
auto v = std::move(*ptr);
ptr->~T(); // one needs explicitly call destructor here to avvoid side effects!!!
return v;
}
}
// template <typename T>
// std::expected<T, std::error_code> getValue(std::string_view key) const
// {
// T v;
// auto err = forKey(key, [&v](auto const& rec) -> std::error_code {
// using VT = decltype(rec.value);
// if constexpr (std::convertible_to<VT, T>) {
// v = rec.value;
// } else if constexpr (std::constructible_from<T, VT>) {
// v = T(rec.value);
// } else {
// return MccKeyValueHolderErrorCode::ERROR_INCOMPATIBLE_TYPE;
// }
// return MccKeyValueHolderErrorCode::ERROR_OK;
// });
// if (err) {
// return std::unexpected(err);
// } else {
// return v;
// }
// }
template <std::ranges::contiguous_range R, typename T>
std::error_code setValue(R const& key, const T& value)
{
return setValue(std::string_view{key}, value);
}
template <typename T>
std::error_code setValue(std::string_view key, const T& value)
{
const size_t hash = utils::FNV1aHash(key);
auto ec = forHash(hash, [&value](auto& rec) -> std::error_code {
using VT = decltype(rec.value);
if constexpr (requires { rec.value = value; }) {
rec.value = value;
} else if constexpr (std::constructible_from<VT, T>) {
rec.value = VT(value);
} else if constexpr (std::ranges::range<VT> && std::ranges::range<T> &&
requires(std::ranges::range_value_t<VT> v, std::ranges::range_value_t<T> t) {
v = t;
}) {
const auto& val = std::is_pointer_v<std::decay_t<T>> ? std::span{value} : value;
size_t N = std::ranges::distance(rec.value.begin(), rec.value.end());
size_t M = std::ranges::distance(val.begin(), val.end());
auto it = std::ranges::begin(rec.value);
std::ranges::for_each_n(val.begin(), N > M ? M : N, [&it](auto const& el) {
*it = el;
++it;
});
if (N >= M) { // resize to match the size of the input range
if constexpr (!traits::mcc_non_resizable_range<VT>) {
rec.value.resize(M);
}
} else { // append if allowed
if constexpr (!traits::mcc_fixed_size_range<VT>) {
std::ranges::for_each_n(val.begin(), N, [&it](auto const& el) {
*it = el;
++it;
});
std::ranges::for_each(val | std::views::drop(N),
[&rec](auto const& el) { std::back_inserter(rec.value) = el; });
}
}
} else {
return MccKeyValueHolderErrorCode::ERROR_INCOMPATIBLE_TYPE;
}
return MccKeyValueHolderErrorCode::ERROR_OK;
});
if (!ec) {
_changedKey.insert(hash);
}
return ec;
}
template <traits::mcc_input_char_range R,
traits::mcc_input_char_range RecDelimT = std::string_view,
mcc_serialization_params_c SerParamsT = mcc_serialization_params_t>
std::error_code fromCharRange(const R& buffer,
size_t skip_records = 0, // number of skipped records (from the beginning)
RecDelimT rec_delim = DEFAULT_RECORD_DELIMITER // records delimiter
)
requires std::ranges::contiguous_range<R>
{
if constexpr (std::is_array_v<std::decay_t<R>>) { // char*, const char*
if constexpr (std::is_array_v<std::decay_t<RecDelimT>>) {
return fromCharRange(std::string_view{buffer}, std::string_view(rec_delim));
} else {
return fromCharRange(std::string_view{buffer}, std::move(rec_delim));
}
} else {
if constexpr (std::is_array_v<std::decay_t<RecDelimT>>) {
return fromCharRange(buffer, std::string_view(rec_delim));
}
}
std::error_code ec{};
std::string_view rec, key, svalue, inline_comm;
std::vector<std::optional<std::string>> head_comm;
size_t key_hash;
_headComment.clear();
_inlineComment.clear();
_changedKey.clear();
// set values to its defaults
setToDefaults();
auto recs = std::views::split(buffer, std::move(rec_delim)) | std::views::drop(skip_records);
for (auto const& el : recs) {
rec = mcc::utils::trimSpaces(el, utils::TrimType::TRIM_LEFT);
inline_comm = {};
if (rec.size()) {
auto found = std::ranges::search(rec, COMMENT_SEQ);
if (found.begin() != rec.end()) { // there was the comment sequence in record
if (found.begin() != rec.begin()) { // inline comment
inline_comm =
std::string_view{found.begin() + COMMENT_SEQ.size(), rec.end()}; // mark inline comment
} else { // head comment
head_comm.push_back(std::string{found.end(), rec.end()});
}
rec = std::string_view(rec.begin(), found.begin());
}
if (rec.size()) {
found = std::ranges::search(rec, KEY_VALUE_DELIM);
if (found.begin() != rec.begin()) {
key = trimSpaces(std::string_view(rec.begin(), found.begin()), utils::TrimType::TRIM_RIGHT);
svalue = std::string_view(found.end(), rec.end());
key_hash = utils::FNV1aHash(key);
ec = forHash(key_hash, [&key_hash, svalue, this]<typename REC_T>(REC_T& v) {
using VT = decltype(v.value);
auto err = MccDeserializer<VT>{}(svalue, v.value, v.serial_pars);
if (err) {
return MccKeyValueHolderErrorCode::ERROR_DESERIAL;
}
_changedKey.insert(key_hash);
return MccKeyValueHolderErrorCode::ERROR_OK;
});
if (ec) { // exit in the case of error
break;
}
// save head comment
_headComment[key_hash] = head_comm;
head_comm.clear();
_inlineComment[key_hash] = inline_comm;
} else { // an empty key
ec = MccKeyValueHolderErrorCode::ERROR_INVALID_KEY;
break;
}
} // just comment string starting from the beginning, just skip it (no error)
} else { // empty record, just skip it (no error)
// head_comm.push_back({el.begin(), el.end()});
head_comm.push_back(std::nullopt);
}
}
return ec;
}
template <OutputPolicy OPOLICY = MccKeyValueHolder::OPOLICY_CHANGED_ONLY,
traits::mcc_output_char_range R,
traits::mcc_input_char_range RecDelimT = std::string_view>
std::error_code toCharRange(R& output_buffer, RecDelimT rec_delim = DEFAULT_RECORD_DELIMITER)
{
if (std::is_pointer_v<std::decay_t<RecDelimT>>) { // char*, const char*, char[], conat char[]
return toCharRange(output_buffer, std::string_view{rec_delim});
}
std::error_code ec{};
#ifdef __GNUG__
// to fix GCC compilation crash for the versions < 16
#if GCC_VERSION < 160000
auto write_rec = [&output_buffer, &rec_delim, &ec, obj_ptr = this]<size_t I = 0>(this auto& self) -> void {
#else
auto write_rec = [&output_buffer, &rec_delim, &ec, this]<size_t I = 0>(this auto& self) -> void {
#endif
#endif
if constexpr (I < NUMBER_OF_RECORDS) {
if constexpr (OPOLICY == MccKeyValueHolder::OPOLICY_CHANGED_ONLY) {
#ifdef __GNUG__
// to fix GCC compilation crash for the versions < 16
#if GCC_VERSION < 160000
if (obj_ptr->_changedKey.count(obj_ptr->_hashes[I]) == 0) {
#else
if (_changedKey.count(_hashes[I]) == 0) {
#endif
#endif
self.template operator()<I + 1>();
return;
}
}
ec = formatRecord<I>(output_buffer, rec_delim);
#ifdef __GNUG__
// to fix GCC compilation crash for the versions < 16
#if GCC_VERSION < 160000
ec = obj_ptr->template formatRecord<I>(output_buffer, rec_delim);
#else
ec = formatRecord<I>(output_buffer, rec_delim);
#endif
#endif
if (ec) {
return;
}
self.template operator()<I + 1>();
}
};
write_rec();
// [&write_rec]<size_t... Is>(std::index_sequence<Is...>) {
// (write_rec.template operator()<Is>(), ...);
// }(std::make_index_sequence<NUMBER_OF_RECORDS>());
return ec;
}
protected:
DESCR_T _keyValue;
std::array<size_t, NUMBER_OF_RECORDS> _hashes{}; // key hashes
std::unordered_map<size_t, std::vector<std::optional<std::string>>>
_headComment{}; // comment string/strings before key
std::unordered_map<size_t, std::string> _inlineComment{}; // inline (after key=value pair) comment
std::unordered_set<size_t> _changedKey{}; // loaded keys (see fromCharRange)
//
// NOTE: deduced this is needed here to use "forKey" method in getter and setter (const and non-const contexts)!!!
//
std::error_code forKey(this auto&& self, std::string_view key, auto&& func)
{
return std::forward<decltype(self)>(self).template forHash<0>(utils::FNV1aHash(key),
std::forward<decltype(func)>(func));
}
//
// 'func' signature:
// std::error func(mcc_keyvalue_record_c&&)
// e.g.:
// [](auto const& rec)->std::error_code {}
// [](auto& rec)->std::error_code {}
//
template <size_t I = 0>
std::error_code forHash(this auto&& self, size_t hash, auto&& func)
{
if constexpr (I < NUMBER_OF_RECORDS) {
if (hash == std::forward<decltype(self)>(self)._hashes[I]) {
return std::forward<decltype(func)>(func)(std::get<I>(std::forward<decltype(self)>(self)._keyValue));
} else {
return std::forward<decltype(self)>(self).template forHash<I + 1>(hash,
std::forward<decltype(func)>(func));
}
}
return MccKeyValueHolderErrorCode::ERROR_INVALID_KEY;
}
template <size_t I = 0, traits::mcc_output_char_range R, traits::mcc_input_char_range RecDelimT>
std::error_code formatRecord(R& output_buffer, RecDelimT rec_delim, bool is_default = false)
{
if constexpr (I < NUMBER_OF_RECORDS) {
auto key = std::get<I>(_keyValue).key;
using val_t = std::remove_cvref_t<decltype(std::get<I>(_keyValue).value)>;
const val_t& val = is_default ? std::get<I>(_keyValue).default_value : std::get<I>(_keyValue).value;
std::string buff;
// auto err = MccSerializer<val_t>{}(buff, std::get<I>(_keyValue).value,
// std::get<I>(_keyValue).serial_pars);
auto err = MccSerializer<val_t>{}(buff, val, std::get<I>(_keyValue).serial_pars);
if (err) {
return MccKeyValueHolderErrorCode::ERROR_SERIAL;
} else {
size_t hash = _hashes[I];
// write head comment
if (_headComment[hash].size()) {
for (auto const& comm : _headComment[hash]) {
if (comm.has_value()) {
std::format_to(std::back_inserter(output_buffer), "{}{}", COMM_SEQ, comm.value());
}
std::ranges::copy(rec_delim, std::back_inserter(output_buffer));
}
}
// key and value
std::format_to(std::back_inserter(output_buffer), "{}{}{}", key, KEY_VALUE_DELIM, buff);
// inline comment
if (_inlineComment[hash].size()) {
std::format_to(std::back_inserter(output_buffer), " {}{}", COMMENT_SEQ, _inlineComment[hash]);
}
// record delimiter
std::ranges::copy(rec_delim, std::back_inserter(output_buffer));
}
}
return MccKeyValueHolderErrorCode::ERROR_OK;
}
template <size_t I = 0>
void copyInst(MccKeyValueHolder const& other)
{
if constexpr (I < NUMBER_OF_RECORDS) {
// here one needs check equality of the keys!!!
if (_hashes[I] == other._hashes[I]) {
auto& orec = std::get<I>(other._keyValue);
std::get<I>(_keyValue).value = orec.value;
std::get<I>(_keyValue).default_value = orec.default_value;
std::get<I>(_keyValue).serial_pars = orec.serial_pars;
if (auto it = other._headComment.find(_hashes[I]); it != other._headComment.end()) {
_headComment[_hashes[I]] = it->second;
}
if (auto it = other._inlineComment.find(_hashes[I]); it != other._inlineComment.end()) {
_inlineComment[_hashes[I]] = it->second;
}
_changedKey.insert(_hashes[I]);
}
copyInst<I + 1>(other);
}
}
template <size_t I = 0>
void moveInst(MccKeyValueHolder&& other)
{
if constexpr (I < NUMBER_OF_RECORDS) {
// here one needs check equality of the keys!!!
if (_hashes[I] == other._hashes[I]) {
auto& orec = std::get<I>(other._keyValue);
std::get<I>(_keyValue).value = std::move(orec.value);
std::get<I>(_keyValue).default_value = std::move(orec.default_value);
std::get<I>(_keyValue).serial_pars = std::move(orec.serial_pars);
if (auto it = other._headComment.find(_hashes[I]); it != other._headComment.end()) {
_headComment[_hashes[I]] = std::move(it->second);
}
if (auto it = other._inlineComment.find(_hashes[I]); it != other._inlineComment.end()) {
_inlineComment[_hashes[I]] = std::move(it->second);
}
_changedKey.insert(_hashes[I]);
}
moveInst<I + 1>(other);
}
}
};
template <mcc_record_value_c T>
struct mcc_simple_kv_record_t {
const std::string_view key;
T value, default_value;
mcc_serialization_params_t serial_pars;
};
template <mcc_record_value_c T>
static mcc_simple_kv_record_t<T> mcc_make_simple_kv_record(
std::string_view key,
T const& def_value,
mcc_serialization_params_t const& spars = mcc_serialization_params_t{})
{
return mcc_simple_kv_record_t<T>{key, def_value, def_value, spars};
}
} // namespace mcc::impl