Files
transmission-mirror/libtransmission/serializable.h
Charles Kerr bf48eadaeb refactor: replace Settings class with Serializable (#7877)
* refactor: replace Settings class with Serializable

* Fields can now be declared as const static,
  so we only have to build this list once per class
  instead of once per iteration.

* Add typesafe single-property getters & setters.

* Split the converter registry into a generic standalone class.

* refactor: make Serializable::Field::getter private

refactor: make Serializable::Field::const_getter private

* docs: tweak code comments

* refactor: make Serializable::Field::Getter private

refactor: make Serializable::Field::ConstGetter private

refactor: make Serializable::Field::MemberStorage private

* chore: fix readability-identifier-naming clang-tidy warnings

* Update libtransmission/serializable.h

Co-authored-by: Yat Ho <lagoho7@gmail.com>

* Update libtransmission/serializable.h

Co-authored-by: Yat Ho <lagoho7@gmail.com>

* Update libtransmission/serializable.h

Co-authored-by: Yat Ho <lagoho7@gmail.com>

* fixup! Update libtransmission/serializable.h

---------

Co-authored-by: Yat Ho <lagoho7@gmail.com>
2025-12-09 22:09:49 -06:00

266 lines
7.8 KiB
C++

// This file Copyright © Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.
#pragma once
#include <new>
#include <optional>
#include <type_traits>
#include <typeindex>
#include <typeinfo>
#include <utility>
#include <small/map.hpp>
#include "libtransmission/quark.h"
#include "libtransmission/variant.h"
namespace libtransmission
{
/**
* Registry for `tr_variant` <-> `T` converters.
* Used by `Serializable` to serialize/deserialize fields in a class.
*/
class Serializers
{
public:
template<typename T>
using Deserialize = bool (*)(tr_variant const& src, T* ptgt);
template<typename T>
using Serialize = tr_variant (*)(T const& src);
template<typename T>
static tr_variant serialize(T const& src)
{
return converter_storage<T>.serialize(src);
}
static tr_variant serialize(void const* const psrc, std::type_index const idx)
{
return converters.at(idx).second(psrc);
}
template<typename T>
static bool deserialize(tr_variant const& src, T* const ptgt)
{
return converter_storage<T>.deserialize(src, ptgt);
}
static bool deserialize(tr_variant const& src, void* const vptgt, std::type_index const idx)
{
return converters.at(idx).first(src, vptgt);
}
template<typename T>
static void add_converter(Deserialize<T> deserialize, Serialize<T> serialize)
{
auto [key, val] = build_converter_entry<T>(deserialize, serialize);
converters.try_emplace(std::move(key), std::move(val));
}
private:
using DeserializeFunc = bool (*)(tr_variant const& src, void* tgt);
using SerializeFunc = tr_variant (*)(void const* src);
using Converters = std::pair<DeserializeFunc, SerializeFunc>;
template<typename T>
struct ConverterStorage
{
Deserialize<T> deserialize = nullptr;
Serialize<T> serialize = nullptr;
};
template<typename T>
static inline ConverterStorage<T> converter_storage;
template<typename T>
static std::pair<std::type_index, Converters> build_converter_entry(Deserialize<T> deserialize, Serialize<T> serialize)
{
converter_storage<T> = ConverterStorage<T>{ deserialize, serialize };
return { std::type_index{ typeid(T*) }, Converters{ &deserialize_impl<T>, &serialize_impl<T> } };
}
template<typename T>
static bool deserialize_impl(tr_variant const& src, void* const tgt)
{
return converter_storage<T>.deserialize(src, static_cast<T*>(tgt));
}
template<typename T>
static tr_variant serialize_impl(void const* const src)
{
return converter_storage<T>.serialize(*static_cast<T const*>(src));
}
using ConvertersMap = small::unordered_map<std::type_index, Converters>;
static ConvertersMap converters;
};
/**
* Base class for classes that are convertble to a `tr_variant`.
* Settings classes use this to load and save state from `settings.json`.
*
* Subclasses must define a field named `fields` which is an iterable
* collection of `Serializable::Field`. This is typically declared as a
* `static const std::array<Field, N>`.
*
* If your subclass has a field with a bespoke type, it must use
* `Serializers::add_converter()` to register how to serialize that type.
*/
template<typename Derived, typename Key = tr_quark>
class Serializable
{
public:
/** Update this object's state from a tr_variant. */
void load(tr_variant const& src)
{
auto const* map = src.get_if<tr_variant::Map>();
if (map == nullptr)
{
return;
}
auto* const derived = static_cast<Derived*>(this);
for (auto const& field : derived->fields)
{
if (auto const iter = map->find(field.key); iter != std::end(*map))
{
Serializers::deserialize(iter->second, field.get(derived), field.idx);
}
}
}
/** Alias for load() */
void deserialize(tr_variant const& src)
{
load(src);
}
/**
* Save this object's fields to a tr_variant.
* @return a tr_variant::Map that holds the serialized form of this object.
*/
[[nodiscard]] auto save() const
{
auto const* const derived = static_cast<Derived const*>(this);
auto map = tr_variant::Map{ std::size(derived->fields) };
for (auto const& field : derived->fields)
{
map.try_emplace(field.key, Serializers::serialize(field.get(derived), field.idx));
}
return map;
}
/** Alias for save() */
[[nodiscard]] tr_variant::Map serialize() const
{
return save();
}
/**
* Set a single property by key.
* Example: `settings_.set(TR_KEY_filename, "foo.txt");`
* @return true if the property was changed.
*/
template<typename T>
bool set(Key const& key_in, T val_in)
{
auto const idx_in = std::type_index{ typeid(T*) };
auto* const derived = static_cast<Derived*>(this);
for (auto const& field : derived->fields)
{
if (key_in == field.key && idx_in == field.idx)
{
if (T& val = *static_cast<T*>(field.get(derived)); val != val_in)
{
val = val_in;
return true; // changed
}
return false; // found but unchanged
}
}
return false; // not found
}
/**
* Get a single property by key.
* Example: `bool enabled = settings_.get<bool>(TR_KEY_incomplete_dir_enabled);`
* @return a std::optional<> which has the value if the key was found.
*/
template<typename T>
[[nodiscard]] std::optional<T> get(Key const& key_in) const
{
auto const idx_in = std::type_index{ typeid(T*) };
auto const* const derived = static_cast<Derived const*>(this);
for (auto const& field : derived->fields)
{
if (key_in == field.key && idx_in == field.idx)
{
return *static_cast<T const*>(field.get(derived));
}
}
return {};
}
protected:
struct Field
{
template<typename T>
Field(Key key_in, T Derived::* ptr)
: key{ std::move(key_in) }
, idx{ typeid(T*) }
, getter_{ &Field::template get_impl<T> }
, const_getter_{ &Field::template get_const_impl<T> }
{
static_assert(sizeof(MemberStorage) >= sizeof(T Derived::*));
static_assert(alignof(MemberStorage) >= alignof(T Derived::*));
new (&storage_) T Derived::*(ptr);
}
void* get(Derived* self) const noexcept
{
return getter_(self, &storage_);
}
void const* get(Derived const* self) const noexcept
{
return const_getter_(self, &storage_);
}
Key key;
std::type_index idx;
private:
using Getter = void* (*)(Derived*, void const*);
using ConstGetter = void const* (*)(Derived const*, void const*);
using MemberStorage = std::aligned_storage_t<sizeof(char Derived::*), alignof(char Derived::*)>;
template<typename T>
static void* get_impl(Derived* self, void const* opaque) noexcept
{
auto const member = *static_cast<T Derived::* const*>(opaque);
return &(self->*member);
}
template<typename T>
static void const* get_const_impl(Derived const* self, void const* opaque) noexcept
{
auto const member = *static_cast<T Derived::* const*>(opaque);
return &(self->*member);
}
Getter getter_;
ConstGetter const_getter_;
MemberStorage storage_;
};
private:
friend Derived;
Serializable() = default;
};
} // namespace libtransmission