2
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ I did something similar yu should check out: ThorsSerializer \$\endgroup\$ Commented Jun 2, 2020 at 2:38

1 Answer 1

1
\$\begingroup\$

Observatoion

I don't really have much to say on this code.
Looks good.If this was at work (and it had unit tests) I would say fine to check in.

The below are very minor comments.

Code Review

Please stop using the leading underscore.
Identifiers with a leading underscore are usually reserved. The rules are not obvious (you break them) but because they are not obvious you should avoid putting the _ at the beginning of an identifier.

Note: The end is fine.

see: What are the rules about using an underscore in a C++ identifier?


I very rarely see the leading :: used to specify an absolute namespace.

::std::false_type 

Sure that works.


Good use of template meta programming.


Not sure I like these.

#define _CONSTEVAL constexpr
#define _RUNTIME inline

Since they are always defined why have them at all?

Also in the class you don't need inline its redundant when used in the class. The general rule is don't use it unless you must. The only time you must is out of class definitions in the header file.


I find this hard to read:

        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>;

When I build types I do it over a couple of lines so it easy to read (by the next person to look at the code).

        using NthElement       = std::tuple_element_t<N, _pack>
        using DecayNthElement  = typename decay_t<NthElement>::value_type;
        using IsSpecNthElement = is_specialization<NthElement, std::optional>;

        using Type = std::conditional_t<
            IsSpecNthElement{},
            std::optional<DecayNthElement>,
            DecayNthElement::value_type
        >;

I would simplify this:

            if (_comp)
            {
                return _comp(value);
            }
            return true;

            // This is just as easy
            // But now I think about it yours is fine.
            return _comp ? _comp(value) : true;

\$\endgroup\$
2
  • \$\begingroup\$ are underscores followed by an uppercase letter reserved in template parameters as well? \$\endgroup\$
    – Quest
    Commented Jun 2, 2020 at 14:25
  • 1
    \$\begingroup\$ @Quest They are reserved everywhere. \$\endgroup\$ Commented Jun 2, 2020 at 15:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.