From 348f82fc1ea9befe44603c42392c3b491fccaf37 Mon Sep 17 00:00:00 2001 From: Oleksandr Koval Date: Tue, 16 Apr 2024 17:52:28 +0300 Subject: [PATCH] implement `visit()` for sets and enums Fixes #45 --- doc/Doxyfile.in | 3 +- doc/examples.md | 267 ++++++++ doc/stringification.md | 249 +------ doc/visit_api.md | 150 +++-- sbepp/src/sbepp/sbepp.hpp | 653 ++++++++++++------- sbeppc/src/sbepp/sbeppc/types_compiler.hpp | 87 ++- test/src/sbepp/test/enum.test.cpp | 43 ++ test/src/sbepp/test/set.test.cpp | 44 ++ test/src/sbepp/test/stringification.test.cpp | 94 +-- 9 files changed, 985 insertions(+), 605 deletions(-) diff --git a/doc/Doxyfile.in b/doc/Doxyfile.in index 5c89979..6cbfa61 100644 --- a/doc/Doxyfile.in +++ b/doc/Doxyfile.in @@ -2404,7 +2404,8 @@ PREDEFINED = SBEPP_DOXYGEN \ SBEPP_HAS_INLINE_VARS=1 \ SBEPP_HAS_CONCEPTS=1 \ SBEPP_CPP17_INLINE_VAR=inline \ - "SBEPP_CPP17_NODISCARD=[[nodiscard]]" + "SBEPP_CPP17_NODISCARD=[[nodiscard]]"\ + SBEPP_DEPRECATED # If the MACRO_EXPANSION and EXPAND_ONLY_PREDEF tags are set to YES then this # tag can be used to specify a list of macro names that should be expanded. The diff --git a/doc/examples.md b/doc/examples.md index e4476fa..3fa4893 100644 --- a/doc/examples.md +++ b/doc/examples.md @@ -313,4 +313,271 @@ void make_msg( auto m = sbepp::make_view(buf.data(), buf.size()); // encode message as usual... } +``` + +--- + +## Stringification {#stringification-example} + +Here's an example of how to build stringification visitor using +[fmt](https://github.com/fmtlib/fmt) and [Visit API](#visit-api) (you can find +the full example in `stringification.test.cpp`): + +```cpp +class to_string_visitor +{ +public: + template + void on_message(T m, Cursor& c, Tag) + { + append_line("message: {}", sbepp::message_traits::name()); + sbepp::visit(sbepp::get_header(m), *this); + append_line("content: "); + indentation++; + sbepp::visit_children(m, c, *this); + indentation--; + } + + template + bool on_group(T g, Cursor& c, Tag) + { + append_line("{}:", sbepp::group_traits::name()); + indentation++; + sbepp::visit_children(g, c, *this); + indentation--; + + return {}; + } + + template + bool on_entry(T entry, Cursor& c) + { + append_line("entry:"); + indentation++; + sbepp::visit_children(entry, c, *this); + indentation--; + + return {}; + } + + template + bool on_data(T d, Tag) + { + on_array(d, sbepp::data_traits::name()); + return {}; + } + + template + bool on_field(T f, Tag) + { + on_encoding(f, sbepp::field_traits::name()); + return {}; + } + + template + bool on_type(T t, Tag) + { + on_encoding(t, sbepp::type_traits::name()); + return {}; + } + + template + bool on_enum(T e, Tag) + { + on_encoding(e, sbepp::enum_traits::name()); + return {}; + } + + template + bool on_set(T s, Tag) + { + on_encoding(s, sbepp::set_traits::name()); + return {}; + } + + template + bool on_composite(T c, Tag) + { + on_encoding(c, sbepp::composite_traits::name()); + return {}; + } + + template + void on_enum_value(auto /*e*/, Tag) + { + append("{}\n", sbepp::enum_value_traits::name()); + } + + void on_enum_value(auto e, sbepp::unknown_enum_value_tag) + { + append("unknown({})\n", sbepp::to_underlying(e)); + } + + template + void on_set_choice(const bool value, Tag) + { + if(value) + { + if(!is_first_choice) + { + append(", "); + } + is_first_choice = false; + append("{}", sbepp::set_choice_traits::name()); + } + } + + const std::string& str() const + { + return res; + } + +private: + std::string res; + std::size_t indentation{}; + bool is_first_choice{}; + + void indent() + { + fmt::format_to(std::back_inserter(res), "{:{}}", "", indentation * 4); + } + + template + void append(fmt::format_string fmt, Args&&... args) + { + fmt::format_to( + std::back_inserter(res), fmt, std::forward(args)...); + } + + template + void append_line(fmt::format_string fmt, Args&&... args) + { + indent(); + append(fmt, std::forward(args)...); + res.push_back('\n'); + } + + void on_encoding(sbepp::required_type auto t, const char* name) + { + append_line("{}: {}", name, *t); + } + + void on_encoding(sbepp::optional_type auto t, const char* name) + { + if(t) + { + append_line("{}: {}", name, *t); + } + else + { + append_line("{}: null", name); + } + } + + void on_encoding(sbepp::array_type auto a, const char* name) + { + on_array(a, name); + } + + template + void on_array(T a, const char* name) + { + if constexpr(std::is_same_v) + { + // output char arrays as C-strings. Keep in mind that they are not + // required to be null-terminated so pass size explicitly + append_line("{}: {:.{}}", name, a.data(), a.size()); + } + else + { + // use standard range-formatter + append_line("{}: {}", name, a); + } + } + + void on_encoding(sbepp::enumeration auto e, const char* name) + { + indent(); + append("{}: ", name); + sbepp::visit(e, *this); + } + + void on_encoding(sbepp::set auto s, const char* name) + { + indent(); + append("{}: (", name); + is_first_choice = true; + sbepp::visit(s, *this); + append(")\n"); + } + + void on_encoding(sbepp::composite auto c, const char* name) + { + append_line("{}:", name); + indentation++; + sbepp::visit_children(c, *this); + indentation--; + } +}; + +// usage: +auto res = sbepp::visit(message); +fmt::print("{}", res.str()); +``` + +Now, for a message like: + +```xml + + 1 + 2 + + + + 0 + 2 + + + + + + + + + + + + + + + + + + +``` + +we can get the following output: + +``` +message: msg28 +messageHeader: + blockLength: 150 + templateId: 28 + schemaId: 1 + version: 0 +content: + required: 1 + optional1: 2 + optional2: null + number: One + option: (A, B) + string: hi + array: [1, 0, 2, 0, 0, 0, 0, 0] + group: + entry: + number: 1 + entry: + number: 2 + varData: [1, 2] + varStr: ab ``` \ No newline at end of file diff --git a/doc/stringification.md b/doc/stringification.md index 1fa67f8..8c0886b 100644 --- a/doc/stringification.md +++ b/doc/stringification.md @@ -1,5 +1,9 @@ # Stringification primitives {#stringification} +@deprecated Functionality described on this page is deprecated in favor of more +generic `sbepp::visit`. You can find up-to-date stringification example +[here](#stringification-example). + Instead of providing fully-fledged stringification mechanism out of the box, `sbepp` provides only low-level primitives which in pair with [Visit API](#visit-api) can be used to build `to_string()`-like routine that @@ -7,247 +11,4 @@ uses a specific formatting/output mechanisms. For most field types their raw value is enough to produce human-readable representation. The only exceptions are enums, for which we need enumerator's name, and sets, for which we need choice names. These two cases are covered by -`sbepp::enum_to_string()` and `sbepp::visit_set()`. - -Here's an example of how to build stringification visitor using -[fmt](https://github.com/fmtlib/fmt) and [Visit API](#visit-api)(you can find -the full example in `stringification.test.cpp`): - -```cpp -class to_string_visitor -{ -public: - template - void on_message(T m, Cursor& c, Tag) - { - append("message: {}\n", sbepp::message_traits::name()); - sbepp::visit(sbepp::get_header(m), *this); - append("content: \n"); - indentation++; - sbepp::visit_children(m, c, *this); - indentation--; - } - - template - bool on_group(T g, Cursor& c, Tag) - { - append("{}:\n", sbepp::group_traits::name()); - indentation++; - sbepp::visit_children(g, c, *this); - indentation--; - - return {}; - } - - template - bool on_entry(T entry, Cursor& c) - { - append("entry:\n"); - indentation++; - sbepp::visit_children(entry, c, *this); - indentation--; - - return {}; - } - - template - bool on_data(T d, Tag) - { - on_array(d, sbepp::data_traits::name()); - return {}; - } - - template - bool on_field(T f, Tag) - { - on_encoding(f, sbepp::field_traits::name()); - return {}; - } - - template - bool on_type(T t, Tag) - { - on_encoding(t, sbepp::type_traits::name()); - return {}; - } - - template - bool on_enum(T e, Tag) - { - on_encoding(e, sbepp::enum_traits::name()); - return {}; - } - - template - bool on_set(T s, Tag) - { - on_encoding(s, sbepp::set_traits::name()); - return {}; - } - - template - bool on_composite(T c, Tag) - { - on_encoding(c, sbepp::composite_traits::name()); - return {}; - } - - const std::string& str() const - { - return res; - } - -private: - std::string res; - std::size_t indentation{}; - - template - void append(fmt::format_string fmt, Args&&... args) - { - fmt::format_to(std::back_inserter(res), "{:{}}", "", indentation * 4); - fmt::format_to( - std::back_inserter(res), fmt, std::forward(args)...); - } - - void on_encoding(sbepp::required_type auto t, const char* name) - { - append("{}: {}\n", name, *t); - } - - void on_encoding(sbepp::optional_type auto t, const char* name) - { - if(t) - { - append("{}: {}\n", name, *t); - } - else - { - append("{}: null\n", name); - } - } - - void on_encoding(sbepp::array_type auto a, const char* name) - { - on_array(a, name); - } - - template - void on_array(T a, const char* name) - { - if constexpr(std::is_same_v) - { - // output char arrays as C-strings. Keep in mind that they are not - // required to be null-terminated so pass size explicitly - append("{}: {:.{}}\n", name, a.data(), a.size()); - } - else - { - // use standard range-formatter - append("{}: {}\n", name, a); - } - } - - void on_encoding(sbepp::enumeration auto e, const char* name) - { - const auto as_string = sbepp::enum_to_string(e); - if(as_string) - { - append("{}: {}\n", name, as_string); - } - else - { - append("{}: unknown({})\n", name, sbepp::to_underlying(e)); - } - } - - void on_encoding(sbepp::set auto s, const char* name) - { - std::size_t n{}; - std::array choices{}; - sbepp::visit_set( - s, - [&n, &choices](const auto value, const auto name) - { - if(value) - { - choices[n] = name; - n++; - } - }); - - append( - "{}: ({})\n", - name, - fmt::join(choices.begin(), choices.begin() + n, ", ")); - } - - void on_encoding(sbepp::composite auto c, const char* name) - { - append("{}:\n", name); - indentation++; - sbepp::visit_children(c, *this); - indentation--; - } -}; - -// usage: -auto res = sbepp::visit(message); -fmt::print("{}", res.str()); -``` - -Now, for a message like: - -```xml - - 1 - 2 - - - - 0 - 2 - - - - - - - - - - - - - - - - - - -``` - -we can get the following output: - -``` -message: msg28 -messageHeader: - blockLength: 150 - templateId: 28 - schemaId: 1 - version: 0 -content: - required: 1 - optional1: 2 - optional2: null - number: One - option: (A, B) - string: hi - array: [1, 0, 2, 0, 0, 0, 0, 0] - group: - entry: - number: 1 - entry: - number: 2 - varData: [1, 2] - varStr: ab -``` \ No newline at end of file +`sbepp::enum_to_string()` and `sbepp::visit_set()`. \ No newline at end of file diff --git a/doc/visit_api.md b/doc/visit_api.md index d6ece8c..8f3ccc2 100644 --- a/doc/visit_api.md +++ b/doc/visit_api.md @@ -1,14 +1,53 @@ # Visit API {#visit-api} -`sbepp` provides a way to visit a message/group/entry/composite and their -children. This can be used for stringification or conversion to another format. -`sbepp` uses this mechanism to implement `sbepp::size_bytes_checked()`. It's based -on two functions `sbepp::visit()` and `sbepp::visit_children()`. They both -have the same signature: `Visitor&& visit(View, Visitor&& = {})`. Here's the -full visitor interface: +Visit API provides a way to access various SBE entities and iterate over their +inner parts. It can be used for [stringification](#stringification-example) or +conversion to another data format. `sbepp` uses this mechanism to implement +`sbepp::size_bytes_checked()` to gradually iterate over message parts and check +that it fits into a buffer. + +The API is based on two functions, `sbepp::visit()` and +`sbepp::visit_children()`. They have different behavior but both have the same +set of overloads: +- `Visitor&& f(ThingToVisit, Visitor&&)` +- `Visitor&& f(ThingToVisit, Cursor&, Visitor&&)` + +To be efficient, Visit API always uses [cursors](#cursor-accessors) under the +hood, the first overload creates it implicitly, the second relies on the +provided one. `Visitor` is an object with member functions in form of +`void/bool on_(EntityValue, [Cursor&,] EntityTag)`, where +`` corresponds to schema entity being visited, e.g. `on_message`, +`on_group`, etc. When return type is `bool`, returned value works as a +stop-flag, i.e., setting it to `true` stops further visiting. As described +above, Visit API relies on cursor accessors, additional `Cursor&` parameter +allows to pass the cursor to a nested `sbepp::visit()`/`sbepp::visit_children()` +call. `EntityTag` represents a [tag](#tags) of an entity. + +Note that visitor is not required to have all possible `on_()` +functions but only those that correspond to the structure of the object being +visited. For example, composite visitor doesn't need `on_group()` function. If +it has only type members, having `on_type()` is enough to visit it. + +In general, visiting has two parts: +- visiting the parent object itself using `sbepp::visit()` +- visiting its children using `sbepp::visit_children()` + +However, this order is not required, each call can be used independently from +the other. For example, if one wants to visit composite members but not the +composite object itself, there's nothing wrong with calling only +`sbepp::visit_children()` without `sbepp::visit()`. + +--- + +## Visiting parent views + +Message/group/entry/composite views are called "parent" in this context because +they are the only ones that can contain nested/children members. For them, +`sbepp::visit()` visits only the provided view itself. Here's a complete visitor +interface for such a call: ```cpp -class my_visitor +class parent_visitor { public: template @@ -20,6 +59,41 @@ public: template bool on_entry(T entry, Cursor& c, Tag); + template + bool on_composite(T composite, Tag); +}; +``` + +For message, group and entry visitors, `Cursor&` is provided to support +efficient children visiting. For composites it's not needed because they support +efficient member access without cursors. + +--- + +## Visiting children + +`sbepp::visit_children()` visits children of a parent view in the following way: +- for message and group entry, visits their *direct* field/group/data members in + schema order +- for group, visits all its entries +- for composite, visits its type/enum/set/composite *direct* members in schema + order + +Here's a corresponding visitor interface: + +```cpp +class children_visitor +{ +public: + template + bool on_group(T group, Cursor& c, Tag); + + template + bool on_entry(T entry, Cursor& c, Tag); + + template + bool on_composite(T composite, Tag); + template bool on_data(T data, Tag); @@ -34,48 +108,38 @@ public: template bool on_set(T set, Tag); - - template - bool on_composite(T composite, Tag); }; ``` -Most functions take value and its [tag](#tags), the `bool` return value -works as a stop flag, i.e., should be set to `true` to stop visitation. Each -function is called for the corresponding schema node so not all of them are -required, only those for nodes in the object being visited. Note that field can -be type/enum/set/composite, one needs to use corresponding traits/concepts (e.g. -`sbepp::is_type`, `sbepp::composite`) to get the actual field kind. - -The difference between `sbepp::visit()` and `sbepp::visit_children()` is that -the former visits only the root object itself, while the latter visits only its -children. To visit children recursively, additional `Cursor` parameter should be -passed to `sbepp::visit_children` because under the hood visitation always uses -cursors. *Note that when visiting message/group header from -`on_message`/`on_group`, cursor should not be passed because headers are -always skipped by cursors.* -For example, here's how to visit message members: +Note that a field can be represented by type/enum/set/composite so +traits/concepts are often needed to distinguish them for further processing +(e.g. `sbepp::is_type`, `sbepp::composite`). + +--- + +## Visiting enums and sets + +When `sbepp::visit()` is applied to a set, it visits all its known choices and +passes their values/tags to a visitor via the following function: ```cpp -class my_visitor -{ -public: - template - void on_message(T message, Cursor& c, Tag) - { - sbepp::visit_children(message, *this, c); - } - - // ... +class set_visitor{ + template + void on_set_choice(bool choice_value, Tag); }; - -auto m = sbepp::make_view(buf.data(), buf.size()); -sbepp::visit(m); ``` -Without that `sbepp::visit_children`, only `on_message` will be called. -Note that visitation can also be started from any group/entry/composite, not -only from message. +For enums, `sbepp::visit()` invokes visitor with enum value and its tag. When +value is not one of the ones defined in schema, it provides +`sbepp::unknown_enum_value_tag` as a tag. The visitor interface is: + +```cpp +class enum_visitor{ + template + void on_enum_value(T enumeration, Tag); +}; +``` -See [Stringification primitives](#stringification) section for an example how -to build your own `to_string()`-like function using Visit API. \ No newline at end of file +See [stringification example](#stringification-example) for an example of how +all of the above can be used together to implement `to_string()`-like +functionality. \ No newline at end of file diff --git a/sbepp/src/sbepp/sbepp.hpp b/sbepp/src/sbepp/sbepp.hpp index 4eab9a5..641dcc4 100644 --- a/sbepp/src/sbepp/sbepp.hpp +++ b/sbepp/src/sbepp/sbepp.hpp @@ -133,11 +133,27 @@ SBEPP_WARNINGS_OFF(); && !(defined(__clang__) && (__cplusplus < 201703L)) # define SBEPP_CPP17_NODISCARD [[nodiscard]] # endif + +# if __has_cpp_attribute(deprecated) \ + && !(defined(__clang__) && (__cplusplus < 201402L)) +# define SBEPP_DEPRECATED [[deprecated]] +# endif #endif + #ifndef SBEPP_CPP17_NODISCARD # define SBEPP_CPP17_NODISCARD #endif +#ifndef SBEPP_DEPRECATED +# if defined(__GNUC__) || defined(__clang__) +# define SBEPP_DEPRECATED __attribute__((deprecated)) +# elif defined(_MSC_VER) +# define SBEPP_DEPRECATED __declspec(deprecated) +# else +# define SBEPP_DEPRECATED +# endif +#endif + //! @brief `1` if constexpr accessors are supported, `0` otherwise #if SBEPP_HAS_BITCAST && SBEPP_HAS_CONSTEXPR_ALGORITHMS # define SBEPP_HAS_CONSTEXPR_ACCESSORS 1 @@ -4552,6 +4568,16 @@ constexpr View::type> return {ptr, size}; } +/** + * @brief Tag for unknown enum values + * + * Passed to `on_enum_value` callback by `sbepp::visit()` for unknown enum + * values. `sbepp::enum_value_traits` is not defined for it. + */ +struct unknown_enum_value_tag +{ +}; + namespace detail { // taken from https://stackoverflow.com/a/34672753 @@ -4575,253 +4601,6 @@ concept derived_from_tmp = is_base_of_tmp::value; #endif } // namespace detail -/** - * @brief Visit a view using given cursor - * - * @tparam Visitor visitor type - * @param view view to visit - * @param c cursor - * @param visitor visitor - * @return forwarded reference to `visitor` - */ -template -SBEPP_CPP14_CONSTEXPR Visitor&& - visit(View view, Cursor& c, Visitor&& visitor = {}) -{ - view(detail::visit_tag{}, visitor, c); - return std::forward(visitor); -} - -/** - * @brief Visits a view - * - * @tparam Visitor visitor - * @param view view to visit - * @param visitor visitor - * @return forwarded reference to `visitor` - */ -template -SBEPP_CPP14_CONSTEXPR Visitor&& visit(View view, Visitor&& visitor = {}) -{ - auto c = sbepp::init_cursor(view); - return sbepp::visit(view, c, std::forward(visitor)); -} - -/** - * @brief Visit view's children using provided cursor - * - * @tparam Visitor visitor type - * @param view view to visit - * @param c cursor - * @param visitor visitor - * @return forwarded reference to `visitor` - */ -template -SBEPP_CPP14_CONSTEXPR Visitor&& - visit_children(View view, Cursor& c, Visitor&& visitor = {}) -{ - view(detail::visit_children_tag{}, visitor, c); - return std::forward(visitor); -} - -/** - * @brief Visit view's children - * - * @tparam Visitor visitor type - * @param view view to visit - * @param visitor visitor - * @return forwarded reference to `visitor` - */ -template -SBEPP_CPP14_CONSTEXPR Visitor&& - visit_children(View view, Visitor&& visitor = {}) -{ - auto c = sbepp::init_cursor(view); - return sbepp::visit_children(view, c, std::forward(visitor)); -} - -namespace detail -{ -class size_bytes_checked_visitor -{ -public: - constexpr explicit size_bytes_checked_visitor( - const std::size_t size) noexcept - : size{size} - { - } - - template - SBEPP_CPP14_CONSTEXPR void on_message(T m, Cursor& c, Tag) noexcept - { - const auto header = sbepp::get_header(m); - const auto header_size = sbepp::size_bytes(header); - if(!validate_and_subtract(header_size)) - { - return; - } - - if(!validate_and_subtract(*header.blockLength())) - { - return; - } - - sbepp::visit_children(m, c, *this); - } - - template - SBEPP_CPP14_CONSTEXPR bool on_group(T g, Cursor& c, Tag) noexcept - { - const auto header = sbepp::get_header(g); - const auto header_size = sbepp::size_bytes(header); - if(!validate_and_subtract(header_size)) - { - return true; - } - - const auto prev_block_length = - set_group_block_length(*header.blockLength()); - sbepp::visit_children(g, c, *this); - set_group_block_length(prev_block_length); - - return !is_valid(); - } - - template - SBEPP_CPP14_CONSTEXPR bool on_entry(T e, Cursor& c) noexcept - { - if(!validate_and_subtract(group_block_length)) - { - return true; - } - - return !sbepp::visit_children(e, c, *this).is_valid(); - } - - template - SBEPP_CPP14_CONSTEXPR bool on_data(T d, Tag) noexcept - { - return !validate_and_subtract(sbepp::size_bytes(d)); - } - - // ignore them all because we validate `blockLength` - template - constexpr bool on_field(T, Tag) const noexcept - { - return {}; - } - - constexpr bool is_valid() const noexcept - { - return valid; - } - - // returns previous value - SBEPP_CPP14_CONSTEXPR std::size_t - set_group_block_length(const std::size_t block_length) noexcept - { - auto prev = group_block_length; - group_block_length = block_length; - return prev; - } - - constexpr std::size_t get_size() const noexcept - { - return size; - } - -private: - std::size_t size; - bool valid{true}; - // current group's blockLength, used to validate entry - std::size_t group_block_length{}; - - SBEPP_CPP14_CONSTEXPR bool - validate_and_subtract(const std::size_t n) noexcept - { - if(size < n) - { - valid = false; - } - else - { - size -= n; - } - - return valid; - } -}; -} // namespace detail - -//! @brief Result type of `size_bytes_checked` -struct size_bytes_checked_result -{ - //! Denotes whether `size` is valid - bool valid; - //! Calculated size, valid only if `valid == true` - std::size_t size; -}; - -// can be used with message/group -/** - * @brief Calculate `view` size with additional safety checks. - * - * Similar to `size_bytes()` but stops if `view` cannot fit into the given - * `size`. Useful to check that incoming message is fully contained within given - * buffer. - * - * @param view message or group view - * @param size buffer size - */ -template -SBEPP_CPP20_CONSTEXPR size_bytes_checked_result - size_bytes_checked(View view, std::size_t size) noexcept -{ - // `init_cursor` skips header, we need to ensure there's enough space for it - if(!sbepp::addressof(view) || (size < detail::get_header_size(view))) - { - return {}; - } - - detail::size_bytes_checked_visitor visitor{size}; - auto c = sbepp::init_cursor(view); - sbepp::visit(view, c, visitor); - if(visitor.is_valid()) - { - return {true, size - visitor.get_size()}; - } - return {}; -} - -/** - * @brief Converts enum to string - * - * @param e enum to convert - * @returns pointer to a null-terminated string representing an enumerator's - * name or `nullptr` if `e` holds unknown value - */ -template -constexpr auto enum_to_string(const E e) noexcept - -> decltype(tag_invoke(detail::enum_to_str_tag{}, e)) -{ - return tag_invoke(detail::enum_to_str_tag{}, e); -} - -/** - * @brief Visits set choices in order of their declaration - * - * @param s set to visit - * @param visitor visitor. Must have signature - * `void (bool choice_value, const char* choice_name)` - * @return forwarded reference to `visitor` - */ -template -constexpr auto visit_set(const Set s, Visitor&& visitor) noexcept - -> decltype(s(detail::visit_set_tag{}, std::forward(visitor))) -{ - return s(detail::visit_set_tag{}, std::forward(visitor)); -} - namespace detail { template @@ -4871,8 +4650,10 @@ struct is_enum : std::false_type template struct is_enum< T, - detail::void_t()))>> - : std::true_type + detail::void_t(), + std::declval(), + std::declval()))>> : std::true_type { }; @@ -4902,6 +4683,10 @@ using is_group = std::integral_constant< bool, is_flat_group::value || is_nested_group::value>; +//! @brief Checks if `T` is a group entry +template +using is_group_entry = detail::is_base_of_tmp; + namespace detail { template @@ -5028,6 +4813,376 @@ concept group = is_group_v; template concept data = is_data_v; #endif + +namespace detail +{ +template +using is_visitable_view = std::integral_constant< + bool, + is_message::value || is_group::value || is_group_entry::value + || is_composite::value>; +} + +/** + * @brief Visit a view using given cursor + * + * @tparam Visitor visitor type + * @param view message, group, entry or composite view + * @param c cursor, passed as is to `visitor` + * @param visitor visitor + * @return forwarded reference to `visitor` + * + * @see @ref visit-api + */ +template< + typename Visitor, + typename View, + typename Cursor, + typename = detail::enable_if_t::value>> +SBEPP_CPP14_CONSTEXPR Visitor&& + visit(View view, Cursor& c, Visitor&& visitor = {}) +{ + view(detail::visit_tag{}, visitor, c); + return std::forward(visitor); +} + +/** + * @brief Visits a view + * + * @tparam Visitor visitor type + * @param view message, group, entry or composite view + * @param visitor visitor + * @return forwarded reference to `visitor` + * + * @see @ref visit-api + */ +template< + typename Visitor, + typename View, + typename = detail::enable_if_t::value>> +SBEPP_CPP14_CONSTEXPR Visitor&& visit(View view, Visitor&& visitor = {}) +{ + auto c = sbepp::init_cursor(view); + return sbepp::visit(view, c, std::forward(visitor)); +} + +#ifndef SBEPP_DOXYGEN +template +SBEPP_CPP14_CONSTEXPR detail::enable_if_t::value, Visitor&&> + visit(Set s, Visitor&& visitor = {}) +{ + s(detail::visit_tag{}, visitor); + return std::forward(visitor); +} + +template +SBEPP_CPP14_CONSTEXPR detail::enable_if_t::value, Visitor&&> + visit(Enum e, Visitor&& visitor = {}) +{ + tag_invoke(detail::visit_tag{}, e, visitor); + return std::forward(visitor); +} + +#else + +/** + * @brief Visits set choices + * + * @tparam Visitor visitor type + * @param s set to visit + * @param visitor visitor instance, must have `on_set_choice` member function + * with the signature equivalent to `void on_set_choice(bool, ChoiceTag)`. + * @return forwarded reference to `visitor` + * + * Visits set choices in order of their schema declaration. For each choice + * calls `visitor.on_set_choice(choice_value, ChoiceTag{})`, where + * `choice_value` is the `bool` value of a choice and `ChoiceTag` is its tag + * that can be passed to `set_choice_traits`. + * + * @see @ref visit-api + */ +template +SBEPP_CPP14_CONSTEXPR Visitor&& visit(Set s, Visitor&& visitor = {}); + +/** + * @brief Visits enum value + * + * @tparam Visitor visitor type + * @param e enum value to visit + * @param visitor visitor instance, must have `on_enum_value` member function + * with the signature equivalent to `void on_enum_value(Enum, EnumValueTag)` + * @return forwarded reference to `visitor` + * + * If `e` is one of `validValue`-s from schema, calls + * `visitor.on_enum_value(e, EnumValueTag{})` where `EnumValueTag` is a matched + * value's tag that can be passed to `enum_value_traits`. Otherwise, calls + * `visitor.on_enum_value(e, sbepp::unknown_enum_value_tag{})`. + * + * @see @ref visit-api + */ +template +SBEPP_CPP14_CONSTEXPR Visitor&& visit(Enum e, Visitor&& visitor = {}); +#endif + +/** + * @brief Visit view's children using provided cursor + * + * @tparam Visitor visitor type + * @param view message, group, entry or composite view + * @param c cursor, ignored for composites, otherwise must point to the first + * `view`'s child + * @param visitor visitor + * @return forwarded reference to `visitor` + * + * @see @ref visit-api + */ +template< + typename Visitor, + typename View, + typename Cursor, + typename = detail::enable_if_t::value>> +SBEPP_CPP14_CONSTEXPR Visitor&& + visit_children(View view, Cursor& c, Visitor&& visitor = {}) +{ + view(detail::visit_children_tag{}, visitor, c); + return std::forward(visitor); +} + +/** + * @brief Visit view's children + * + * @tparam Visitor visitor type + * @param view message, group, entry or composite view + * @param visitor visitor + * @return forwarded reference to `visitor` + * + * @see @ref visit-api + */ +template< + typename Visitor, + typename View, + typename = detail::enable_if_t::value>> +SBEPP_CPP14_CONSTEXPR Visitor&& + visit_children(View view, Visitor&& visitor = {}) +{ + auto c = sbepp::init_cursor(view); + return sbepp::visit_children(view, c, std::forward(visitor)); +} + +namespace detail +{ +class enum_to_string_visitor +{ +public: + template + SBEPP_CPP14_CONSTEXPR void on_enum_value(auto /*e*/, Tag) noexcept + { + name_value = sbepp::enum_value_traits::name(); + } + + SBEPP_CPP14_CONSTEXPR void + on_enum_value(auto /*e*/, sbepp::unknown_enum_value_tag) noexcept + { + name_value = nullptr; + } + + constexpr const char* name() const noexcept + { + return name_value; + } + +private: + const char* name_value; +}; +} // namespace detail + +/** + * @brief Converts enum to string + * + * @param e enum to convert + * @returns pointer to a null-terminated string representing an enumerator's + * name or `nullptr` if `e` holds unknown value + * + * @deprecated Deprecated in favor of `sbepp::visit`, will be removed in the + * next major update. + */ +template::value>> +SBEPP_DEPRECATED constexpr const char* enum_to_string(const E e) noexcept +{ + return visit(e).name(); +} + +/** + * @brief Visits set choices in order of their declaration + * + * @param s set to visit + * @param visitor visitor. Must have signature + * `void (bool choice_value, const char* choice_name)` + * @return forwarded reference to `visitor` + * + * @deprecated Deprecated in favor of `sbepp::visit`, will be removed in the + * next major update. + */ +template +SBEPP_DEPRECATED constexpr auto + visit_set(const Set s, Visitor&& visitor) noexcept + -> decltype(s(detail::visit_set_tag{}, std::forward(visitor))) +{ + return s(detail::visit_set_tag{}, std::forward(visitor)); +} + +namespace detail +{ +class size_bytes_checked_visitor +{ +public: + constexpr explicit size_bytes_checked_visitor( + const std::size_t size) noexcept + : size{size} + { + } + + template + SBEPP_CPP14_CONSTEXPR void on_message(T m, Cursor& c, Tag) noexcept + { + const auto header = sbepp::get_header(m); + const auto header_size = sbepp::size_bytes(header); + if(!validate_and_subtract(header_size)) + { + return; + } + + if(!validate_and_subtract(*header.blockLength())) + { + return; + } + + sbepp::visit_children(m, c, *this); + } + + template + SBEPP_CPP14_CONSTEXPR bool on_group(T g, Cursor& c, Tag) noexcept + { + const auto header = sbepp::get_header(g); + const auto header_size = sbepp::size_bytes(header); + if(!validate_and_subtract(header_size)) + { + return true; + } + + const auto prev_block_length = + set_group_block_length(*header.blockLength()); + sbepp::visit_children(g, c, *this); + set_group_block_length(prev_block_length); + + return !is_valid(); + } + + template + SBEPP_CPP14_CONSTEXPR bool on_entry(T e, Cursor& c) noexcept + { + if(!validate_and_subtract(group_block_length)) + { + return true; + } + + return !sbepp::visit_children(e, c, *this).is_valid(); + } + + template + SBEPP_CPP14_CONSTEXPR bool on_data(T d, Tag) noexcept + { + return !validate_and_subtract(sbepp::size_bytes(d)); + } + + // ignore them all because we validate `blockLength` + template + constexpr bool on_field(T, Tag) const noexcept + { + return {}; + } + + constexpr bool is_valid() const noexcept + { + return valid; + } + + // returns previous value + SBEPP_CPP14_CONSTEXPR std::size_t + set_group_block_length(const std::size_t block_length) noexcept + { + auto prev = group_block_length; + group_block_length = block_length; + return prev; + } + + constexpr std::size_t get_size() const noexcept + { + return size; + } + +private: + std::size_t size; + bool valid{true}; + // current group's blockLength, used to validate entry + std::size_t group_block_length{}; + + SBEPP_CPP14_CONSTEXPR bool + validate_and_subtract(const std::size_t n) noexcept + { + if(size < n) + { + valid = false; + } + else + { + size -= n; + } + + return valid; + } +}; +} // namespace detail + +//! @brief Result type of `size_bytes_checked` +struct size_bytes_checked_result +{ + //! Denotes whether `size` is valid + bool valid; + //! Calculated size, valid only if `valid == true` + std::size_t size; +}; + +// can be used with message/group +/** + * @brief Calculate `view` size with additional safety checks. + * + * Similar to `size_bytes()` but stops if `view` cannot fit into the given + * `size`. Useful to check that incoming message is fully contained within given + * buffer. + * + * @param view message or group view + * @param size buffer size + */ +template +SBEPP_CPP20_CONSTEXPR size_bytes_checked_result + size_bytes_checked(View view, std::size_t size) noexcept +{ + // `init_cursor` skips header, we need to ensure there's enough space for it + if(!sbepp::addressof(view) || (size < detail::get_header_size(view))) + { + return {}; + } + + detail::size_bytes_checked_visitor visitor{size}; + auto c = sbepp::init_cursor(view); + sbepp::visit(view, c, visitor); + if(visitor.is_valid()) + { + return {true, size - visitor.get_size()}; + } + return {}; +} } // namespace sbepp #if SBEPP_HAS_RANGES && SBEPP_HAS_CONCEPTS diff --git a/sbeppc/src/sbepp/sbeppc/types_compiler.hpp b/sbeppc/src/sbepp/sbeppc/types_compiler.hpp index 35d66cd..7d3754e 100644 --- a/sbeppc/src/sbepp/sbeppc/types_compiler.hpp +++ b/sbeppc/src/sbepp/sbeppc/types_compiler.hpp @@ -322,42 +322,41 @@ class {type_name} return fmt::format("{}", fmt::join(enumerators, ",\n")); } - static std::string make_enum_to_string(sbe::enumeration& e) + static std::string make_enum_visit_impl(const sbe::enumeration& e) { - std::string switch_cases; + std::vector switch_cases; + switch_cases.reserve(e.valid_values.size()); + for(const auto& valid_value : e.valid_values) { - switch_cases.append(fmt::format( + switch_cases.push_back(fmt::format( // clang-format off -R"( - case {enum_type}::{enumerator}: - return "{enumerator}"; -)", +R"(case {enum_type}::{enumerator}: + visitor.on_enum_value(e, {tag}{{}}); + break;)", // clang-format on fmt::arg("enum_type", e.impl_name), - fmt::arg("enum_name", e.name), - fmt::arg("enumerator", valid_value.name))); + fmt::arg("enumerator", valid_value.name), + fmt::arg("tag", valid_value.tag))); } return fmt::format( // clang-format off -R"( -inline SBEPP_CPP14_CONSTEXPR const char* - tag_invoke( - ::sbepp::detail::enum_to_str_tag, - {enum} e) noexcept +R"(template +SBEPP_CPP14_CONSTEXPR void tag_invoke( + ::sbepp::detail::visit_tag, {enum} e, Visitor& visitor) noexcept {{ switch(e) {{ {switch_cases} default: - return nullptr; + visitor.on_enum_value(e, ::sbepp::unknown_enum_value_tag{{}}); }} }} )", // clang-format on fmt::arg("enum", e.impl_name), - fmt::arg("switch_cases", switch_cases)); + fmt::arg("switch_cases", fmt::join(switch_cases, "\n "))); } std::string compile_encoding(sbe::enumeration& e) @@ -378,13 +377,13 @@ enum class {name} : {type} {enumerators} }}; -{enum_to_string_impl} +{enum_visit_impl} )", // clang-format on fmt::arg("name", e.impl_name), fmt::arg("type", e.underlying_type), fmt::arg("enumerators", enumerators), - fmt::arg("enum_to_string_impl", make_enum_to_string(e))); + fmt::arg("enum_visit_impl", make_enum_visit_impl(e))); } static bool is_unsigned(const std::string_view type) @@ -434,33 +433,57 @@ R"( return res; } - static std::string make_visit_set_impl(sbe::set& s) + static std::string make_visit_set_impl(const sbe::set& s) { - std::string choice_visitors; + std::vector visitors; + visitors.reserve(s.choices.size()); + for(const auto& choice : s.choices) { - choice_visitors.append(fmt::format( - // clang-format off -R"( - visitor(this->{choice_name}(), "{choice_name}");)", - // clang-format on - fmt::arg("set_name", s.name), + visitors.push_back(fmt::format( + "visitor(this->{choice_name}(), \"{choice_name}\");", fmt::arg("choice_name", choice.name))); } return fmt::format( // clang-format off -R"( -template +R"(template SBEPP_CPP14_CONSTEXPR Visitor&& operator()( ::sbepp::detail::visit_set_tag, Visitor&& visitor) const noexcept {{ - {choice_visitors} + {visitors} return std::forward(visitor); }} )", // clang-format on - fmt::arg("choice_visitors", choice_visitors)); + fmt::arg("visitors", fmt::join(visitors, "\n "))); + } + + static std::string make_visit_set_impl2(const sbe::set& s) + { + std::vector visitors; + visitors.reserve(s.choices.size()); + + for(const auto& choice : s.choices) + { + visitors.push_back(fmt::format( + "visitor.on_set_choice(this->{choice_name}(), {tag}{{}});", + fmt::arg("choice_name", choice.name), + fmt::arg("tag", choice.tag))); + } + + return fmt::format( + // clang-format off +R"( +template +SBEPP_CPP14_CONSTEXPR void operator()( + ::sbepp::detail::visit_tag, Visitor& visitor) const noexcept +{{ + {visitors} +}} +)", + // clang-format on + fmt::arg("visitors", fmt::join(visitors, "\n "))); } std::string compile_encoding(sbe::set& s) @@ -487,13 +510,15 @@ class {name} : public ::sbepp::detail::bitset_base<{type}> {accessors} {visit_set_impl} + {visit_set_impl2} }}; )", // clang-format on fmt::arg("name", s.impl_name), fmt::arg("type", s.underlying_type), fmt::arg("accessors", make_set_accessors(s)), - fmt::arg("visit_set_impl", make_visit_set_impl(s))); + fmt::arg("visit_set_impl", make_visit_set_impl(s)), + fmt::arg("visit_set_impl2", make_visit_set_impl2(s))); } static std::string get_const_impl_type(const sbe::encoding& enc) diff --git a/test/src/sbepp/test/enum.test.cpp b/test/src/sbepp/test/enum.test.cpp index 77a969c..3e5b1c1 100644 --- a/test/src/sbepp/test/enum.test.cpp +++ b/test/src/sbepp/test/enum.test.cpp @@ -63,6 +63,49 @@ TEST(EnumTest, EnumToStringReturnsNullptrIfValueIsUnknown) ASSERT_EQ(sbepp::enum_to_string(e), nullptr); } +template +class enum_test_visitor +{ +public: + void on_enum_value(enum_t value, ValidTag) + { + valid = (value == Enumerator); + } + + template + void on_enum_value(enum_t /*value*/, WrongTag) + { + valid = false; + } + + bool is_valid() const + { + return valid; + } + +private: + bool valid{}; +}; + +TEST(EnumTest, EnumVisitorIsCalledWithCorrectValueAndTag) +{ + const auto visitor = sbepp::visit>(enum_t::One); + + ASSERT_TRUE(visitor.is_valid()); +} + +TEST(EnumTest, EnumVisitorIsCalledWithUnknownTagForUnknownValue) +{ + constexpr auto unknown_value = static_cast(4); + const auto visitor = sbepp::visit< + enum_test_visitor>( + unknown_value); + + ASSERT_TRUE(visitor.is_valid()); +} + #if SBEPP_HAS_CONSTEXPR_ACCESSORS constexpr auto underlying = sbepp::to_underlying(enum_t::One); #endif diff --git a/test/src/sbepp/test/set.test.cpp b/test/src/sbepp/test/set.test.cpp index 6d6afa0..1a7f6be 100644 --- a/test/src/sbepp/test/set.test.cpp +++ b/test/src/sbepp/test/set.test.cpp @@ -172,6 +172,50 @@ TEST(SetTest, VisitSetVisitsChoices) }); } +// tests that `A == true` and `B == false` +class options_set_visitor +{ +public: + void on_set_choice(bool value, test_schema::schema::types::options_set::A) + { + valid &= ((choice_index == 0) && (value == true)); + choice_index++; + } + + void on_set_choice(bool value, test_schema::schema::types::options_set::B) + { + valid &= ((choice_index == 1) && (value == false)); + choice_index++; + } + + template + void on_set_choice(bool /*value*/, Tag) + { + // should not be called + valid = false; + } + + bool is_valid() const + { + return valid && (choice_index == 2); + } + +private: + bool valid{true}; + std::size_t choice_index{}; +}; + +TEST(SetTest, VisitSetVisitsChoices2) +{ + set_t s{}; + s.A(true); + s.B(false); + + auto visitor = sbepp::visit(s); + + ASSERT_TRUE(visitor.is_valid()); +} + #if SBEPP_HAS_CONSTEXPR_ACCESSORS constexpr set_t constexpr_test() { diff --git a/test/src/sbepp/test/stringification.test.cpp b/test/src/sbepp/test/stringification.test.cpp index 1349d2b..130b8ee 100644 --- a/test/src/sbepp/test/stringification.test.cpp +++ b/test/src/sbepp/test/stringification.test.cpp @@ -26,9 +26,9 @@ class to_string_visitor template void on_message(T m, Cursor& c, Tag) { - append("message: {}\n", sbepp::message_traits::name()); + append_line("message: {}", sbepp::message_traits::name()); sbepp::visit(sbepp::get_header(m), *this); - append("content: \n"); + append_line("content: "); indentation++; sbepp::visit_children(m, c, *this); indentation--; @@ -37,7 +37,7 @@ class to_string_visitor template bool on_group(T g, Cursor& c, Tag) { - append("{}:\n", sbepp::group_traits::name()); + append_line("{}:", sbepp::group_traits::name()); indentation++; sbepp::visit_children(g, c, *this); indentation--; @@ -48,7 +48,7 @@ class to_string_visitor template bool on_entry(T entry, Cursor& c) { - append("entry:\n"); + append_line("entry:"); indentation++; sbepp::visit_children(entry, c, *this); indentation--; @@ -98,6 +98,31 @@ class to_string_visitor return {}; } + template + void on_enum_value(auto /*e*/, Tag) + { + append("{}\n", sbepp::enum_value_traits::name()); + } + + void on_enum_value(auto e, sbepp::unknown_enum_value_tag) + { + append("unknown({})\n", sbepp::to_underlying(e)); + } + + template + void on_set_choice(const bool value, Tag) + { + if(value) + { + if(!is_first_choice) + { + append(", "); + } + is_first_choice = false; + append("{}", sbepp::set_choice_traits::name()); + } + } + const std::string& str() const { return res; @@ -106,29 +131,42 @@ class to_string_visitor private: std::string res; std::size_t indentation{}; + bool is_first_choice{}; + + void indent() + { + fmt::format_to(std::back_inserter(res), "{:{}}", "", indentation * 4); + } template void append(fmt::format_string fmt, Args&&... args) { - fmt::format_to(std::back_inserter(res), "{:{}}", "", indentation * 4); fmt::format_to( std::back_inserter(res), fmt, std::forward(args)...); } + template + void append_line(fmt::format_string fmt, Args&&... args) + { + indent(); + append(fmt, std::forward(args)...); + res.push_back('\n'); + } + void on_encoding(sbepp::required_type auto t, const char* name) { - append("{}: {}\n", name, *t); + append_line("{}: {}", name, *t); } void on_encoding(sbepp::optional_type auto t, const char* name) { if(t) { - append("{}: {}\n", name, *t); + append_line("{}: {}", name, *t); } else { - append("{}: null\n", name); + append_line("{}: null", name); } } @@ -144,52 +182,34 @@ class to_string_visitor { // output char arrays as C-strings. Keep in mind that they are not // required to be null-terminated so pass size explicitly - append("{}: {:.{}}\n", name, a.data(), a.size()); + append_line("{}: {:.{}}", name, a.data(), a.size()); } else { // use standard range-formatter - append("{}: {}\n", name, a); + append_line("{}: {}", name, a); } } void on_encoding(sbepp::enumeration auto e, const char* name) { - const auto as_string = sbepp::enum_to_string(e); - if(as_string) - { - append("{}: {}\n", name, as_string); - } - else - { - append("{}: unknown({})\n", name, sbepp::to_underlying(e)); - } + indent(); + append("{}: ", name); + sbepp::visit(e, *this); } void on_encoding(sbepp::set auto s, const char* name) { - std::size_t n{}; - std::array choices{}; - sbepp::visit_set( - s, - [&n, &choices](const auto value, const auto name) - { - if(value) - { - choices[n] = name; - n++; - } - }); - - append( - "{}: ({})\n", - name, - fmt::join(choices.begin(), choices.begin() + n, ", ")); + indent(); + append("{}: (", name); + is_first_choice = true; + sbepp::visit(s, *this); + append(")\n"); } void on_encoding(sbepp::composite auto c, const char* name) { - append("{}:\n", name); + append_line("{}:", name); indentation++; sbepp::visit_children(c, *this); indentation--;