There are probably a lot better ways to do it, but take it as a learning exercise. Basically below is the JSON InputValidation and parsing using nlohmann::json
which takes expected fields, objects arrays and verifies its presence and (optionally) parses them into an appropriate c++ structure.
inputvalidation.hpp:
namespace iv
{
template<typename _Tp>
class Field;
template<typename... _Ts>
class Object;
template<typename _Tp>
class Array;
template<typename _Old, typename _New>
class Deprecated;
namespace detail
{
template<class _Tp, template<class...> class Template>
struct is_specialization : ::std::false_type {};
template<template<class...> class Template, class... Args>
struct is_specialization<Template<Args...>, Template> : ::std::true_type {};
template<typename _Tp>
struct remove_opt { using type = _Tp; };
template<typename _Tp>
struct remove_opt<::std::optional<_Tp>> { using type = _Tp; };
template<typename _Tp>
using remove_opt_t = typename remove_opt<_Tp>::type;
template<typename _Tp>
using decay_t = ::std::decay_t<remove_opt_t<_Tp>>;
#define _CONSTEVAL constexpr
template<typename _pack, std::size_t N>
_CONSTEVAL std::size_t elem_size(std::size_t& ref, std::array<std::size_t, std::tuple_size_v<_pack>>& offsets) noexcept
{
using _Tp = std::conditional_t<
is_specialization<std::tuple_element_t<N, _pack>, std::optional>{},
std::optional<typename decay_t<std::tuple_element_t<N, _pack>>::value_type>,
typename decay_t<std::tuple_element_t<N, _pack>>::value_type>;
while (ref % alignof(_Tp) != 0)
++ref;
offsets[N] = ref;
ref += sizeof(_Tp);
return alignof(_Tp);
}
template<typename _pack, typename std::size_t... Indices>
_CONSTEVAL const std::tuple<
const size_t,
const size_t,
const std::array<std::size_t, std::tuple_size_v<_pack>>>
structure_type_helper(std::index_sequence<Indices...>)
{
std::size_t size = 0;
std::array<std::size_t, std::tuple_size_v<_pack>> offsets = {};
auto pad = (elem_size<_pack, Indices>(size, offsets) | ...);
std::size_t padding = 1;
while (pad >>= 1)
padding *= 2;
return std::make_tuple(size, padding, offsets);
}
template<typename _Tp>
struct structure_type
{
static constexpr const auto _storage = structure_type_helper<_Tp>(std::make_index_sequence<std::tuple_size_v<_Tp>>());
using type = typename std::aligned_storage_t<std::get<0>(_storage), std::get<1>(_storage)>;
static constexpr const std::array<std::size_t, std::tuple_size_v<_Tp>>& offsets = std::get<2>(_storage);
};
template<typename _Tp>
using structure_type_t = typename structure_type<_Tp>::type;
#undef _CONSTEVAL
template<typename _pack, typename std::size_t... Indices>
inline bool typeCheck(const nlohmann::json& j, const _pack& tuple, std::index_sequence<Indices...>) noexcept;
template<typename _pack, typename std::size_t... Indices>
inline void fromTuple(const _pack& tuple, const nlohmann::json& j, uint8_t* where, std::index_sequence<Indices...>);
}
template<typename _Tp>
class Field
{
static_assert(!std::is_reference_v<_Tp> && !std::is_pointer_v<_Tp>,
"Field type can not have a reference or a pointer type");
static_assert(!detail::is_specialization<_Tp, Field>{},
"Field type can not have field as a value type");
public:
using value_type = _Tp;
using comparator_type = bool(const value_type&);
constexpr Field() = default;
constexpr explicit Field(const char* tp) : _name(tp) {}
constexpr explicit Field(const char* tp, comparator_type f) : _name(tp), _comp(f) {}
bool check(const nlohmann::json& j) const noexcept
{
try
{
auto value = j.get<value_type>();
if (_comp)
{
return _comp(value);
}
return true;
}
catch (...)
{
return false;
}
}
value_type parse(const nlohmann::json& j) const
{
return j.get<value_type>();
}
constexpr const char* name() const noexcept { return _name; }
private:
const char* _name = nullptr;
comparator_type* _comp = nullptr;
};
template<typename... _Ts>
class Object
{
static_assert(sizeof...(_Ts), "Object must have at least one field");
public:
using tuple_type = std::tuple<_Ts...>;
using value_type = typename detail::structure_type_t<tuple_type>;
constexpr Object() = default;
constexpr explicit Object(const char* tp, tuple_type&& fields) : _name(tp), _pack(std::move(fields)) {}
constexpr explicit Object(const char* tp, const Object& ref) : _name(tp), _pack(ref._pack) {}
bool check(const nlohmann::json& j) const noexcept
{
if (j.is_object() != true)
{
return false;
}
if constexpr (sizeof...(_Ts) != 0)
{
return detail::typeCheck(j, _pack, std::make_index_sequence<std::tuple_size_v<tuple_type>>());
}
}
value_type parse(const nlohmann::json& j) const
{
value_type storage;
uint8_t* ptr = reinterpret_cast<uint8_t*>(&storage);
detail::fromTuple(_pack, j, ptr, std::make_index_sequence<std::tuple_size_v<tuple_type>>());
return storage;
}
constexpr const char * name() const noexcept { return _name; }
constexpr const tuple_type& pack() const noexcept { return _pack; }
private:
const char* _name = nullptr;
tuple_type _pack;
};
template<typename _Tp>
class Array
{
static_assert(!std::is_reference_v<_Tp> && !std::is_pointer_v<_Tp>,
"Can not create an array of pointers or references");
static_assert(!detail::is_specialization<_Tp, std::optional>{},
"Can not create an array of optionals");
public:
using value_type = std::vector<typename _Tp::value_type>;
constexpr Array() = default;
constexpr explicit Array(const char* tp) : _name(tp) {}
constexpr explicit Array(const char* tp, std::size_t limit) : _name(tp), _lim(limit) {}
constexpr explicit Array(const char* tp, const _Tp& check, std::size_t limit = 0)
: _name(tp), _comp(check), _lim(limit) {}
bool check(const nlohmann::json& j) const noexcept
{
if (j.is_array() != true)
{
return false;
}
if (_lim && j.size() > _lim)
{
return false;
}
for (const auto& elem : j)
{
if (_comp.check(elem) != true)
{
return false;
}
}
return true;
}
value_type parse(const nlohmann::json& j) const
{
value_type ret; ret.reserve(16);
for (const auto& elem : j)
{
ret.push_back(_comp.parse(elem));
}
return ret;
}
constexpr const char * name() const noexcept { return _name; }
constexpr std::size_t limit() const noexcept { return _lim; }
private:
const char* _name = nullptr;
_Tp _comp;
std::size_t _lim = 0;
};
template<typename _Old, typename _New>
class Deprecated
{
static_assert(!detail::is_specialization<_Old, Deprecated>{} && !detail::is_specialization<_New, Deprecated>{},
"Deprecation of deprecated type is not allowed");
public:
using depr_type = _Old;
using new_type = _New;
using value_type = std::variant<typename depr_type::value_type, typename new_type::value_type>;
constexpr Deprecated() = default;
constexpr explicit Deprecated(_Old&& depr, _New&& replacement) : _old(depr), _new(replacement) {}
bool check(const nlohmann::json& j) const noexcept
{
return _new.check(j) || _old.check(j);
}
value_type parse(const nlohmann::json& j) const
{
return _new.check(j) ? _new.parse(j) : _old.parse(j);
}
constexpr const char * name() const noexcept { return _new.name(); }
_Old _old;
_New _new;
};
namespace detail
{
#define _RUNTIME inline
template<std::size_t N, class... _Ts>
_RUNTIME const decay_t<std::tuple_element_t<N, std::tuple<_Ts...>>>& getVal(const std::tuple<_Ts...>& tuple) noexcept
{
if constexpr (is_specialization<std::decay_t<std::tuple_element_t<N, std::tuple<_Ts...>>>, std::optional>{})
{
return std::get<N>(tuple).value();
}
else
{
return std::get<N>(tuple);
}
}
template<std::size_t N, class... _Ts>
_RUNTIME bool typeCheckHelper(const nlohmann::json& j, const std::tuple<_Ts...>& tuple) noexcept
{
auto it = j.find(getVal<N>(tuple).name());
if (it == j.end() || it->is_null()) // element not found
{
if constexpr (is_specialization<std::decay_t<std::tuple_element_t<N, std::tuple<_Ts...>>>, std::optional>{})
{
return true;
}
//TODO: Handle error - field not found
return false;
}
if (getVal<N>(tuple).check(*it) == false)
{
//TODO: handle error - invalid field type
return false;
}
return true;
}
template<typename _pack, typename std::size_t... Indices>
_RUNTIME bool typeCheck(const nlohmann::json& j, const _pack& tuple, std::index_sequence<Indices...>) noexcept
{
return (typeCheckHelper<Indices>(j, tuple) && ...);
}
template<typename _Tp>
_RUNTIME const decay_t<_Tp>& getVal(const _Tp& ref)
{
if constexpr (is_specialization<std::decay_t<_Tp>, std::optional>{})
{
return ref.value();
}
else
{
return ref;
}
}
template<typename _Tp>
_RUNTIME void fromTupleImpl(_Tp&& element, const nlohmann::json& data, uint8_t* where)
{
using _Ty = std::conditional_t<
is_specialization<_Tp, std::optional>{},
std::optional<typename decay_t<_Tp>::value_type>,
typename decay_t<_Tp>::value_type>;
new (where) _Ty(getVal(element).parse(data[getVal(element).name()]));
}
template<typename _pack, typename std::size_t... Indices>
_RUNTIME void fromTuple(const _pack& tuple, const nlohmann::json& j, uint8_t* where, std::index_sequence<Indices...>)
{
((void)fromTupleImpl(std::get<Indices>(tuple), j, where + structure_type<_pack>::offsets[Indices]), ...);
}
#undef _RUNTIME
}
template<typename... _Ts>
constexpr Object<_Ts...> make_object(const char* name, _Ts&& ...args)
{
return Object<_Ts...>{name, std::make_tuple(std::forward<decltype(args)>(args)...)};
}
template<typename... _Ts>
constexpr std::optional<Object<_Ts...>> make_nullable_object(const char* name, _Ts&& ...args)
{
return Object<_Ts...>{name, std::make_tuple(std::forward<decltype(args)>(args)...)};
}
template<typename _Tp, typename... _Ts>
constexpr _Tp get(const Object<_Ts...>& ref, const nlohmann::json& j)
{
static_assert(alignof(detail::structure_type_t<std::tuple<_Ts...>>) == alignof(_Tp)
&& alignof(detail::structure_type_t<std::tuple<_Ts...>>) == alignof(_Tp),
"Invalidly calculated structure alignment and/or size.");
auto _storage = ref.parse(j);
return *reinterpret_cast<_Tp*>(&_storage);
}
}
Usage:
// this is 'read' from the file
nlohmann::json j;
j["first"] = 1;
j["second"] = "string";
j["third"]["subfield1"] = "asdf";
j["third"]["subfield2"] = 1954;
j["third"]["subfield3"].push_back(1);
j["third"]["subfield3"].push_back(8);
j["third"]["subfield3"].push_back(27);
// structure metadata - tell the validator what do you expect in JSON
auto obj = make_object("",
Field<int>{"first"},
Field<std::string>{"second"},
make_object("third",
Field<std::string>{"subfield1"},
Field<int>{"subfield2"},
Array<Field<double>>{"subfield3"}
)
);
// create a structure that reflects the JSON layout
struct s1 {
int a;
std::string b;
struct {
std::string a;
int b;
std::vector<double> c;
} c;
};
// verify that it has everything you're expecting and parse it
if (obj.check(j))
{
s1 s = get<s1>(obj, j);
// do whatever you want with the structure
}
You can also have an array of objects if you want. Go ahead and experiment if you want..
Side note: At the moment having std::vector of structure containing std::string have unexpected effects when accessing the string on clang and gcc. Works with MSVC tho. I don't know what the problem is unfortunately. I've track that to the std::vector itself so far.