diff --git a/include/envoy/request_info/BUILD b/include/envoy/request_info/BUILD index 57bc0ae18b780..09a30c66a8672 100644 --- a/include/envoy/request_info/BUILD +++ b/include/envoy/request_info/BUILD @@ -13,8 +13,15 @@ envoy_cc_library( hdrs = ["request_info.h"], external_deps = ["abseil_optional"], deps = [ + ":dynamic_metadata_interface", "//include/envoy/common:time_interface", "//include/envoy/http:protocol_interface", "//include/envoy/upstream:upstream_interface", ], ) + +envoy_cc_library( + name = "dynamic_metadata_interface", + hdrs = ["dynamic_metadata.h"], + external_deps = ["abseil_optional"], +) diff --git a/include/envoy/request_info/dynamic_metadata.h b/include/envoy/request_info/dynamic_metadata.h new file mode 100644 index 0000000000000..145ee08a5debe --- /dev/null +++ b/include/envoy/request_info/dynamic_metadata.h @@ -0,0 +1,70 @@ +#pragma once + +#include + +#include "envoy/common/exception.h" +#include "envoy/common/pure.h" + +#include "common/common/fmt.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace RequestInfo { + +class DynamicMetadata { +public: + class Object { + public: + virtual ~Object(){}; + }; + + virtual ~DynamicMetadata(){}; + + /** + * @param data_name the name of the data being set. + * @param data an owning pointer to the data to be stored. + * Note that it is an error to call setData() twice with the same data_name; this is to + * enforce a single authoritative source for each piece of data stored in DynamicMetadata. + */ + virtual void setData(absl::string_view data_name, std::unique_ptr&& data) PURE; + + /** + * @param data_name the name of the data being set. + * @return a reference to the stored data. + * Note that it is an error to access data that has not previously been set. + * This function will fail if the data stored under |data_name| cannot be + * dynamically cast to the type specified. + */ + template const T& getData(absl::string_view data_name) const { + const T* result = dynamic_cast(getDataGeneric(data_name)); + if (!result) { + throw EnvoyException( + fmt::format("Data stored under {} cannot be coerced to specified type", data_name)); + } + return *result; + } + + /** + * @param data_name the name of the data being probed. + * @return Whether data of the type and name specified exists in the + * data store. + */ + template bool hasData(absl::string_view data_name) const { + return (hasDataWithName(data_name) && + (dynamic_cast(getDataGeneric(data_name)) != nullptr)); + } + + /** + * @param data_name the name of the data being probed. + * @return Whether data of any type and the name specified exists in the + * data store. + */ + virtual bool hasDataWithName(absl::string_view data_name) const PURE; + +protected: + virtual const Object* getDataGeneric(absl::string_view data_name) const PURE; +}; + +} // namespace RequestInfo +} // namespace Envoy diff --git a/include/envoy/request_info/request_info.h b/include/envoy/request_info/request_info.h index ee21a3ae7e8b1..8d8a011196746 100644 --- a/include/envoy/request_info/request_info.h +++ b/include/envoy/request_info/request_info.h @@ -8,6 +8,7 @@ #include "envoy/common/pure.h" #include "envoy/common/time.h" #include "envoy/http/protocol.h" +#include "envoy/request_info/dynamic_metadata.h" #include "envoy/upstream/upstream.h" #include "absl/types/optional.h" diff --git a/source/common/request_info/BUILD b/source/common/request_info/BUILD index 77d0e7d0774f7..f94d5bcf65da2 100644 --- a/source/common/request_info/BUILD +++ b/source/common/request_info/BUILD @@ -17,6 +17,15 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "dynamic_metadata_lib", + srcs = ["dynamic_metadata_impl.cc"], + hdrs = ["dynamic_metadata_impl.h"], + deps = [ + "//include/envoy/request_info:dynamic_metadata_interface", + ], +) + envoy_cc_library( name = "utility_lib", srcs = ["utility.cc"], diff --git a/source/common/request_info/dynamic_metadata_impl.cc b/source/common/request_info/dynamic_metadata_impl.cc new file mode 100644 index 0000000000000..bf99a6cfb795c --- /dev/null +++ b/source/common/request_info/dynamic_metadata_impl.cc @@ -0,0 +1,32 @@ +#include "common/request_info/dynamic_metadata_impl.h" + +#include "envoy/common/exception.h" + +namespace Envoy { +namespace RequestInfo { + +void DynamicMetadataImpl::setData(absl::string_view data_name, std::unique_ptr&& data) { + if (data_storage_.find(data_name) != data_storage_.end()) { + throw EnvoyException("DynamicMetadata::setData called twice with same name."); + } + // absl::string_view will not convert to std::string without an explicit case; see + // https://github.com/abseil/abseil-cpp/blob/master/absl/strings/string_view.h#L328 + data_storage_[static_cast(data_name)] = std::move(data); +} + +bool DynamicMetadataImpl::hasDataWithName(absl::string_view data_name) const { + return data_storage_.count(data_name) > 0; +} + +const DynamicMetadata::Object* +DynamicMetadataImpl::getDataGeneric(absl::string_view data_name) const { + const auto& it = data_storage_.find(data_name); + + if (it == data_storage_.end()) { + throw EnvoyException("DynamicMetadata::getData called for unknown data name."); + } + return it->second.get(); +} + +} // namespace RequestInfo +} // namespace Envoy diff --git a/source/common/request_info/dynamic_metadata_impl.h b/source/common/request_info/dynamic_metadata_impl.h new file mode 100644 index 0000000000000..51a18472aa79c --- /dev/null +++ b/source/common/request_info/dynamic_metadata_impl.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "envoy/request_info/dynamic_metadata.h" + +#include "absl/strings/string_view.h" + +namespace Envoy { +namespace RequestInfo { + +class DynamicMetadataImpl : public DynamicMetadata { +public: + // DynamicMetadata + void setData(absl::string_view data_name, std::unique_ptr&& data) override; + bool hasDataWithName(absl::string_view) const override; + const Object* getDataGeneric(absl::string_view data_name) const override; + +private: + // The explicit non-type-specific comparator is necessary to allow use of find() method + // with absl::string_view. See + // https://stackoverflow.com/questions/20317413/what-are-transparent-comparators. + std::map, std::less<>> data_storage_; +}; + +} // namespace RequestInfo +} // namespace Envoy diff --git a/test/common/request_info/BUILD b/test/common/request_info/BUILD index 5dde76ff832db..b7cdfa87f6482 100644 --- a/test/common/request_info/BUILD +++ b/test/common/request_info/BUILD @@ -8,6 +8,15 @@ load( envoy_package() +envoy_cc_test( + name = "dynamic_metadata_impl_test", + srcs = ["dynamic_metadata_impl_test.cc"], + deps = [ + "//source/common/request_info:dynamic_metadata_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "request_info_impl_test", srcs = ["request_info_impl_test.cc"], diff --git a/test/common/request_info/dynamic_metadata_impl_test.cc b/test/common/request_info/dynamic_metadata_impl_test.cc new file mode 100644 index 0000000000000..a2245fdf4b8ef --- /dev/null +++ b/test/common/request_info/dynamic_metadata_impl_test.cc @@ -0,0 +1,171 @@ +#include "envoy/common/exception.h" + +#include "common/request_info/dynamic_metadata_impl.h" + +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace RequestInfo { +namespace { + +class TestStoredTypeTracking : public DynamicMetadata::Object { +public: + TestStoredTypeTracking(int value, size_t* access_count, size_t* destruction_count) + : value_(value), access_count_(access_count), destruction_count_(destruction_count) {} + ~TestStoredTypeTracking() { + if (destruction_count_) { + ++*destruction_count_; + } + } + + int access() const { + if (access_count_) { + ++*access_count_; + } + return value_; + } + +private: + int value_; + size_t* access_count_; + size_t* destruction_count_; +}; + +class SimpleType : public DynamicMetadata::Object { +public: + SimpleType(int value) : value_(value) {} + + int access() const { return value_; } + +private: + int value_; +}; + +class DynamicMetadataImplTest : public testing::Test { +public: + DynamicMetadataImplTest() { resetDynamicMetadata(); } + + void resetDynamicMetadata() { dynamic_metadata_ = std::make_unique(); } + DynamicMetadata& dynamic_metadata() { return *dynamic_metadata_; } + +private: + std::unique_ptr dynamic_metadata_; +}; + +} // namespace + +TEST_F(DynamicMetadataImplTest, Simple) { + size_t access_count = 0u; + size_t destruction_count = 0u; + dynamic_metadata().setData( + "test_name", std::make_unique(5, &access_count, &destruction_count)); + EXPECT_EQ(0u, access_count); + EXPECT_EQ(0u, destruction_count); + + EXPECT_EQ(5, dynamic_metadata().getData("test_name").access()); + EXPECT_EQ(1u, access_count); + EXPECT_EQ(0u, destruction_count); + + resetDynamicMetadata(); + EXPECT_EQ(1u, access_count); + EXPECT_EQ(1u, destruction_count); +} + +TEST_F(DynamicMetadataImplTest, SameTypes) { + size_t access_count_1 = 0u; + size_t access_count_2 = 0u; + size_t destruction_count = 0u; + static const int ValueOne = 5; + static const int ValueTwo = 6; + + dynamic_metadata().setData("test_1", std::make_unique( + ValueOne, &access_count_1, &destruction_count)); + dynamic_metadata().setData("test_2", std::make_unique( + ValueTwo, &access_count_2, &destruction_count)); + EXPECT_EQ(0u, access_count_1); + EXPECT_EQ(0u, access_count_2); + EXPECT_EQ(0u, destruction_count); + + EXPECT_EQ(ValueOne, dynamic_metadata().getData("test_1").access()); + EXPECT_EQ(1u, access_count_1); + EXPECT_EQ(0u, access_count_2); + EXPECT_EQ(ValueTwo, dynamic_metadata().getData("test_2").access()); + EXPECT_EQ(1u, access_count_1); + EXPECT_EQ(1u, access_count_2); + resetDynamicMetadata(); + EXPECT_EQ(2u, destruction_count); +} + +TEST_F(DynamicMetadataImplTest, SimpleType) { + dynamic_metadata().setData("test_1", std::make_unique(1)); + dynamic_metadata().setData("test_2", std::make_unique(2)); + + EXPECT_EQ(1, dynamic_metadata().getData("test_1").access()); + EXPECT_EQ(2, dynamic_metadata().getData("test_2").access()); +} + +TEST_F(DynamicMetadataImplTest, NameConflict) { + dynamic_metadata().setData("test_1", std::make_unique(1)); + EXPECT_THROW_WITH_MESSAGE(dynamic_metadata().setData("test_1", std::make_unique(2)), + EnvoyException, + "DynamicMetadata::setData called twice with same name."); + EXPECT_EQ(1, dynamic_metadata().getData("test_1").access()); +} + +TEST_F(DynamicMetadataImplTest, NameConflictDifferentTypes) { + dynamic_metadata().setData("test_1", std::make_unique(1)); + EXPECT_THROW_WITH_MESSAGE( + dynamic_metadata().setData("test_1", + std::make_unique(2, nullptr, nullptr)), + EnvoyException, "DynamicMetadata::setData called twice with same name."); +} + +TEST_F(DynamicMetadataImplTest, UnknownName) { + EXPECT_THROW_WITH_MESSAGE(dynamic_metadata().getData("test_1"), EnvoyException, + "DynamicMetadata::getData called for unknown data name."); +} + +TEST_F(DynamicMetadataImplTest, WrongTypeGet) { + dynamic_metadata().setData("test_name", + std::make_unique(5, nullptr, nullptr)); + EXPECT_EQ(5, dynamic_metadata().getData("test_name").access()); + EXPECT_THROW_WITH_MESSAGE(dynamic_metadata().getData("test_name"), EnvoyException, + "Data stored under test_name cannot be coerced to specified type"); +} + +namespace { + +class A : public DynamicMetadata::Object {}; + +class B : public A {}; + +class C : public B {}; + +} // namespace + +TEST_F(DynamicMetadataImplTest, FungibleInheritance) { + dynamic_metadata().setData("testB", std::make_unique()); + EXPECT_TRUE(dynamic_metadata().hasData("testB")); + EXPECT_TRUE(dynamic_metadata().hasData("testB")); + EXPECT_FALSE(dynamic_metadata().hasData("testB")); + + dynamic_metadata().setData("testC", std::make_unique()); + EXPECT_TRUE(dynamic_metadata().hasData("testC")); + EXPECT_TRUE(dynamic_metadata().hasData("testC")); + EXPECT_TRUE(dynamic_metadata().hasData("testC")); +} + +TEST_F(DynamicMetadataImplTest, HasData) { + dynamic_metadata().setData("test_1", std::make_unique(1)); + EXPECT_TRUE(dynamic_metadata().hasData("test_1")); + EXPECT_FALSE(dynamic_metadata().hasData("test_2")); + EXPECT_FALSE(dynamic_metadata().hasData("test_1")); + EXPECT_FALSE(dynamic_metadata().hasData("test_2")); + EXPECT_TRUE(dynamic_metadata().hasDataWithName("test_1")); + EXPECT_FALSE(dynamic_metadata().hasDataWithName("test_2")); +} + +} // namespace RequestInfo +} // namespace Envoy