This is an automated email from the ASF dual-hosted git repository.
gangwu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 0dbb593 refactor: simplify test structure for rest catalog models
(#290)
0dbb593 is described below
commit 0dbb593df6cd3564efdbd283dcc1db3c299beeb1
Author: Gang Wu <[email protected]>
AuthorDate: Tue Nov 4 21:27:02 2025 +0800
refactor: simplify test structure for rest catalog models (#290)
- simplified rest catalog model definitions.
- added common functions to operate json ser/de.
- refactored cases to be able to be shared.
---
src/iceberg/catalog/rest/json_internal.cc | 72 +-
src/iceberg/catalog/rest/json_internal.h | 97 +--
src/iceberg/test/rest_catalog_test.cc | 6 +-
src/iceberg/test/rest_json_internal_test.cc | 1097 +++++++++++----------------
src/iceberg/util/json_util_internal.h | 15 +
5 files changed, 541 insertions(+), 746 deletions(-)
diff --git a/src/iceberg/catalog/rest/json_internal.cc
b/src/iceberg/catalog/rest/json_internal.cc
index 55f1c38..452de7a 100644
--- a/src/iceberg/catalog/rest/json_internal.cc
+++ b/src/iceberg/catalog/rest/json_internal.cc
@@ -65,9 +65,7 @@ constexpr std::string_view kIdentifiers = "identifiers";
nlohmann::json ToJson(const CreateNamespaceRequest& request) {
nlohmann::json json;
json[kNamespace] = request.namespace_.levels;
- if (!request.properties.empty()) {
- json[kProperties] = request.properties;
- }
+ SetContainerField(json, kProperties, request.properties);
return json;
}
@@ -83,15 +81,9 @@ Result<CreateNamespaceRequest>
CreateNamespaceRequestFromJson(
}
nlohmann::json ToJson(const UpdateNamespacePropertiesRequest& request) {
- // Initialize as an empty object so that when all optional fields are absent
we return
- // {} instead of null
nlohmann::json json = nlohmann::json::object();
- if (!request.removals.empty()) {
- json[kRemovals] = request.removals;
- }
- if (!request.updates.empty()) {
- json[kUpdates] = request.updates;
- }
+ SetContainerField(json, kRemovals, request.removals);
+ SetContainerField(json, kUpdates, request.updates);
return json;
}
@@ -145,13 +137,9 @@ Result<RenameTableRequest>
RenameTableRequestFromJson(const nlohmann::json& json
// LoadTableResult (used by CreateTableResponse, LoadTableResponse)
nlohmann::json ToJson(const LoadTableResult& result) {
nlohmann::json json;
- if (!result.metadata_location.empty()) {
- json[kMetadataLocation] = result.metadata_location;
- }
+ SetOptionalStringField(json, kMetadataLocation, result.metadata_location);
json[kMetadata] = ToJson(*result.metadata);
- if (!result.config.empty()) {
- json[kConfig] = result.config;
- }
+ SetContainerField(json, kConfig, result.config);
return json;
}
@@ -162,17 +150,14 @@ Result<LoadTableResult> LoadTableResultFromJson(const
nlohmann::json& json) {
ICEBERG_ASSIGN_OR_RAISE(auto metadata_json,
GetJsonValue<nlohmann::json>(json, kMetadata));
ICEBERG_ASSIGN_OR_RAISE(result.metadata,
TableMetadataFromJson(metadata_json));
- ICEBERG_ASSIGN_OR_RAISE(
- result.config, (GetJsonValueOrDefault<std::unordered_map<std::string,
std::string>>(
- json, kConfig)));
+ ICEBERG_ASSIGN_OR_RAISE(result.config,
+ GetJsonValueOrDefault<decltype(result.config)>(json,
kConfig));
return result;
}
nlohmann::json ToJson(const ListNamespacesResponse& response) {
nlohmann::json json;
- if (!response.next_page_token.empty()) {
- json[kNextPageToken] = response.next_page_token;
- }
+ SetOptionalStringField(json, kNextPageToken, response.next_page_token);
nlohmann::json namespaces = nlohmann::json::array();
for (const auto& ns : response.namespaces) {
namespaces.push_back(ToJson(ns));
@@ -198,9 +183,7 @@ Result<ListNamespacesResponse>
ListNamespacesResponseFromJson(
nlohmann::json ToJson(const CreateNamespaceResponse& response) {
nlohmann::json json;
json[kNamespace] = response.namespace_.levels;
- if (!response.properties.empty()) {
- json[kProperties] = response.properties;
- }
+ SetContainerField(json, kProperties, response.properties);
return json;
}
@@ -218,9 +201,7 @@ Result<CreateNamespaceResponse>
CreateNamespaceResponseFromJson(
nlohmann::json ToJson(const GetNamespaceResponse& response) {
nlohmann::json json;
json[kNamespace] = response.namespace_.levels;
- if (!response.properties.empty()) {
- json[kProperties] = response.properties;
- }
+ SetContainerField(json, kProperties, response.properties);
return json;
}
@@ -238,19 +219,17 @@ nlohmann::json ToJson(const
UpdateNamespacePropertiesResponse& response) {
nlohmann::json json;
json[kUpdated] = response.updated;
json[kRemoved] = response.removed;
- if (!response.missing.empty()) {
- json[kMissing] = response.missing;
- }
+ SetContainerField(json, kMissing, response.missing);
return json;
}
Result<UpdateNamespacePropertiesResponse>
UpdateNamespacePropertiesResponseFromJson(
const nlohmann::json& json) {
UpdateNamespacePropertiesResponse response;
- ICEBERG_ASSIGN_OR_RAISE(response.updated,
- GetJsonValue<std::vector<std::string>>(json,
kUpdated));
- ICEBERG_ASSIGN_OR_RAISE(response.removed,
- GetJsonValue<std::vector<std::string>>(json,
kRemoved));
+ ICEBERG_ASSIGN_OR_RAISE(
+ response.updated, GetJsonValueOrDefault<std::vector<std::string>>(json,
kUpdated));
+ ICEBERG_ASSIGN_OR_RAISE(
+ response.removed, GetJsonValueOrDefault<std::vector<std::string>>(json,
kRemoved));
ICEBERG_ASSIGN_OR_RAISE(
response.missing, GetJsonValueOrDefault<std::vector<std::string>>(json,
kMissing));
return response;
@@ -258,9 +237,7 @@ Result<UpdateNamespacePropertiesResponse>
UpdateNamespacePropertiesResponseFromJ
nlohmann::json ToJson(const ListTablesResponse& response) {
nlohmann::json json;
- if (!response.next_page_token.empty()) {
- json[kNextPageToken] = response.next_page_token;
- }
+ SetOptionalStringField(json, kNextPageToken, response.next_page_token);
nlohmann::json identifiers_json = nlohmann::json::array();
for (const auto& identifier : response.identifiers) {
identifiers_json.push_back(ToJson(identifier));
@@ -282,4 +259,21 @@ Result<ListTablesResponse>
ListTablesResponseFromJson(const nlohmann::json& json
return response;
}
+#define ICEBERG_DEFINE_FROM_JSON(Model) \
+ template <> \
+ Result<Model> FromJson<Model>(const nlohmann::json& json) { \
+ return Model##FromJson(json); \
+ }
+
+ICEBERG_DEFINE_FROM_JSON(ListNamespacesResponse)
+ICEBERG_DEFINE_FROM_JSON(CreateNamespaceRequest)
+ICEBERG_DEFINE_FROM_JSON(CreateNamespaceResponse)
+ICEBERG_DEFINE_FROM_JSON(GetNamespaceResponse)
+ICEBERG_DEFINE_FROM_JSON(UpdateNamespacePropertiesRequest)
+ICEBERG_DEFINE_FROM_JSON(UpdateNamespacePropertiesResponse)
+ICEBERG_DEFINE_FROM_JSON(ListTablesResponse)
+ICEBERG_DEFINE_FROM_JSON(LoadTableResult)
+ICEBERG_DEFINE_FROM_JSON(RegisterTableRequest)
+ICEBERG_DEFINE_FROM_JSON(RenameTableRequest)
+
} // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/json_internal.h
b/src/iceberg/catalog/rest/json_internal.h
index 11b567a..129e883 100644
--- a/src/iceberg/catalog/rest/json_internal.h
+++ b/src/iceberg/catalog/rest/json_internal.h
@@ -21,81 +21,36 @@
#include <nlohmann/json_fwd.hpp>
+#include "iceberg/catalog/rest/iceberg_rest_export.h"
#include "iceberg/catalog/rest/types.h"
#include "iceberg/result.h"
namespace iceberg::rest {
-/// \brief Serializes a `ListNamespacesResponse` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const ListNamespacesResponse&
response);
-
-/// \brief Deserializes a JSON object into a `ListNamespacesResponse` object.
-ICEBERG_REST_EXPORT Result<ListNamespacesResponse>
ListNamespacesResponseFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes a `CreateNamespaceRequest` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const CreateNamespaceRequest&
request);
-
-/// \brief Deserializes a JSON object into a `CreateNamespaceRequest` object.
-ICEBERG_REST_EXPORT Result<CreateNamespaceRequest>
CreateNamespaceRequestFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes a `CreateNamespaceResponse` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const CreateNamespaceResponse&
response);
-
-/// \brief Deserializes a JSON object into a `CreateNamespaceResponse` object.
-ICEBERG_REST_EXPORT Result<CreateNamespaceResponse>
CreateNamespaceResponseFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes a `GetNamespaceResponse` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const GetNamespaceResponse&
response);
-
-/// \brief Deserializes a JSON object into a `GetNamespaceResponse` object.
-ICEBERG_REST_EXPORT Result<GetNamespaceResponse> GetNamespaceResponseFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes an `UpdateNamespacePropertiesRequest` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(
- const UpdateNamespacePropertiesRequest& request);
-
-/// \brief Deserializes a JSON object into an
`UpdateNamespacePropertiesRequest` object.
-ICEBERG_REST_EXPORT Result<UpdateNamespacePropertiesRequest>
-UpdateNamespacePropertiesRequestFromJson(const nlohmann::json& json);
-
-/// \brief Serializes an `UpdateNamespacePropertiesResponse` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(
- const UpdateNamespacePropertiesResponse& response);
-
-/// \brief Deserializes a JSON object into an
`UpdateNamespacePropertiesResponse` object.
-ICEBERG_REST_EXPORT Result<UpdateNamespacePropertiesResponse>
-UpdateNamespacePropertiesResponseFromJson(const nlohmann::json& json);
-
-/// \brief Serializes a `ListTablesResponse` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const ListTablesResponse& response);
-
-/// \brief Deserializes a JSON object into a `ListTablesResponse` object.
-ICEBERG_REST_EXPORT Result<ListTablesResponse> ListTablesResponseFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes a `LoadTableResult` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const LoadTableResult& result);
-
-/// \brief Deserializes a JSON object into a `LoadTableResult` object.
-ICEBERG_REST_EXPORT Result<LoadTableResult> LoadTableResultFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes a `RegisterTableRequest` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const RegisterTableRequest& request);
-
-/// \brief Deserializes a JSON object into a `RegisterTableRequest` object.
-ICEBERG_REST_EXPORT Result<RegisterTableRequest> RegisterTableRequestFromJson(
- const nlohmann::json& json);
-
-/// \brief Serializes a `RenameTableRequest` object to JSON.
-ICEBERG_REST_EXPORT nlohmann::json ToJson(const RenameTableRequest& request);
-
-/// \brief Deserializes a JSON object into a `RenameTableRequest` object.
-ICEBERG_REST_EXPORT Result<RenameTableRequest> RenameTableRequestFromJson(
- const nlohmann::json& json);
+template <typename Model>
+Result<Model> FromJson(const nlohmann::json& json);
+
+#define ICEBERG_DECLARE_JSON_SERDE(Model)
\
+ ICEBERG_REST_EXPORT Result<Model> Model##FromJson(const nlohmann::json&
json); \
+
\
+ template <>
\
+ ICEBERG_REST_EXPORT Result<Model> FromJson(const nlohmann::json& json);
\
+
\
+ ICEBERG_REST_EXPORT nlohmann::json ToJson(const Model& model);
+
+/// \note Don't forget to add `ICEBERG_DEFINE_FROM_JSON` to the end of
+/// `json_internal.cc` to define the `FromJson` function for the model.
+ICEBERG_DECLARE_JSON_SERDE(ListNamespacesResponse)
+ICEBERG_DECLARE_JSON_SERDE(CreateNamespaceRequest)
+ICEBERG_DECLARE_JSON_SERDE(CreateNamespaceResponse)
+ICEBERG_DECLARE_JSON_SERDE(GetNamespaceResponse)
+ICEBERG_DECLARE_JSON_SERDE(UpdateNamespacePropertiesRequest)
+ICEBERG_DECLARE_JSON_SERDE(UpdateNamespacePropertiesResponse)
+ICEBERG_DECLARE_JSON_SERDE(ListTablesResponse)
+ICEBERG_DECLARE_JSON_SERDE(LoadTableResult)
+ICEBERG_DECLARE_JSON_SERDE(RegisterTableRequest)
+ICEBERG_DECLARE_JSON_SERDE(RenameTableRequest)
+
+#undef ICEBERG_DECLARE_JSON_SERDE
} // namespace iceberg::rest
diff --git a/src/iceberg/test/rest_catalog_test.cc
b/src/iceberg/test/rest_catalog_test.cc
index b10f8c5..fda9ef6 100644
--- a/src/iceberg/test/rest_catalog_test.cc
+++ b/src/iceberg/test/rest_catalog_test.cc
@@ -51,7 +51,7 @@ class RestCatalogIntegrationTest : public ::testing::Test {
std::thread server_thread_;
};
-TEST_F(RestCatalogIntegrationTest, GetConfigSuccessfully) {
+TEST_F(RestCatalogIntegrationTest, DISABLED_GetConfigSuccessfully) {
server_->Get("/v1/config", [](const httplib::Request&, httplib::Response&
res) {
res.status = 200;
res.set_content(R"({"warehouse": "s3://test-bucket"})",
"application/json");
@@ -68,7 +68,7 @@ TEST_F(RestCatalogIntegrationTest, GetConfigSuccessfully) {
EXPECT_EQ(json_body["warehouse"], "s3://test-bucket");
}
-TEST_F(RestCatalogIntegrationTest, ListNamespacesReturnsMultipleResults) {
+TEST_F(RestCatalogIntegrationTest,
DISABLED_ListNamespacesReturnsMultipleResults) {
server_->Get("/v1/namespaces", [](const httplib::Request&,
httplib::Response& res) {
res.status = 200;
res.set_content(R"({
@@ -93,7 +93,7 @@ TEST_F(RestCatalogIntegrationTest,
ListNamespacesReturnsMultipleResults) {
EXPECT_THAT(json_body["namespaces"][0][0], "accounting");
}
-TEST_F(RestCatalogIntegrationTest, HandlesServerError) {
+TEST_F(RestCatalogIntegrationTest, DISABLED_HandlesServerError) {
server_->Get("/v1/config", [](const httplib::Request&, httplib::Response&
res) {
res.status = 500;
res.set_content("Internal Server Error", "text/plain");
diff --git a/src/iceberg/test/rest_json_internal_test.cc
b/src/iceberg/test/rest_json_internal_test.cc
index c042f7f..d95f6a2 100644
--- a/src/iceberg/test/rest_json_internal_test.cc
+++ b/src/iceberg/test/rest_json_internal_test.cc
@@ -34,6 +34,7 @@
namespace iceberg::rest {
+// TODO(gangwu): perhaps add these equality operators to the types themselves?
bool operator==(const CreateNamespaceRequest& lhs, const
CreateNamespaceRequest& rhs) {
return lhs.namespace_.levels == rhs.namespace_.levels &&
lhs.properties == rhs.properties;
@@ -91,39 +92,100 @@ bool operator==(const RenameTableRequest& lhs, const
RenameTableRequest& rhs) {
lhs.destination.name == rhs.destination.name;
}
-struct CreateNamespaceRequestParam {
+// Test parameter structure for roundtrip tests
+template <typename Model>
+struct JsonRoundTripParam {
std::string test_name;
std::string expected_json_str;
- Namespace namespace_;
- std::unordered_map<std::string, std::string> properties;
+ Model model;
};
-class CreateNamespaceRequestTest
- : public ::testing::TestWithParam<CreateNamespaceRequestParam> {
+// Generic test class for roundtrip tests
+template <typename Model>
+class JsonRoundTripTest : public
::testing::TestWithParam<JsonRoundTripParam<Model>> {
+ using Base = ::testing::TestWithParam<JsonRoundTripParam<Model>>;
+
protected:
void TestRoundTrip() {
- const auto& param = GetParam();
-
- // Build original object
- CreateNamespaceRequest original;
- original.namespace_ = param.namespace_;
- original.properties = param.properties;
+ const auto& param = Base::GetParam();
- // ToJson and verify JSON string
- auto json = ToJson(original);
+ // ToJson
+ auto json = ToJson(param.model);
auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json) << "ToJson mismatch";
+ ASSERT_EQ(json, expected_json) << "ToJson mismatch";
+
+ // FromJson
+ auto result = FromJson<Model>(expected_json);
+ ASSERT_THAT(result, IsOk()) << result.error().message;
+ auto parsed = std::move(result.value());
+ ASSERT_EQ(parsed, param.model);
+ }
+};
+
+#define DECLARE_ROUNDTRIP_TEST(Model) \
+ using Model##Test = JsonRoundTripTest<Model>; \
+ using Model##Param = JsonRoundTripParam<Model>; \
+ TEST_P(Model##Test, RoundTrip) { TestRoundTrip(); }
+
+// Invalid JSON test parameter structure
+template <typename Model>
+struct JsonInvalidParam {
+ std::string test_name;
+ std::string invalid_json_str;
+ std::string expected_error_message;
+};
+
+// Generic test class for invalid JSON deserialization
+template <typename Model>
+class JsonInvalidTest : public
::testing::TestWithParam<JsonInvalidParam<Model>> {
+ using Base = ::testing::TestWithParam<JsonInvalidParam<Model>>;
+
+ protected:
+ void TestInvalidJson() {
+ const auto& param = Base::GetParam();
+
+ auto result =
FromJson<Model>(nlohmann::json::parse(param.invalid_json_str));
+ ASSERT_THAT(result, IsError(ErrorKind::kJsonParseError));
+ ASSERT_THAT(result, HasErrorMessage(param.expected_error_message))
+ << result.error().message;
+ }
+};
- // FromJson and verify object equality
- auto result = CreateNamespaceRequestFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
+#define DECLARE_INVALID_TEST(Model) \
+ using Model##InvalidTest = JsonInvalidTest<Model>; \
+ using Model##InvalidParam = JsonInvalidParam<Model>; \
+ TEST_P(Model##InvalidTest, InvalidJson) { TestInvalidJson(); }
- EXPECT_EQ(parsed, original);
+// Deserialization test parameter structure
+template <typename Model>
+struct JsonDeserParam {
+ std::string test_name;
+ std::string json_str;
+ Model expected_model;
+};
+
+// Generic test class for deserialization tests (FromJson only)
+template <typename Model>
+class JsonDeserTest : public ::testing::TestWithParam<JsonDeserParam<Model>> {
+ using Base = ::testing::TestWithParam<JsonDeserParam<Model>>;
+
+ protected:
+ void TestDeserialize() {
+ const auto& param = Base::GetParam();
+
+ auto result = FromJson<Model>(nlohmann::json::parse(param.json_str));
+ ASSERT_THAT(result, IsOk()) << result.error().message;
+ auto parsed = std::move(result.value());
+ ASSERT_EQ(parsed, param.expected_model);
}
};
-TEST_P(CreateNamespaceRequestTest, RoundTrip) { TestRoundTrip(); }
+#define DECLARE_DESERIALIZE_TEST(Model) \
+ using Model##DeserializeTest = JsonDeserTest<Model>; \
+ using Model##DeserializeParam = JsonDeserParam<Model>; \
+ TEST_P(Model##DeserializeTest, Deserialize) { TestDeserialize(); }
+
+DECLARE_ROUNDTRIP_TEST(CreateNamespaceRequest)
INSTANTIATE_TEST_SUITE_P(
CreateNamespaceRequestCases, CreateNamespaceRequestTest,
@@ -133,787 +195,556 @@ INSTANTIATE_TEST_SUITE_P(
.test_name = "FullRequest",
.expected_json_str =
R"({"namespace":["accounting","tax"],"properties":{"owner":"Hank"}})",
- .namespace_ = Namespace{{"accounting", "tax"}},
- .properties = {{"owner", "Hank"}},
- },
+ .model = {.namespace_ = Namespace{{"accounting", "tax"}},
+ .properties = {{"owner", "Hank"}}}},
// Request with empty properties (omit properties field when empty)
CreateNamespaceRequestParam{
.test_name = "EmptyProperties",
.expected_json_str = R"({"namespace":["accounting","tax"]})",
- .namespace_ = Namespace{{"accounting", "tax"}},
- .properties = {},
+ .model = {.namespace_ = Namespace{{"accounting", "tax"}}},
},
// Request with empty namespace
CreateNamespaceRequestParam{
.test_name = "EmptyNamespace",
.expected_json_str = R"({"namespace":[]})",
- .namespace_ = Namespace{},
- .properties = {},
+ .model = {.namespace_ = Namespace{}, .properties = {}},
}),
[](const ::testing::TestParamInfo<CreateNamespaceRequestParam>& info) {
return info.param.test_name;
});
-TEST(CreateNamespaceRequestTest, DeserializeWithoutDefaults) {
- // Properties is null
- std::string json_null_props =
R"({"namespace":["accounting","tax"],"properties":null})";
- auto result1 =
CreateNamespaceRequestFromJson(nlohmann::json::parse(json_null_props));
- ASSERT_TRUE(result1.has_value());
- EXPECT_EQ(result1.value().namespace_.levels,
- std::vector<std::string>({"accounting", "tax"}));
- EXPECT_TRUE(result1.value().properties.empty());
-
- // Properties is missing
- std::string json_missing_props = R"({"namespace":["accounting","tax"]})";
- auto result2 =
-
CreateNamespaceRequestFromJson(nlohmann::json::parse(json_missing_props));
- ASSERT_TRUE(result2.has_value());
- EXPECT_EQ(result2.value().namespace_.levels,
- std::vector<std::string>({"accounting", "tax"}));
- EXPECT_TRUE(result2.value().properties.empty());
-}
-
-TEST(CreateNamespaceRequestTest, InvalidRequests) {
- // Incorrect type for namespace
- std::string json_wrong_ns_type =
- R"({"namespace":"accounting%1Ftax","properties":null})";
- auto result1 =
-
CreateNamespaceRequestFromJson(nlohmann::json::parse(json_wrong_ns_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Failed to parse 'namespace' from "
- "{\"namespace\":\"accounting%1Ftax\",\"properties\":null}: "
- "[json.exception.type_error.302] type must be array, but is
string");
-
- // Incorrect type for properties
- std::string json_wrong_props_type =
- R"({"namespace":["accounting","tax"],"properties":[]})";
- auto result2 =
-
CreateNamespaceRequestFromJson(nlohmann::json::parse(json_wrong_props_type));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message,
- "Failed to parse 'properties' from "
- "{\"namespace\":[\"accounting\",\"tax\"],\"properties\":[]}: "
- "[json.exception.type_error.302] type must be object, but is
array");
-
- // Misspelled keys
- std::string json_misspelled =
- R"({"namepsace":["accounting","tax"],"propertiezzzz":{"owner":"Hank"}})";
- auto result3 =
CreateNamespaceRequestFromJson(nlohmann::json::parse(json_misspelled));
- EXPECT_FALSE(result3.has_value());
- EXPECT_THAT(result3, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(
- result3.error().message,
- "Missing 'namespace' in "
-
"{\"namepsace\":[\"accounting\",\"tax\"],\"propertiezzzz\":{\"owner\":\"Hank\"}}");
-
- // Empty JSON
- std::string json_empty = R"({})";
- auto result4 =
CreateNamespaceRequestFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result4.has_value());
- EXPECT_THAT(result4, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result4.error().message, "Missing 'namespace' in {}");
-}
+DECLARE_INVALID_TEST(CreateNamespaceRequest)
-struct CreateNamespaceResponseParam {
- std::string test_name;
- std::string expected_json_str;
- Namespace namespace_;
- std::unordered_map<std::string, std::string> properties;
-};
-
-class CreateNamespaceResponseTest
- : public ::testing::TestWithParam<CreateNamespaceResponseParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
-
- CreateNamespaceResponse original;
- original.namespace_ = param.namespace_;
- original.properties = param.properties;
-
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
+INSTANTIATE_TEST_SUITE_P(
+ CreateNamespaceRequestInvalidCases, CreateNamespaceRequestInvalidTest,
+ ::testing::Values(
+ // Incorrect type for namespace field
+ CreateNamespaceRequestInvalidParam{
+ .test_name = "WrongNamespaceType",
+ .invalid_json_str =
R"({"namespace":"accounting%1Ftax","properties":null})",
+ .expected_error_message = "type must be array, but is string"},
+ // Incorrect type for properties field
+ CreateNamespaceRequestInvalidParam{
+ .test_name = "WrongPropertiesType",
+ .invalid_json_str =
R"({"namespace":["accounting","tax"],"properties":[]})",
+ .expected_error_message = "type must be object, but is array"},
+ // Misspelled required field
+ CreateNamespaceRequestInvalidParam{
+ .test_name = "MisspelledKeys",
+ .invalid_json_str =
+
R"({"namepsace":["accounting","tax"],"propertiezzzz":{"owner":"Hank"}})",
+ .expected_error_message = "Missing 'namespace'"},
+ // Empty JSON object
+ CreateNamespaceRequestInvalidParam{
+ .test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing 'namespace'"}),
+ [](const ::testing::TestParamInfo<CreateNamespaceRequestInvalidParam>&
info) {
+ return info.param.test_name;
+ });
- auto result = CreateNamespaceResponseFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
+DECLARE_DESERIALIZE_TEST(CreateNamespaceRequest)
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ CreateNamespaceRequestDeserializeCases,
CreateNamespaceRequestDeserializeTest,
+ ::testing::Values(
+ // Properties field is null (should deserialize to empty map)
+ CreateNamespaceRequestDeserializeParam{
+ .test_name = "NullProperties",
+ .json_str =
R"({"namespace":["accounting","tax"],"properties":null})",
+ .expected_model = {.namespace_ = Namespace{{"accounting",
"tax"}}}},
+ // Properties field is missing (should deserialize to empty map)
+ CreateNamespaceRequestDeserializeParam{
+ .test_name = "MissingProperties",
+ .json_str = R"({"namespace":["accounting","tax"]})",
+ .expected_model = {.namespace_ = Namespace{{"accounting",
"tax"}}}}),
+ [](const ::testing::TestParamInfo<CreateNamespaceRequestDeserializeParam>&
info) {
+ return info.param.test_name;
+ });
-TEST_P(CreateNamespaceResponseTest, RoundTrip) { TestRoundTrip(); }
+DECLARE_ROUNDTRIP_TEST(CreateNamespaceResponse)
INSTANTIATE_TEST_SUITE_P(
CreateNamespaceResponseCases, CreateNamespaceResponseTest,
::testing::Values(
+ // Full response with namespace and properties
CreateNamespaceResponseParam{
.test_name = "FullResponse",
.expected_json_str =
R"({"namespace":["accounting","tax"],"properties":{"owner":"Hank"}})",
- .namespace_ = Namespace{{"accounting", "tax"}},
- .properties = {{"owner", "Hank"}},
- },
+ .model = {.namespace_ = Namespace{{"accounting", "tax"}},
+ .properties = {{"owner", "Hank"}}}},
+ // Response with empty properties (omit properties field when empty)
CreateNamespaceResponseParam{
.test_name = "EmptyProperties",
.expected_json_str = R"({"namespace":["accounting","tax"]})",
- .namespace_ = Namespace{{"accounting", "tax"}},
- .properties = {},
- },
+ .model = {.namespace_ = Namespace{{"accounting", "tax"}}}},
+ // Response with empty namespace
CreateNamespaceResponseParam{.test_name = "EmptyNamespace",
.expected_json_str =
R"({"namespace":[]})",
- .namespace_ = Namespace{},
- .properties = {}}),
+ .model = {.namespace_ = Namespace{}}}),
[](const ::testing::TestParamInfo<CreateNamespaceResponseParam>& info) {
return info.param.test_name;
});
-TEST(CreateNamespaceResponseTest, DeserializeWithoutDefaults) {
- std::string json_missing_props = R"({"namespace":["accounting","tax"]})";
- auto result1 =
-
CreateNamespaceResponseFromJson(nlohmann::json::parse(json_missing_props));
- ASSERT_TRUE(result1.has_value());
- EXPECT_TRUE(result1.value().properties.empty());
-
- std::string json_null_props =
R"({"namespace":["accounting","tax"],"properties":null})";
- auto result2 =
CreateNamespaceResponseFromJson(nlohmann::json::parse(json_null_props));
- ASSERT_TRUE(result2.has_value());
- EXPECT_TRUE(result2.value().properties.empty());
-}
-
-TEST(CreateNamespaceResponseTest, InvalidResponses) {
- std::string json_wrong_ns_type =
- R"({"namespace":"accounting%1Ftax","properties":null})";
- auto result1 =
-
CreateNamespaceResponseFromJson(nlohmann::json::parse(json_wrong_ns_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Failed to parse 'namespace' from "
- "{\"namespace\":\"accounting%1Ftax\",\"properties\":null}: "
- "[json.exception.type_error.302] type must be array, but is
string");
-
- std::string json_wrong_props_type =
- R"({"namespace":["accounting","tax"],"properties":[]})";
- auto result2 =
-
CreateNamespaceResponseFromJson(nlohmann::json::parse(json_wrong_props_type));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message,
- "Failed to parse 'properties' from "
- "{\"namespace\":[\"accounting\",\"tax\"],\"properties\":[]}: "
- "[json.exception.type_error.302] type must be object, but is
array");
-
- std::string json_empty = R"({})";
- auto result3 =
CreateNamespaceResponseFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result3.has_value());
- EXPECT_THAT(result3, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result3.error().message, "Missing 'namespace' in {}");
-}
+DECLARE_DESERIALIZE_TEST(CreateNamespaceResponse)
-struct GetNamespaceResponseParam {
- std::string test_name;
- std::string expected_json_str;
- Namespace namespace_;
- std::unordered_map<std::string, std::string> properties;
-};
-
-class GetNamespaceResponseTest
- : public ::testing::TestWithParam<GetNamespaceResponseParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
-
- GetNamespaceResponse original;
- original.namespace_ = param.namespace_;
- original.properties = param.properties;
-
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
+INSTANTIATE_TEST_SUITE_P(
+ CreateNamespaceResponseDeserializeCases,
CreateNamespaceResponseDeserializeTest,
+ ::testing::Values(
+ // Properties field is missing (should deserialize to empty map)
+ CreateNamespaceResponseDeserializeParam{
+ .test_name = "MissingProperties",
+ .json_str = R"({"namespace":["accounting","tax"]})",
+ .expected_model = {.namespace_ = Namespace{{"accounting",
"tax"}}}},
+ // Properties field is null (should deserialize to empty map)
+ CreateNamespaceResponseDeserializeParam{
+ .test_name = "NullProperties",
+ .json_str =
R"({"namespace":["accounting","tax"],"properties":null})",
+ .expected_model = {.namespace_ = Namespace{{"accounting",
"tax"}}}}),
+ [](const
::testing::TestParamInfo<CreateNamespaceResponseDeserializeParam>& info) {
+ return info.param.test_name;
+ });
- auto result = GetNamespaceResponseFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
+DECLARE_INVALID_TEST(CreateNamespaceResponse)
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ CreateNamespaceResponseInvalidCases, CreateNamespaceResponseInvalidTest,
+ ::testing::Values(
+ // Incorrect type for namespace field
+ CreateNamespaceResponseInvalidParam{
+ .test_name = "WrongNamespaceType",
+ .invalid_json_str =
R"({"namespace":"accounting%1Ftax","properties":null})",
+ .expected_error_message = "type must be array, but is string"},
+ // Incorrect type for properties field
+ CreateNamespaceResponseInvalidParam{
+ .test_name = "WrongPropertiesType",
+ .invalid_json_str =
R"({"namespace":["accounting","tax"],"properties":[]})",
+ .expected_error_message = "type must be object, but is array"},
+ // Empty JSON object
+ CreateNamespaceResponseInvalidParam{
+ .test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing 'namespace'"}),
+ [](const ::testing::TestParamInfo<CreateNamespaceResponseInvalidParam>&
info) {
+ return info.param.test_name;
+ });
-TEST_P(GetNamespaceResponseTest, RoundTrip) { TestRoundTrip(); }
+DECLARE_ROUNDTRIP_TEST(GetNamespaceResponse)
INSTANTIATE_TEST_SUITE_P(
GetNamespaceResponseCases, GetNamespaceResponseTest,
::testing::Values(
+ // Full response with namespace and properties
GetNamespaceResponseParam{
.test_name = "FullResponse",
.expected_json_str =
R"({"namespace":["accounting","tax"],"properties":{"owner":"Hank"}})",
- .namespace_ = Namespace{{"accounting", "tax"}},
- .properties = {{"owner", "Hank"}}},
+ .model = {.namespace_ = Namespace{{"accounting", "tax"}},
+ .properties = {{"owner", "Hank"}}}},
+ // Response with empty properties (omit properties field when empty)
GetNamespaceResponseParam{
.test_name = "EmptyProperties",
.expected_json_str = R"({"namespace":["accounting","tax"]})",
- .namespace_ = Namespace{{"accounting", "tax"}},
- .properties = {}}),
+ .model = {.namespace_ = Namespace{{"accounting", "tax"}}}}),
[](const ::testing::TestParamInfo<GetNamespaceResponseParam>& info) {
return info.param.test_name;
});
-TEST(GetNamespaceResponseTest, DeserializeWithoutDefaults) {
- std::string json_null_props =
R"({"namespace":["accounting","tax"],"properties":null})";
- auto result =
GetNamespaceResponseFromJson(nlohmann::json::parse(json_null_props));
- ASSERT_TRUE(result.has_value());
- EXPECT_TRUE(result.value().properties.empty());
-}
-
-TEST(GetNamespaceResponseTest, InvalidResponses) {
- std::string json_wrong_ns_type =
- R"({"namespace":"accounting%1Ftax","properties":null})";
- auto result1 =
GetNamespaceResponseFromJson(nlohmann::json::parse(json_wrong_ns_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Failed to parse 'namespace' from "
- "{\"namespace\":\"accounting%1Ftax\",\"properties\":null}: "
- "[json.exception.type_error.302] type must be array, but is
string");
-
- std::string json_wrong_props_type =
- R"({"namespace":["accounting","tax"],"properties":[]})";
- auto result2 =
-
GetNamespaceResponseFromJson(nlohmann::json::parse(json_wrong_props_type));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message,
- "Failed to parse 'properties' from "
- "{\"namespace\":[\"accounting\",\"tax\"],\"properties\":[]}: "
- "[json.exception.type_error.302] type must be object, but is
array");
-
- std::string json_empty = R"({})";
- auto result3 =
GetNamespaceResponseFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result3.has_value());
- EXPECT_THAT(result3, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result3.error().message, "Missing 'namespace' in {}");
-}
-
-struct ListNamespacesResponseParam {
- std::string test_name;
- std::string expected_json_str;
- std::vector<Namespace> namespaces;
- std::string next_page_token;
-};
-
-class ListNamespacesResponseTest
- : public ::testing::TestWithParam<ListNamespacesResponseParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
-
- ListNamespacesResponse original;
- original.namespaces = param.namespaces;
- original.next_page_token = param.next_page_token;
+DECLARE_DESERIALIZE_TEST(GetNamespaceResponse)
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
+INSTANTIATE_TEST_SUITE_P(
+ GetNamespaceResponseDeserializeCases, GetNamespaceResponseDeserializeTest,
+ ::testing::Values(
+ // Properties field is null (should deserialize to empty map)
+ GetNamespaceResponseDeserializeParam{
+ .test_name = "NullProperties",
+ .json_str =
R"({"namespace":["accounting","tax"],"properties":null})",
+ .expected_model = {.namespace_ = Namespace{{"accounting",
"tax"}}}}),
+ [](const ::testing::TestParamInfo<GetNamespaceResponseDeserializeParam>&
info) {
+ return info.param.test_name;
+ });
- auto result = ListNamespacesResponseFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
+DECLARE_INVALID_TEST(GetNamespaceResponse)
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ GetNamespaceResponseInvalidCases, GetNamespaceResponseInvalidTest,
+ ::testing::Values(
+ // Incorrect type for namespace field
+ GetNamespaceResponseInvalidParam{
+ .test_name = "WrongNamespaceType",
+ .invalid_json_str =
R"({"namespace":"accounting%1Ftax","properties":null})",
+ .expected_error_message = "type must be array, but is string"},
+ // Incorrect type for properties field
+ GetNamespaceResponseInvalidParam{
+ .test_name = "WrongPropertiesType",
+ .invalid_json_str =
R"({"namespace":["accounting","tax"],"properties":[]})",
+ .expected_error_message = "type must be object, but is array"},
+ // Empty JSON object
+ GetNamespaceResponseInvalidParam{
+ .test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing 'namespace'"}),
+ [](const ::testing::TestParamInfo<GetNamespaceResponseInvalidParam>& info)
{
+ return info.param.test_name;
+ });
-TEST_P(ListNamespacesResponseTest, RoundTrip) { TestRoundTrip(); }
+DECLARE_ROUNDTRIP_TEST(ListNamespacesResponse)
INSTANTIATE_TEST_SUITE_P(
ListNamespacesResponseCases, ListNamespacesResponseTest,
::testing::Values(
+ // Full response with multiple namespaces
ListNamespacesResponseParam{
.test_name = "FullResponse",
.expected_json_str = R"({"namespaces":[["accounting"],["tax"]]})",
- .namespaces = {Namespace{{"accounting"}}, Namespace{{"tax"}}},
- .next_page_token = ""},
+ .model = {.next_page_token = "",
+ .namespaces = {Namespace{{"accounting"}},
Namespace{{"tax"}}}}},
+ // Response with empty namespaces
ListNamespacesResponseParam{.test_name = "EmptyNamespaces",
.expected_json_str =
R"({"namespaces":[]})",
- .namespaces = {},
- .next_page_token = ""},
+ .model = {.next_page_token = ""}},
+ // Response with page token
ListNamespacesResponseParam{
.test_name = "WithPageToken",
.expected_json_str =
R"({"namespaces":[["accounting"],["tax"]],"next-page-token":"token"})",
- .namespaces = {Namespace{{"accounting"}}, Namespace{{"tax"}}},
- .next_page_token = "token"}),
+ .model = {.next_page_token = "token",
+ .namespaces = {Namespace{{"accounting"}},
Namespace{{"tax"}}}}}),
[](const ::testing::TestParamInfo<ListNamespacesResponseParam>& info) {
return info.param.test_name;
});
-TEST(ListNamespacesResponseTest, InvalidResponses) {
- std::string json_wrong_type = R"({"namespaces":"accounting"})";
- auto result1 =
ListNamespacesResponseFromJson(nlohmann::json::parse(json_wrong_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Cannot parse namespace from non-array:\"accounting\"");
-
- std::string json_empty = R"({})";
- auto result2 =
ListNamespacesResponseFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message, "Missing 'namespaces' in {}");
-}
-
-struct UpdateNamespacePropertiesRequestParam {
- std::string test_name;
- std::string expected_json_str;
- std::vector<std::string> removals;
- std::unordered_map<std::string, std::string> updates;
-};
-
-class UpdateNamespacePropertiesRequestTest
- : public ::testing::TestWithParam<UpdateNamespacePropertiesRequestParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
-
- UpdateNamespacePropertiesRequest original;
- original.removals = param.removals;
- original.updates = param.updates;
-
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
-
- auto result = UpdateNamespacePropertiesRequestFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
+DECLARE_INVALID_TEST(ListNamespacesResponse)
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ ListNamespacesResponseInvalidCases, ListNamespacesResponseInvalidTest,
+ ::testing::Values(
+ // Incorrect type for namespaces field
+ ListNamespacesResponseInvalidParam{
+ .test_name = "WrongNamespacesType",
+ .invalid_json_str = R"({"namespaces":"accounting"})",
+ .expected_error_message = "Cannot parse namespace from non-array"},
+ // Empty JSON object
+ ListNamespacesResponseInvalidParam{
+ .test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing 'namespaces'"}),
+ [](const ::testing::TestParamInfo<ListNamespacesResponseInvalidParam>&
info) {
+ return info.param.test_name;
+ });
-TEST_P(UpdateNamespacePropertiesRequestTest, RoundTrip) { TestRoundTrip(); }
+DECLARE_ROUNDTRIP_TEST(UpdateNamespacePropertiesRequest)
INSTANTIATE_TEST_SUITE_P(
UpdateNamespacePropertiesRequestCases,
UpdateNamespacePropertiesRequestTest,
::testing::Values(
+ // Full request with both removals and updates
UpdateNamespacePropertiesRequestParam{
.test_name = "FullRequest",
.expected_json_str =
R"({"removals":["foo","bar"],"updates":{"owner":"Hank"}})",
- .removals = {"foo", "bar"},
- .updates = {{"owner", "Hank"}}},
+ .model = {.removals = {"foo", "bar"}, .updates = {{"owner",
"Hank"}}}},
+ // Request with only updates
UpdateNamespacePropertiesRequestParam{
.test_name = "OnlyUpdates",
.expected_json_str = R"({"updates":{"owner":"Hank"}})",
- .removals = {},
- .updates = {{"owner", "Hank"}}},
+ .model = {.updates = {{"owner", "Hank"}}}},
+ // Request with only removals
UpdateNamespacePropertiesRequestParam{
.test_name = "OnlyRemovals",
.expected_json_str = R"({"removals":["foo","bar"]})",
- .removals = {"foo", "bar"},
- .updates = {}},
- UpdateNamespacePropertiesRequestParam{.test_name = "AllEmpty",
- .expected_json_str = R"({})",
- .removals = {},
- .updates = {}}),
+ .model = {.removals = {"foo", "bar"}}},
+ // Request with all empty fields
+ UpdateNamespacePropertiesRequestParam{
+ .test_name = "AllEmpty", .expected_json_str = R"({})", .model =
{}}),
[](const ::testing::TestParamInfo<UpdateNamespacePropertiesRequestParam>&
info) {
return info.param.test_name;
});
-TEST(UpdateNamespacePropertiesRequestTest, DeserializeWithoutDefaults) {
- // Removals is null
- std::string json1 = R"({"removals":null,"updates":{"owner":"Hank"}})";
- auto result1 =
UpdateNamespacePropertiesRequestFromJson(nlohmann::json::parse(json1));
- ASSERT_TRUE(result1.has_value());
- EXPECT_TRUE(result1.value().removals.empty());
-
- // Removals is missing
- std::string json2 = R"({"updates":{"owner":"Hank"}})";
- auto result2 =
UpdateNamespacePropertiesRequestFromJson(nlohmann::json::parse(json2));
- ASSERT_TRUE(result2.has_value());
- EXPECT_TRUE(result2.value().removals.empty());
-
- // Updates is null
- std::string json3 = R"({"removals":["foo","bar"],"updates":null})";
- auto result3 =
UpdateNamespacePropertiesRequestFromJson(nlohmann::json::parse(json3));
- ASSERT_TRUE(result3.has_value());
- EXPECT_TRUE(result3.value().updates.empty());
-
- // All missing
- std::string json4 = R"({})";
- auto result4 =
UpdateNamespacePropertiesRequestFromJson(nlohmann::json::parse(json4));
- ASSERT_TRUE(result4.has_value());
- EXPECT_TRUE(result4.value().removals.empty());
- EXPECT_TRUE(result4.value().updates.empty());
-}
-
-TEST(UpdateNamespacePropertiesRequestTest, InvalidRequests) {
- std::string json_wrong_removals_type =
- R"({"removals":{"foo":"bar"},"updates":{"owner":"Hank"}})";
- auto result1 = UpdateNamespacePropertiesRequestFromJson(
- nlohmann::json::parse(json_wrong_removals_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Failed to parse 'removals' from "
-
"{\"removals\":{\"foo\":\"bar\"},\"updates\":{\"owner\":\"Hank\"}}: "
- "[json.exception.type_error.302] type must be array, but is
object");
-
- std::string json_wrong_updates_type =
- R"({"removals":["foo","bar"],"updates":["owner"]})";
- auto result2 = UpdateNamespacePropertiesRequestFromJson(
- nlohmann::json::parse(json_wrong_updates_type));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message,
- "Failed to parse 'updates' from "
- "{\"removals\":[\"foo\",\"bar\"],\"updates\":[\"owner\"]}: "
- "[json.exception.type_error.302] type must be object, but is
array");
-}
-
-struct UpdateNamespacePropertiesResponseParam {
- std::string test_name;
- std::string expected_json_str;
- std::vector<std::string> updated;
- std::vector<std::string> removed;
- std::vector<std::string> missing;
-};
-
-class UpdateNamespacePropertiesResponseTest
- : public ::testing::TestWithParam<UpdateNamespacePropertiesResponseParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
-
- UpdateNamespacePropertiesResponse original;
- original.updated = param.updated;
- original.removed = param.removed;
- original.missing = param.missing;
+DECLARE_DESERIALIZE_TEST(UpdateNamespacePropertiesRequest)
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
-
- auto result = UpdateNamespacePropertiesResponseFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
-
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ UpdateNamespacePropertiesRequestDeserializeCases,
+ UpdateNamespacePropertiesRequestDeserializeTest,
+ ::testing::Values(
+ // Removals is null (should deserialize to empty vector)
+ UpdateNamespacePropertiesRequestDeserializeParam{
+ .test_name = "NullRemovals",
+ .json_str = R"({"removals":null,"updates":{"owner":"Hank"}})",
+ .expected_model = {.updates = {{"owner", "Hank"}}}},
+ // Removals is missing (should deserialize to empty vector)
+ UpdateNamespacePropertiesRequestDeserializeParam{
+ .test_name = "MissingRemovals",
+ .json_str = R"({"updates":{"owner":"Hank"}})",
+ .expected_model = {.updates = {{"owner", "Hank"}}}},
+ // Updates is null (should deserialize to empty map)
+ UpdateNamespacePropertiesRequestDeserializeParam{
+ .test_name = "NullUpdates",
+ .json_str = R"({"removals":["foo","bar"],"updates":null})",
+ .expected_model = {.removals = {"foo", "bar"}}},
+ // All fields missing (should deserialize to empty)
+ UpdateNamespacePropertiesRequestDeserializeParam{
+ .test_name = "AllMissing", .json_str = R"({})", .expected_model =
{}}),
+ [](const
::testing::TestParamInfo<UpdateNamespacePropertiesRequestDeserializeParam>&
+ info) { return info.param.test_name; });
+
+DECLARE_INVALID_TEST(UpdateNamespacePropertiesRequest)
-TEST_P(UpdateNamespacePropertiesResponseTest, RoundTrip) { TestRoundTrip(); }
+INSTANTIATE_TEST_SUITE_P(
+ UpdateNamespacePropertiesRequestInvalidCases,
+ UpdateNamespacePropertiesRequestInvalidTest,
+ ::testing::Values(
+ // Incorrect type for removals field
+ UpdateNamespacePropertiesRequestInvalidParam{
+ .test_name = "WrongRemovalsType",
+ .invalid_json_str =
+ R"({"removals":{"foo":"bar"},"updates":{"owner":"Hank"}})",
+ .expected_error_message = "type must be array, but is object"},
+ // Incorrect type for updates field
+ UpdateNamespacePropertiesRequestInvalidParam{
+ .test_name = "WrongUpdatesType",
+ .invalid_json_str =
R"({"removals":["foo","bar"],"updates":["owner"]})",
+ .expected_error_message = "type must be object, but is array"}),
+ [](const
::testing::TestParamInfo<UpdateNamespacePropertiesRequestInvalidParam>&
+ info) { return info.param.test_name; });
+
+DECLARE_ROUNDTRIP_TEST(UpdateNamespacePropertiesResponse)
INSTANTIATE_TEST_SUITE_P(
UpdateNamespacePropertiesResponseCases,
UpdateNamespacePropertiesResponseTest,
::testing::Values(
+ // Full response with updated, removed, and missing fields
UpdateNamespacePropertiesResponseParam{
.test_name = "FullResponse",
.expected_json_str =
R"({"removed":["foo"],"updated":["owner"],"missing":["bar"]})",
- .updated = {"owner"},
- .removed = {"foo"},
- .missing = {"bar"}},
+ .model = {.updated = {"owner"}, .removed = {"foo"}, .missing =
{"bar"}}},
+ // Response with only updated field
UpdateNamespacePropertiesResponseParam{
.test_name = "OnlyUpdated",
.expected_json_str = R"({"removed":[],"updated":["owner"]})",
- .updated = {"owner"},
- .removed = {},
- .missing = {}},
+ .model = {.updated = {"owner"}}},
+ // Response with only removed field
UpdateNamespacePropertiesResponseParam{
.test_name = "OnlyRemoved",
.expected_json_str = R"({"removed":["foo"],"updated":[]})",
- .updated = {},
- .removed = {"foo"},
- .missing = {}},
+ .model = {.removed = {"foo"}}},
+ // Response with only missing field
UpdateNamespacePropertiesResponseParam{
.test_name = "OnlyMissing",
.expected_json_str =
R"({"removed":[],"updated":[],"missing":["bar"]})",
- .updated = {},
- .removed = {},
- .missing = {"bar"}},
+ .model = {.missing = {"bar"}}},
+ // Response with all empty fields
UpdateNamespacePropertiesResponseParam{
.test_name = "AllEmpty",
.expected_json_str = R"({"removed":[],"updated":[]})",
- .updated = {},
- .removed = {},
- .missing = {}}),
+ .model = {}}),
[](const ::testing::TestParamInfo<UpdateNamespacePropertiesResponseParam>&
info) {
return info.param.test_name;
});
-TEST(UpdateNamespacePropertiesResponseTest, DeserializeWithoutDefaults) {
- // Only updated, others missing
- std::string json2 = R"({"updated":["owner"],"removed":[]})";
- auto result2 =
UpdateNamespacePropertiesResponseFromJson(nlohmann::json::parse(json2));
- ASSERT_TRUE(result2.has_value());
- EXPECT_EQ(result2.value().updated, std::vector<std::string>({"owner"}));
- EXPECT_TRUE(result2.value().removed.empty());
- EXPECT_TRUE(result2.value().missing.empty());
-
- // All missing
- std::string json3 = R"({})";
- auto result3 =
UpdateNamespacePropertiesResponseFromJson(nlohmann::json::parse(json3));
- EXPECT_FALSE(result3.has_value()); // updated and removed are required
-}
-
-TEST(UpdateNamespacePropertiesResponseTest, InvalidResponses) {
- std::string json_wrong_removed_type =
- R"({"removed":{"foo":true},"updated":["owner"],"missing":["bar"]})";
- auto result1 = UpdateNamespacePropertiesResponseFromJson(
- nlohmann::json::parse(json_wrong_removed_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Failed to parse 'removed' from "
-
"{\"missing\":[\"bar\"],\"removed\":{\"foo\":true},\"updated\":[\"owner\"]}: "
- "[json.exception.type_error.302] type must be array, but is
object");
-
- std::string json_wrong_updated_type =
R"({"updated":"owner","missing":["bar"]})";
- auto result2 = UpdateNamespacePropertiesResponseFromJson(
- nlohmann::json::parse(json_wrong_updated_type));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(
- result2.error().message,
- "Failed to parse 'updated' from
{\"missing\":[\"bar\"],\"updated\":\"owner\"}: "
- "[json.exception.type_error.302] type must be array, but is string");
-}
-
-struct ListTablesResponseParam {
- std::string test_name;
- std::string expected_json_str;
- std::vector<TableIdentifier> identifiers;
- std::string next_page_token;
-};
-
-class ListTablesResponseTest : public
::testing::TestWithParam<ListTablesResponseParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
+DECLARE_DESERIALIZE_TEST(UpdateNamespacePropertiesResponse)
- ListTablesResponse original;
- original.identifiers = param.identifiers;
- original.next_page_token = param.next_page_token;
-
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
-
- auto result = ListTablesResponseFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
-
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ UpdateNamespacePropertiesResponseDeserializeCases,
+ UpdateNamespacePropertiesResponseDeserializeTest,
+ ::testing::Values(
+ // Only updated and removed present, missing is optional
+ UpdateNamespacePropertiesResponseDeserializeParam{
+ .test_name = "MissingOptional",
+ .json_str = R"({"updated":["owner"],"removed":[]})",
+ .expected_model = {.updated = {"owner"}}},
+ // All fields are missing
+ UpdateNamespacePropertiesResponseDeserializeParam{
+ .test_name = "AllMissing", .json_str = R"({})", .expected_model =
{}}),
+ [](const
::testing::TestParamInfo<UpdateNamespacePropertiesResponseDeserializeParam>&
+ info) { return info.param.test_name; });
+
+DECLARE_INVALID_TEST(UpdateNamespacePropertiesResponse)
-TEST_P(ListTablesResponseTest, RoundTrip) { TestRoundTrip(); }
+INSTANTIATE_TEST_SUITE_P(
+ UpdateNamespacePropertiesResponseInvalidCases,
+ UpdateNamespacePropertiesResponseInvalidTest,
+ ::testing::Values(
+ // Incorrect type for removed field
+ UpdateNamespacePropertiesResponseInvalidParam{
+ .test_name = "WrongRemovedType",
+ .invalid_json_str =
+
R"({"removed":{"foo":true},"updated":["owner"],"missing":["bar"]})",
+ .expected_error_message = "type must be array, but is object"},
+ // Incorrect type for updated field
+ UpdateNamespacePropertiesResponseInvalidParam{
+ .test_name = "WrongUpdatedType",
+ .invalid_json_str = R"({"updated":"owner","missing":["bar"]})",
+ .expected_error_message = "type must be array, but is string"},
+ // Valid top-level (array) types, but at least one entry in the list
is not the
+ // expected type
+ UpdateNamespacePropertiesResponseInvalidParam{
+ .test_name = "InvalidArrayEntryType",
+ .invalid_json_str =
+ R"({"removed":["foo", "bar",
123456],"updated":["owner"],"missing":["bar"]})",
+ .expected_error_message = " type must be string, but is number"}),
+ [](const
::testing::TestParamInfo<UpdateNamespacePropertiesResponseInvalidParam>&
+ info) { return info.param.test_name; });
+
+DECLARE_ROUNDTRIP_TEST(ListTablesResponse)
INSTANTIATE_TEST_SUITE_P(
ListTablesResponseCases, ListTablesResponseTest,
::testing::Values(
+ // Full response with table identifiers
ListTablesResponseParam{
.test_name = "FullResponse",
.expected_json_str =
R"({"identifiers":[{"namespace":["accounting","tax"],"name":"paid"}]})",
- .identifiers = {TableIdentifier{Namespace{{"accounting", "tax"}},
"paid"}},
- .next_page_token = ""},
+ .model = {.next_page_token = "",
+ .identifiers = {TableIdentifier{Namespace{{"accounting",
"tax"}},
+ "paid"}}}},
+ // Response with empty identifiers
ListTablesResponseParam{.test_name = "EmptyIdentifiers",
.expected_json_str = R"({"identifiers":[]})",
- .identifiers = {},
- .next_page_token = ""},
+ .model = {.next_page_token = ""}},
+ // Response with page token
ListTablesResponseParam{
.test_name = "WithPageToken",
.expected_json_str =
R"({"identifiers":[{"namespace":["accounting","tax"],"name":"paid"}],"next-page-token":"token"})",
- .identifiers = {TableIdentifier{Namespace{{"accounting", "tax"}},
"paid"}},
- .next_page_token = "token"}),
+ .model = {.next_page_token = "token",
+ .identifiers = {TableIdentifier{Namespace{{"accounting",
"tax"}},
+ "paid"}}}}),
[](const ::testing::TestParamInfo<ListTablesResponseParam>& info) {
return info.param.test_name;
});
-TEST(ListTablesResponseTest, InvalidResponses) {
- std::string json_wrong_type = R"({"identifiers":"accounting%1Ftax"})";
- auto result1 =
ListTablesResponseFromJson(nlohmann::json::parse(json_wrong_type));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message, "Missing 'name' in \"accounting%1Ftax\"");
-
- std::string json_empty = R"({})";
- auto result2 = ListTablesResponseFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message, "Missing 'identifiers' in {}");
-
- std::string json_invalid_identifier =
- R"({"identifiers":[{"namespace":"accounting.tax","name":"paid"}]})";
- auto result3 =
-
ListTablesResponseFromJson(nlohmann::json::parse(json_invalid_identifier));
- EXPECT_FALSE(result3.has_value());
- EXPECT_THAT(result3, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result3.error().message,
- "Failed to parse 'namespace' from "
- "{\"name\":\"paid\",\"namespace\":\"accounting.tax\"}: "
- "[json.exception.type_error.302] type must be array, but is
string");
-}
+DECLARE_INVALID_TEST(ListTablesResponse)
-struct RenameTableRequestParam {
- std::string test_name;
- std::string expected_json_str;
- TableIdentifier source;
- TableIdentifier destination;
-};
-
-class RenameTableRequestTest : public
::testing::TestWithParam<RenameTableRequestParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
-
- RenameTableRequest original;
- original.source = param.source;
- original.destination = param.destination;
-
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
-
- auto result = RenameTableRequestFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
-
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ ListTablesResponseInvalidCases, ListTablesResponseInvalidTest,
+ ::testing::Values(
+ // Incorrect type for identifiers field (string instead of array)
+ ListTablesResponseInvalidParam{
+ .test_name = "WrongIdentifiersType",
+ .invalid_json_str = R"({"identifiers":"accounting%1Ftax"})",
+ .expected_error_message = "Missing 'name'"},
+ // Empty JSON object
+ ListTablesResponseInvalidParam{.test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing
'identifiers'"},
+ // Invalid identifier with wrong namespace type
+ ListTablesResponseInvalidParam{
+ .test_name = "InvalidIdentifierNamespaceType",
+ .invalid_json_str =
+
R"({"identifiers":[{"namespace":"accounting.tax","name":"paid"}]})",
+ .expected_error_message = "type must be array, but is string"}),
+ [](const ::testing::TestParamInfo<ListTablesResponseInvalidParam>& info) {
+ return info.param.test_name;
+ });
-TEST_P(RenameTableRequestTest, RoundTrip) { TestRoundTrip(); }
+DECLARE_ROUNDTRIP_TEST(RenameTableRequest)
INSTANTIATE_TEST_SUITE_P(
RenameTableRequestCases, RenameTableRequestTest,
- ::testing::Values(RenameTableRequestParam{
- .test_name = "FullRequest",
- .expected_json_str =
-
R"({"source":{"namespace":["accounting","tax"],"name":"paid"},"destination":{"namespace":["accounting","tax"],"name":"paid_2022"}})",
- .source = TableIdentifier{Namespace{{"accounting", "tax"}}, "paid"},
- .destination = TableIdentifier{Namespace{{"accounting", "tax"}},
"paid_2022"}}),
+ ::testing::Values(
+ // Full request with source and destination table identifiers
+ RenameTableRequestParam{
+ .test_name = "FullRequest",
+ .expected_json_str =
+
R"({"source":{"namespace":["accounting","tax"],"name":"paid"},"destination":{"namespace":["accounting","tax"],"name":"paid_2022"}})",
+ .model = {.source = TableIdentifier{Namespace{{"accounting",
"tax"}}, "paid"},
+ .destination = TableIdentifier{Namespace{{"accounting",
"tax"}},
+ "paid_2022"}}}),
[](const ::testing::TestParamInfo<RenameTableRequestParam>& info) {
return info.param.test_name;
});
-TEST(RenameTableRequestTest, InvalidRequests) {
- std::string json_source_null_name =
-
R"({"source":{"namespace":["accounting","tax"],"name":null},"destination":{"namespace":["accounting","tax"],"name":"paid_2022"}})";
- auto result1 =
RenameTableRequestFromJson(nlohmann::json::parse(json_source_null_name));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Missing 'name' in
{\"name\":null,\"namespace\":[\"accounting\",\"tax\"]}");
-
- std::string json_dest_null_name =
-
R"({"source":{"namespace":["accounting","tax"],"name":"paid"},"destination":{"namespace":["accounting","tax"],"name":null}})";
- auto result2 =
RenameTableRequestFromJson(nlohmann::json::parse(json_dest_null_name));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message,
- "Missing 'name' in
{\"name\":null,\"namespace\":[\"accounting\",\"tax\"]}");
-
- std::string json_empty = R"({})";
- auto result3 = RenameTableRequestFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result3.has_value());
- EXPECT_THAT(result3, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result3.error().message, "Missing 'source' in {}");
-}
-
-struct RegisterTableRequestParam {
- std::string test_name;
- std::string expected_json_str;
- std::string name;
- std::string metadata_location;
- bool overwrite;
-};
-
-class RegisterTableRequestTest
- : public ::testing::TestWithParam<RegisterTableRequestParam> {
- protected:
- void TestRoundTrip() {
- const auto& param = GetParam();
+DECLARE_INVALID_TEST(RenameTableRequest)
- RegisterTableRequest original;
- original.name = param.name;
- original.metadata_location = param.metadata_location;
- original.overwrite = param.overwrite;
-
- auto json = ToJson(original);
- auto expected_json = nlohmann::json::parse(param.expected_json_str);
- EXPECT_EQ(json, expected_json);
-
- auto result = RegisterTableRequestFromJson(expected_json);
- ASSERT_TRUE(result.has_value()) << result.error().message;
- auto& parsed = result.value();
-
- EXPECT_EQ(parsed, original);
- }
-};
+INSTANTIATE_TEST_SUITE_P(
+ RenameTableRequestInvalidCases, RenameTableRequestInvalidTest,
+ ::testing::Values(
+ // Source table name is null
+ RenameTableRequestInvalidParam{
+ .test_name = "SourceNameNull",
+ .invalid_json_str =
+
R"({"source":{"namespace":["accounting","tax"],"name":null},"destination":{"namespace":["accounting","tax"],"name":"paid_2022"}})",
+ .expected_error_message = "Missing 'name'"},
+ // Destination table name is null
+ RenameTableRequestInvalidParam{
+ .test_name = "DestinationNameNull",
+ .invalid_json_str =
+
R"({"source":{"namespace":["accounting","tax"],"name":"paid"},"destination":{"namespace":["accounting","tax"],"name":null}})",
+ .expected_error_message = "Missing 'name'"},
+ // Empty JSON object
+ RenameTableRequestInvalidParam{.test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing
'source'"}),
+ [](const ::testing::TestParamInfo<RenameTableRequestInvalidParam>& info) {
+ return info.param.test_name;
+ });
-TEST_P(RegisterTableRequestTest, RoundTrip) { TestRoundTrip(); }
+DECLARE_ROUNDTRIP_TEST(RegisterTableRequest)
INSTANTIATE_TEST_SUITE_P(
RegisterTableRequestCases, RegisterTableRequestTest,
::testing::Values(
+ // Request with overwrite set to true
RegisterTableRequestParam{
.test_name = "WithOverwriteTrue",
.expected_json_str =
R"({"name":"table1","metadata-location":"s3://bucket/metadata.json","overwrite":true})",
- .name = "table1",
- .metadata_location = "s3://bucket/metadata.json",
- .overwrite = true},
+ .model = {.name = "table1",
+ .metadata_location = "s3://bucket/metadata.json",
+ .overwrite = true}},
+ // Request without overwrite field (defaults to false, omitted in
serialization)
RegisterTableRequestParam{
.test_name = "WithoutOverwrite",
.expected_json_str =
R"({"name":"table1","metadata-location":"s3://bucket/metadata.json"})",
- .name = "table1",
- .metadata_location = "s3://bucket/metadata.json",
- .overwrite = false}),
+ .model = {.name = "table1",
+ .metadata_location = "s3://bucket/metadata.json"}}),
[](const ::testing::TestParamInfo<RegisterTableRequestParam>& info) {
return info.param.test_name;
});
-TEST(RegisterTableRequestTest, DeserializeWithoutDefaults) {
- // Overwrite missing (defaults to false)
- std::string json1 =
- R"({"name":"table1","metadata-location":"s3://bucket/metadata.json"})";
- auto result1 = RegisterTableRequestFromJson(nlohmann::json::parse(json1));
- ASSERT_TRUE(result1.has_value());
- EXPECT_FALSE(result1.value().overwrite);
-}
+DECLARE_DESERIALIZE_TEST(RegisterTableRequest)
-TEST(RegisterTableRequestTest, InvalidRequests) {
- std::string json_missing_name =
R"({"metadata-location":"s3://bucket/metadata.json"})";
- auto result1 =
RegisterTableRequestFromJson(nlohmann::json::parse(json_missing_name));
- EXPECT_FALSE(result1.has_value());
- EXPECT_THAT(result1, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result1.error().message,
- "Missing 'name' in
{\"metadata-location\":\"s3://bucket/metadata.json\"}");
-
- std::string json_missing_location = R"({"name":"table1"})";
- auto result2 =
-
RegisterTableRequestFromJson(nlohmann::json::parse(json_missing_location));
- EXPECT_FALSE(result2.has_value());
- EXPECT_THAT(result2, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result2.error().message,
- "Missing 'metadata-location' in {\"name\":\"table1\"}");
-
- std::string json_empty = R"({})";
- auto result3 =
RegisterTableRequestFromJson(nlohmann::json::parse(json_empty));
- EXPECT_FALSE(result3.has_value());
- EXPECT_THAT(result3, IsError(ErrorKind::kJsonParseError));
- EXPECT_EQ(result3.error().message, "Missing 'name' in {}");
-}
+INSTANTIATE_TEST_SUITE_P(
+ RegisterTableRequestDeserializeCases, RegisterTableRequestDeserializeTest,
+ ::testing::Values(
+ // Overwrite missing (should default to false)
+ RegisterTableRequestDeserializeParam{
+ .test_name = "MissingOverwrite",
+ .json_str =
+
R"({"name":"table1","metadata-location":"s3://bucket/metadata.json"})",
+ .expected_model = {.name = "table1",
+ .metadata_location =
"s3://bucket/metadata.json",
+ .overwrite = false}}),
+ [](const ::testing::TestParamInfo<RegisterTableRequestDeserializeParam>&
info) {
+ return info.param.test_name;
+ });
+
+DECLARE_INVALID_TEST(RegisterTableRequest)
+
+INSTANTIATE_TEST_SUITE_P(
+ RegisterTableRequestInvalidCases, RegisterTableRequestInvalidTest,
+ ::testing::Values(
+ // Missing required name field
+ RegisterTableRequestInvalidParam{
+ .test_name = "MissingName",
+ .invalid_json_str =
R"({"metadata-location":"s3://bucket/metadata.json"})",
+ .expected_error_message = "Missing 'name' in"},
+ // Missing required metadata-location field
+ RegisterTableRequestInvalidParam{
+ .test_name = "MissingMetadataLocation",
+ .invalid_json_str = R"({"name":"table1"})",
+ .expected_error_message = "Missing 'metadata-location'"},
+ // Empty JSON object
+ RegisterTableRequestInvalidParam{.test_name = "EmptyJson",
+ .invalid_json_str = R"({})",
+ .expected_error_message = "Missing
'name'"}),
+ [](const ::testing::TestParamInfo<RegisterTableRequestInvalidParam>& info)
{
+ return info.param.test_name;
+ });
} // namespace iceberg::rest
diff --git a/src/iceberg/util/json_util_internal.h
b/src/iceberg/util/json_util_internal.h
index 6205ad1..65764c4 100644
--- a/src/iceberg/util/json_util_internal.h
+++ b/src/iceberg/util/json_util_internal.h
@@ -39,6 +39,21 @@ void SetOptionalField(nlohmann::json& json, std::string_view
key,
}
}
+template <typename T>
+ requires requires(const T& t) { t.empty(); }
+void SetContainerField(nlohmann::json& json, std::string_view key, const T&
value) {
+ if (!value.empty()) {
+ json[key] = value;
+ }
+}
+
+inline void SetOptionalStringField(nlohmann::json& json, std::string_view key,
+ const std::string& value) {
+ if (!value.empty()) {
+ json[key] = value;
+ }
+}
+
inline std::string SafeDumpJson(const nlohmann::json& json) {
return json.dump(/*indent=*/-1, /*indent_char=*/' ', /*ensure_ascii=*/false,
nlohmann::detail::error_handler_t::ignore);