Skip to content

Commit c27306d

Browse files
author
pananton
committed
feat userver/proto-structs: tests and fixes
* Stabilize the serialization API for Well-known types * Introduce an extendable discovery system for Well-known types * Suppose that a proto struct uses `foo::bar::Something` for a field type, which is NOT a Protobuf type * Then `<userver/proto-structs/io/foo/bar/something.hpp>` should bring in the type definition (usually by including the proper header) * Then `<userver/proto-structs/io/foo/bar/something_conv.hpp>` should bring in proto-structs serialization for the type * Serialization discovery was inspired by userver chaotic and postgres * Implement serialization for all main Well-known types: integers, strings, optional, vector, maps, `utils::Box`, * Implement the collection of path to the current Protobuf node in case of a convertion error * Add diverse conversion tests for various hand-written proto structs Relates: <https://nda.ya.ru/t/cgDVoMPU7JV5ar> commit_hash:a51e1837061bf5fbdbaed98cc987f03c68f4a5d1
1 parent f122b64 commit c27306d

File tree

86 files changed

+4905
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+4905
-909
lines changed

.mapping.json

Lines changed: 65 additions & 9 deletions
Large diffs are not rendered by default.

libraries/proto-structs/include/userver/proto-structs/any.hpp

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,43 +18,40 @@ namespace proto_structs {
1818
/// @brief Wrapper for `google.protobuf.Any` which provides interface to access stored message as compatible struct
1919
class Any final {
2020
public:
21+
using ProtobufMessage = ::google::protobuf::Any;
22+
2123
/// @brief Creates empty `Any`.
2224
Any() noexcept = default;
2325

2426
/// @brief Creates wrapper initializing its underlying storage with @a proto_any
2527
Any(google::protobuf::Any proto_any) : storage_(std::move(proto_any)) {}
2628

2729
/// @brief Creates `Any` holding @a obj
28-
/// @tparam TStruct struct with defined compatability
29-
/// @throws ConversionError if @a obj can't be converted to its compatible protobuf message
30+
/// @tparam TStruct proto struct type
31+
/// @throws WriteError if conversion of @a obj to its compatible message has failed
3032
/// @throws AnyPackError if compatible protobuf message can not be packed to `google.protobuf.Any`
31-
/// @note This overload only works for structs for which compatability is defined using
32-
/// @ref proto_structs::CompatibleMessageTrait
3333
template <typename TStruct>
34-
requires traits::CompatibleStruct<std::remove_cvref_t<TStruct>>
34+
requires(!std::is_same_v<std::remove_cvref_t<TStruct>, ::google::protobuf::Any>) &&
35+
(!std::is_same_v<std::remove_cvref_t<TStruct>, Any>) && traits::ProtoStruct<std::remove_cvref_t<TStruct>>
3536
Any(TStruct&& obj) {
3637
Pack(std::forward<TStruct>(obj));
3738
}
3839

3940
/// @brief Packs @a obj in `Any`
40-
/// @tparam TStruct struct with defined compatability
41-
/// @throws ConversionError if @a obj can't be converted to its compatible protobuf message
41+
/// @tparam TStruct proto struct type
42+
/// @throws WriteError if conversion of @a obj to its compatible message has failed
4243
/// @throws AnyPackError if compatible protobuf message can not be packed to `google.protobuf.Any`
43-
/// @note This overload only works for structs for which compatability is defined using
44-
/// @ref proto_structs::CompatibleMessageTrait
45-
/// @note Method caches @a obj and later calls to @ref Get will not trigger unpacking
4644
template <typename TStruct>
47-
requires traits::CompatibleStruct<std::remove_cvref_t<TStruct>>
45+
requires(!std::is_same_v<std::remove_cvref_t<TStruct>, ::google::protobuf::Any>) &&
46+
(!std::is_same_v<std::remove_cvref_t<TStruct>, Any>) && traits::ProtoStruct<std::remove_cvref_t<TStruct>>
4847
Any& operator=(TStruct&& obj) {
4948
Pack(std::forward<TStruct>(obj));
5049
return *this;
5150
}
5251

5352
/// @brief Returns `true` if `Any` contains `TStruct`
54-
/// @tparam TStruct struct with defined compatability
55-
/// @note This method only works for structs for which compatability is defined using
56-
/// @ref proto_structs::CompatibleMessageTrait
57-
template <traits::CompatibleStruct TStruct>
53+
/// @tparam TStruct proto struct type
54+
template <traits::ProtoStruct TStruct>
5855
bool Is() const noexcept {
5956
using Message = traits::CompatibleMessageType<TStruct>;
6057
return storage_.Is<Message>();
@@ -68,12 +65,10 @@ class Any final {
6865
}
6966

7067
/// @brief Unpacks `Any` to `TStruct` struct
71-
/// @tparam TStruct struct with defined compatability
68+
/// @tparam TStruct proto struct type
7269
/// @throws AnyUnpackError if underlying `google.protobuf.Any` does not contain message compatible to `TStruct`
73-
/// @throws ConversionError if struct can be converted from its compatible message
74-
/// @note This method only works for structs for which compatability is defined using
75-
/// @ref proto_structs::CompatibleMessageTrait
76-
template <traits::CompatibleStruct TStruct>
70+
/// @throws ReadError if conversion of unpacked protobuf message to proto struct has failed
71+
template <traits::ProtoStruct TStruct>
7772
TStruct Unpack() {
7873
using Message = traits::CompatibleMessageType<TStruct>;
7974
return MessageToStruct<TStruct>(Unpack<Message>());
@@ -94,25 +89,22 @@ class Any final {
9489
}
9590

9691
/// @brief Packs @a obj to `Any`
97-
/// @tparam TStruct struct with defined compatability
98-
/// @throws ConversionError if @a obj can't be converted to its compatible protobuf message
99-
/// @throws AnyPackError if packing of compatible protobuf message to `google.protobuf.Any` had failed
100-
/// @note This method only works for structs for which compatability is defined using
101-
/// @ref proto_structs::CompatibleMessageTrait
102-
/// @note Method caches @a obj and later calls to @ref Get will not trigger unpacking
92+
/// @tparam TStruct proto struct type
93+
/// @throws WriteError if conversion of @a obj to its compatible message has failed
94+
/// @throws AnyPackError if packing of compatible protobuf message to `google.protobuf.Any` has failed
10395
template <typename TStruct>
104-
requires traits::CompatibleStruct<std::remove_cvref_t<TStruct>>
96+
requires traits::ProtoStruct<std::remove_cvref_t<TStruct>>
10597
void Pack(TStruct&& obj) {
10698
using Message = traits::CompatibleMessageType<std::remove_cvref_t<TStruct>>;
10799
Pack<Message>(StructToMessage(std::forward<TStruct>(obj)));
108100
}
109101

110102
/// @brief Packs @a message to underlying `google.protobuf.Any`
111103
/// @tparam TMessage protobuf message type
112-
/// @throws AnyPackError if packing of protobuf message to `google.protobuf.Any` had failed
104+
/// @throws AnyPackError if packing of protobuf message to `google.protobuf.Any` has failed
113105
template <traits::ProtoMessage TMessage>
114106
void Pack(const TMessage& message) {
115-
if (!storage_.PackFrom(&message)) {
107+
if (!storage_.PackFrom(message)) {
116108
throw AnyUnpackError(TMessage::descriptor()->full_name());
117109
}
118110
}
@@ -121,7 +113,7 @@ class Any final {
121113
const ::google::protobuf::Any& GetProtobufAny() const& noexcept { return storage_; }
122114

123115
/// @brief Returns underlying `google.protobuf.Any`
124-
::google::protobuf::Any GetProtobufAny() && noexcept { return std::move(storage_); }
116+
::google::protobuf::Any&& GetProtobufAny() && noexcept { return std::move(storage_); }
125117

126118
private:
127119
::google::protobuf::Any storage_;

libraries/proto-structs/include/userver/proto-structs/convert.hpp

Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,91 +2,95 @@
22

33
/// @file userver/proto-structs/convert.hpp
44
/// @brief Functions for protobuf message to/from struct conversion
5+
///
6+
/// Conversion functions rely on user-defined functions with the following signatures:
7+
/// 1. `StructType ReadProtoStruct(proto_structs::io::ReadContext&,
8+
/// proto_structs::io::To<StructType>,
9+
/// const MessageType&)`
10+
///
11+
/// 2. `void WriteProtoStruct(proto_structs::io::WriteContext&,
12+
/// const StructType&,
13+
/// MessageType&)`
14+
///
15+
/// This functions should be foundable by ADL lookup (just place them in the namespace where `StructType` is defined).
16+
///
17+
/// It is recommended to define `WriteProtoStruct` function as template with a `StructType` parameter and use universal
18+
/// reference `StructType&&` as a second parameter to enable perfect forwarding for conversions.
519

620
#include <type_traits>
721
#include <utility>
822

9-
#include <userver/proto-structs/exceptions.hpp>
10-
#include <userver/proto-structs/impl/context.hpp>
23+
#include <userver/proto-structs/io/context.hpp>
1124
#include <userver/proto-structs/type_mapping.hpp>
12-
#include <userver/utils/assert.hpp>
1325

1426
USERVER_NAMESPACE_BEGIN
1527

1628
namespace proto_structs {
1729

18-
/// @brief Converts protobuf message @a msg to struct @a obj
30+
/// @brief Converts protobuf message @a msg to proto struct @a obj
1931
/// @tparam TMessage protobuf message type
20-
/// @tparam TStruct struct type
21-
/// @throws ConversionError if conversion failed (in that case `obj` is not modified)
22-
/// @note If function throws an exception, @a obj is not modified
23-
///
24-
/// Function calls `ReadStruct(obj, msg)` under the hood which should be foundable by argument-dependent lookup.
32+
/// @tparam TStruct proto struct type
33+
/// @throws ReadError if conversion has failed
34+
/// @warning If function throws an exception, @a obj is left in a valid but unspecified state
2535
template <traits::ProtoMessage TMessage, traits::ProtoStruct TStruct>
2636
void MessageToStruct(const TMessage& msg, TStruct& obj) {
27-
impl::ReadContext ctx;
28-
obj = ReadStruct(ctx, To<std::remove_cv_t<TStruct>>{}, msg);
29-
UINVARIANT(!ctx.HasError(), "Read context contains error that should have been thrown");
37+
io::ReadContext ctx(*msg.GetDescriptor());
38+
39+
try {
40+
obj = ReadProtoStruct(ctx, io::To<std::remove_cv_t<TStruct>>{}, msg);
41+
} catch (const ReadError&) {
42+
// simply re-throw proper errors
43+
throw;
44+
} catch (const std::exception& e) {
45+
// some user-defined `ReadProtoStruct` threw an exception instead of adding an error to context,
46+
// adding it manually an re-throwing as a proper exception type
47+
ctx.AddError(e.what());
48+
}
3049
}
3150

32-
/// @brief Converts protobuf message @a msg to specified structure type
33-
/// @tparam TStruct struct type
51+
/// @brief Converts protobuf message @a msg to specified proto struct type
52+
/// @tparam TStruct proto struct type
3453
/// @tparam TMessage protobuf message type
35-
/// @throws ConversionError if conversion failed
36-
///
37-
/// Function calls `ReadStruct(obj, msg)` under the hood which should be foundable by argument-dependent lookup.
54+
/// @throws ReadError if conversion has failed
3855
template <traits::ProtoStruct TStruct, traits::ProtoMessage TMessage>
3956
[[nodiscard]] TStruct MessageToStruct(const TMessage& msg) {
4057
TStruct obj;
4158
MessageToStruct(msg, obj);
4259
return obj;
4360
}
4461

45-
/// @brief Converts struct instance @a obj to protobuf message @a msg
46-
/// @tparam TStruct struct type
62+
/// @brief Converts proto struct @a obj to protobuf message @a msg
63+
/// @tparam TStruct proto struct type
4764
/// @tparam TMessage protobuf message type
48-
/// @throws ConversionError if conversion failed (in that case `msg` is not modified)
65+
/// @throws WriteError if conversion has failed
4966
/// @warning If function throws an exception, @a msg is left in a valid but unspecified state
50-
///
51-
/// Function calls `WriteStruct(std::forward<TStruct>(obj), msg)` under the hood which should be foundable by
52-
/// argument-dependent lookup.
5367
template <typename TStruct, traits::ProtoMessage TMessage>
5468
requires traits::ProtoStruct<std::remove_cvref_t<TStruct>>
5569
void StructToMessage(TStruct&& obj, TMessage& msg) {
56-
impl::WriteContext ctx;
57-
WriteStruct(ctx, std::forward<TStruct>(obj), msg);
58-
UINVARIANT(!ctx.HasError(), "Write context contains error that should have been thrown");
59-
}
70+
io::WriteContext ctx(*msg.GetDescriptor());
6071

61-
/// @brief Converts struct instance @a obj to protobuf message @a msg of the specified type
62-
/// @tparam TStruct struct type
63-
/// @tparam TMessage protobuf message type
64-
/// @throws ConversionError if conversion failed
65-
///
66-
/// Function calls `WriteStruct(std::forward<TStruct>(obj), msg)` under the hood which should be foundable by
67-
/// argument-dependent lookup.
68-
template <traits::ProtoMessage TMessage, typename TStruct>
69-
requires traits::ProtoStruct<std::remove_cvref_t<TStruct>>
70-
[[nodiscard]] TMessage StructToMessage(TStruct&& obj) {
71-
TMessage msg;
72-
StructToMessage(std::forward<TStruct>(obj), msg);
73-
return msg;
72+
try {
73+
WriteProtoStruct(ctx, std::forward<TStruct>(obj), msg);
74+
} catch (const WriteError&) {
75+
// simply re-throw proper errors
76+
throw;
77+
} catch (const std::exception& e) {
78+
// some user-defined `WriteProtoStruct` threw an exception instead of adding an error to context,
79+
// adding it manually an re-throwing as a proper exception type
80+
ctx.AddError(e.what());
81+
}
7482
}
7583

76-
/// @brief Converts struct instance @a obj to it's compatible protobuf message
77-
/// @tparam TStruct struct type
78-
/// @throws ConversionError if conversion failed
79-
///
80-
/// Compatability information should be provided for `TStruct` with @ref proto_structs::compatible_message, otherwise
81-
/// compilation will fail.
82-
///
83-
/// Function calls `WriteStruct(std::forward<TStruct>(obj), msg)` under the hood which should be foundable by
84-
/// argument-dependent lookup.
84+
/// @brief Converts proto struct @a obj to it's compatible protobuf message type
85+
/// @tparam TStruct proto struct type
86+
/// @throws WriteError if conversion has failed
8587
template <typename TStruct>
86-
requires traits::CompatibleStruct<std::remove_cvref_t<TStruct>>
88+
requires traits::ProtoStruct<std::remove_cvref_t<TStruct>>
8789
[[nodiscard]] traits::CompatibleMessageType<std::remove_cvref_t<TStruct>> StructToMessage(TStruct&& obj) {
8890
using Message = traits::CompatibleMessageType<std::remove_cvref_t<TStruct>>;
89-
return StructToMessage<Message>(std::forward<TStruct>(obj));
91+
Message msg;
92+
StructToMessage(std::forward<TStruct>(obj), msg);
93+
return msg;
9094
}
9195

9296
} // namespace proto_structs

libraries/proto-structs/include/userver/proto-structs/exceptions.hpp

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
USERVER_NAMESPACE_BEGIN
1010

11+
/// @brief Top namespace for the proto-structs library
1112
namespace proto_structs {
1213

1314
/// @brief Library basic exception type
@@ -17,35 +18,50 @@ class Error : public std::runtime_error {
1718
using std::runtime_error::runtime_error;
1819
};
1920

20-
/// @brief Conversion error
21-
/// Exception is thrown if protobuf message field can't be converted to/from struct field.
21+
/// @brief Conversion error base class
2222
class ConversionError : public Error {
23+
protected:
24+
using Error::Error;
25+
};
26+
27+
/// @brief Error reading proto struct from protobuf message
28+
class ReadError final : public ConversionError {
29+
public:
30+
/// @brief Creates error with information what protobuf message field was considered invalid
31+
/// Parameter @a path contains dot-separated field names from the top-level message up to erroneous field, for
32+
/// example, "msg.field.nested_field".
33+
ReadError(std::string_view path, std::string_view reason);
34+
};
35+
36+
/// @brief Error writing proto struct to protobuf message
37+
class WriteError final : public ConversionError {
2338
public:
24-
/// @brief Creates error signalling that conversion failed for field @a field_name of message @a message_name
25-
ConversionError(std::string_view message_name, std::string_view field_name, std::string_view reason);
39+
/// @brief Creates error with information what protobuf message field was not correctly initialized
40+
/// Parameter @a path contains dot-separated field names from the top-level message up to field that was not
41+
/// initialized, for example, "msg.field.nested_field".
42+
WriteError(std::string_view path, std::string_view reason);
2643
};
2744

28-
/// @brief Trying to access unset @ref proto_structs::Oneof field
29-
/// @note This exception is also thrown if @ref proto_structs::Oneof is cleared
45+
/// @brief Invalid attempt to access unset @ref proto_structs::Oneof field
3046
class OneofAccessError : public Error {
3147
public:
32-
/// @brief Creates error when attempting to access @a field_idx field of `oneof`
48+
/// @brief Creates error on invalid attempt to access @a field_idx field of `oneof`
3349
explicit OneofAccessError(std::size_t field_idx);
3450
};
3551

36-
/// @brief Failed to pack struct's compatible message to @ref proto_structs::Any underlying storage.
37-
/// @note This exception is thrown *after* struct to protobuf message conversion.
52+
/// @brief Error packing protobuf message to @ref proto_structs::Any underlying storage.
3853
class AnyPackError : public Error {
3954
public:
40-
/// @brief Creates error when trying to pack protobuf message @a message_name
55+
/// @brief Creates error with information what protobuf message was not packed
4156
explicit AnyPackError(std::string_view message_name);
4257
};
4358

44-
/// @brief Failed to unpack struct's compatible message from @ref proto_structs::Any underlying storage
45-
/// @note This exception is thrown *before* protobuf message to struct conversion.
59+
/// @brief Error unpacking protobuf message from @ref proto_structs::Any underlying storage.
60+
/// The main reason of this exception is the attempt to unpack message type different from the one stored in the
61+
/// @ref proto_structs::Any underlying storage
4662
class AnyUnpackError : public Error {
4763
public:
48-
/// @brief Creates error when trying to unpack protobuf message @a message_name
64+
/// @brief Creates error with information what protobuf message was not unpacked
4965
explicit AnyUnpackError(std::string_view message_name);
5066
};
5167

libraries/proto-structs/include/userver/proto-structs/fwd.hpp

Lines changed: 0 additions & 24 deletions
This file was deleted.

libraries/proto-structs/include/userver/proto-structs/hash_map.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
#pragma once
22

3-
/// @file
3+
/// @file userver/proto-structs/hash_map.hpp
44
/// @brief @copybrief proto_structs::HashMap
55

6+
#include <string_view>
67
#include <type_traits>
78
#include <unordered_map>
89

0 commit comments

Comments
 (0)