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 f93f54da feat: implement REST catalog namespace operations (#404)
f93f54da is described below
commit f93f54da326581808451e377085d39bca11ca0e4
Author: Feiyang Li <[email protected]>
AuthorDate: Thu Dec 11 17:28:45 2025 +0800
feat: implement REST catalog namespace operations (#404)
---
src/iceberg/catalog/rest/http_client.cc | 36 +++++--
src/iceberg/catalog/rest/json_internal.cc | 3 +
src/iceberg/catalog/rest/rest_catalog.cc | 74 +++++++++++---
src/iceberg/catalog/rest/rest_util.cc | 131 ++++++++++++++++++++++++
src/iceberg/catalog/rest/rest_util.h | 9 ++
src/iceberg/catalog/rest/types.h | 4 +-
src/iceberg/test/rest_catalog_test.cc | 151 ++++++++++++++++++++++++++++
src/iceberg/test/rest_json_internal_test.cc | 20 ++--
8 files changed, 394 insertions(+), 34 deletions(-)
diff --git a/src/iceberg/catalog/rest/http_client.cc
b/src/iceberg/catalog/rest/http_client.cc
index 3e70b9d9..d1138b78 100644
--- a/src/iceberg/catalog/rest/http_client.cc
+++ b/src/iceberg/catalog/rest/http_client.cc
@@ -25,6 +25,7 @@
#include "iceberg/catalog/rest/constant.h"
#include "iceberg/catalog/rest/error_handlers.h"
#include "iceberg/catalog/rest/json_internal.h"
+#include "iceberg/catalog/rest/rest_util.h"
#include "iceberg/json_internal.h"
#include "iceberg/result.h"
#include "iceberg/util/macros.h"
@@ -63,6 +64,9 @@ std::unordered_map<std::string, std::string>
HttpResponse::headers() const {
namespace {
+/// \brief Default error type for unparseable REST responses.
+constexpr std::string_view kRestExceptionType = "RESTException";
+
/// \brief Merges global default headers with request-specific headers.
///
/// Combines the global headers derived from RestCatalogProperties with the
headers
@@ -96,16 +100,36 @@ bool IsSuccessful(int32_t status_code) {
|| status_code == 304; // Not Modified
}
+/// \brief Builds a default ErrorResponse when the response body cannot be
parsed.
+ErrorResponse BuildDefaultErrorResponse(const cpr::Response& response) {
+ return {
+ .code = static_cast<uint32_t>(response.status_code),
+ .type = std::string(kRestExceptionType),
+ .message = !response.reason.empty() ? response.reason
+ :
GetStandardReasonPhrase(response.status_code),
+ };
+}
+
+/// \brief Tries to parse the response body as an ErrorResponse.
+Result<ErrorResponse> TryParseErrorResponse(const std::string& text) {
+ if (text.empty()) {
+ return InvalidArgument("Empty response body");
+ }
+ ICEBERG_ASSIGN_OR_RAISE(auto json_result, FromJsonString(text));
+ ICEBERG_ASSIGN_OR_RAISE(auto error_result,
ErrorResponseFromJson(json_result));
+ return error_result;
+}
+
/// \brief Handles failure responses by invoking the provided error handler.
Status HandleFailureResponse(const cpr::Response& response,
const ErrorHandler& error_handler) {
- if (!IsSuccessful(response.status_code)) {
- // TODO(gangwu): response status code is lost, wrap it with RestError.
- ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.text));
- ICEBERG_ASSIGN_OR_RAISE(auto error_response, ErrorResponseFromJson(json));
- return error_handler.Accept(error_response);
+ if (IsSuccessful(response.status_code)) {
+ return {};
}
- return {};
+ auto parse_result = TryParseErrorResponse(response.text);
+ const ErrorResponse final_error =
+ parse_result.value_or(BuildDefaultErrorResponse(response));
+ return error_handler.Accept(final_error);
}
} // namespace
diff --git a/src/iceberg/catalog/rest/json_internal.cc
b/src/iceberg/catalog/rest/json_internal.cc
index 61fce938..c60b406d 100644
--- a/src/iceberg/catalog/rest/json_internal.cc
+++ b/src/iceberg/catalog/rest/json_internal.cc
@@ -213,6 +213,7 @@ Result<LoadTableResult> LoadTableResultFromJson(const
nlohmann::json& json) {
ICEBERG_ASSIGN_OR_RAISE(result.metadata,
TableMetadataFromJson(metadata_json));
ICEBERG_ASSIGN_OR_RAISE(result.config,
GetJsonValueOrDefault<decltype(result.config)>(json,
kConfig));
+ ICEBERG_RETURN_UNEXPECTED(result.Validate());
return result;
}
@@ -257,6 +258,7 @@ Result<CreateNamespaceResponse>
CreateNamespaceResponseFromJson(
ICEBERG_ASSIGN_OR_RAISE(
response.properties,
GetJsonValueOrDefault<decltype(response.properties)>(json, kProperties));
+ ICEBERG_RETURN_UNEXPECTED(response.Validate());
return response;
}
@@ -274,6 +276,7 @@ Result<GetNamespaceResponse>
GetNamespaceResponseFromJson(const nlohmann::json&
ICEBERG_ASSIGN_OR_RAISE(
response.properties,
GetJsonValueOrDefault<decltype(response.properties)>(json, kProperties));
+ ICEBERG_RETURN_UNEXPECTED(response.Validate());
return response;
}
diff --git a/src/iceberg/catalog/rest/rest_catalog.cc
b/src/iceberg/catalog/rest/rest_catalog.cc
index e4553ace..4a77f658 100644
--- a/src/iceberg/catalog/rest/rest_catalog.cc
+++ b/src/iceberg/catalog/rest/rest_catalog.cc
@@ -34,7 +34,9 @@
#include "iceberg/catalog/rest/rest_catalog.h"
#include "iceberg/catalog/rest/rest_util.h"
#include "iceberg/json_internal.h"
+#include "iceberg/partition_spec.h"
#include "iceberg/result.h"
+#include "iceberg/schema.h"
#include "iceberg/table.h"
#include "iceberg/util/macros.h"
@@ -99,7 +101,7 @@ Result<std::vector<Namespace>>
RestCatalog::ListNamespaces(const Namespace& ns)
if (!next_token.empty()) {
params[kQueryParamPageToken] = next_token;
}
- ICEBERG_ASSIGN_OR_RAISE(const auto& response,
+ ICEBERG_ASSIGN_OR_RAISE(const auto response,
client_->Get(endpoint, params, /*headers=*/{},
*NamespaceErrorHandler::Instance()));
ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
@@ -115,29 +117,69 @@ Result<std::vector<Namespace>>
RestCatalog::ListNamespaces(const Namespace& ns)
}
Status RestCatalog::CreateNamespace(
- [[maybe_unused]] const Namespace& ns,
- [[maybe_unused]] const std::unordered_map<std::string, std::string>&
properties) {
- return NotImplemented("Not implemented");
+ const Namespace& ns, const std::unordered_map<std::string, std::string>&
properties) {
+ ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespaces());
+ CreateNamespaceRequest request{.namespace_ = ns, .properties = properties};
+ ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
+ ICEBERG_ASSIGN_OR_RAISE(const auto response,
+ client_->Post(endpoint, json_request, /*headers=*/{},
+ *NamespaceErrorHandler::Instance()));
+ ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
+ ICEBERG_ASSIGN_OR_RAISE(auto create_response,
CreateNamespaceResponseFromJson(json));
+ return {};
}
Result<std::unordered_map<std::string, std::string>>
RestCatalog::GetNamespaceProperties(
- [[maybe_unused]] const Namespace& ns) const {
- return NotImplemented("Not implemented");
-}
-
-Status RestCatalog::DropNamespace([[maybe_unused]] const Namespace& ns) {
- return NotImplemented("Not implemented");
+ const Namespace& ns) const {
+ ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
+ ICEBERG_ASSIGN_OR_RAISE(const auto response,
+ client_->Get(endpoint, /*params=*/{}, /*headers=*/{},
+ *NamespaceErrorHandler::Instance()));
+ ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
+ ICEBERG_ASSIGN_OR_RAISE(auto get_response,
GetNamespaceResponseFromJson(json));
+ return get_response.properties;
}
-Result<bool> RestCatalog::NamespaceExists([[maybe_unused]] const Namespace&
ns) const {
- return NotImplemented("Not implemented");
+Status RestCatalog::DropNamespace(const Namespace& ns) {
+ ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
+ ICEBERG_ASSIGN_OR_RAISE(
+ const auto response,
+ client_->Delete(endpoint, /*headers=*/{},
*DropNamespaceErrorHandler::Instance()));
+ return {};
+}
+
+Result<bool> RestCatalog::NamespaceExists(const Namespace& ns) const {
+ ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->Namespace_(ns));
+ // TODO(Feiyang Li): checks if the server supports the namespace exists
endpoint, if
+ // not, triggers a fallback mechanism
+ auto response_or_error =
+ client_->Head(endpoint, /*headers=*/{},
*NamespaceErrorHandler::Instance());
+ if (!response_or_error.has_value()) {
+ const auto& error = response_or_error.error();
+ // catch NoSuchNamespaceException/404 and return false
+ if (error.kind == ErrorKind::kNoSuchNamespace) {
+ return false;
+ }
+ ICEBERG_RETURN_UNEXPECTED(response_or_error);
+ }
+ return true;
}
Status RestCatalog::UpdateNamespaceProperties(
- [[maybe_unused]] const Namespace& ns,
- [[maybe_unused]] const std::unordered_map<std::string, std::string>&
updates,
- [[maybe_unused]] const std::unordered_set<std::string>& removals) {
- return NotImplemented("Not implemented");
+ const Namespace& ns, const std::unordered_map<std::string, std::string>&
updates,
+ const std::unordered_set<std::string>& removals) {
+ ICEBERG_ASSIGN_OR_RAISE(auto endpoint, paths_->NamespaceProperties(ns));
+ UpdateNamespacePropertiesRequest request{
+ .removals = std::vector<std::string>(removals.begin(), removals.end()),
+ .updates = updates};
+ ICEBERG_ASSIGN_OR_RAISE(auto json_request, ToJsonString(ToJson(request)));
+ ICEBERG_ASSIGN_OR_RAISE(const auto response,
+ client_->Post(endpoint, json_request, /*headers=*/{},
+ *NamespaceErrorHandler::Instance()));
+ ICEBERG_ASSIGN_OR_RAISE(auto json, FromJsonString(response.body()));
+ ICEBERG_ASSIGN_OR_RAISE(auto update_response,
+ UpdateNamespacePropertiesResponseFromJson(json));
+ return {};
}
Result<std::vector<TableIdentifier>> RestCatalog::ListTables(
diff --git a/src/iceberg/catalog/rest/rest_util.cc
b/src/iceberg/catalog/rest/rest_util.cc
index 5a0f166d..a1a63fa1 100644
--- a/src/iceberg/catalog/rest/rest_util.cc
+++ b/src/iceberg/catalog/rest/rest_util.cc
@@ -19,6 +19,8 @@
#include "iceberg/catalog/rest/rest_util.h"
+#include <format>
+
#include <cpr/util.h>
#include "iceberg/table_identifier.h"
@@ -120,4 +122,133 @@ std::unordered_map<std::string, std::string> MergeConfigs(
return merged;
}
+std::string GetStandardReasonPhrase(int32_t status_code) {
+ switch (status_code) {
+ case 100:
+ return "Continue";
+ case 101:
+ return "Switching Protocols";
+ case 102:
+ return "Processing";
+ case 103:
+ return "Early Hints";
+ case 200:
+ return "OK";
+ case 201:
+ return "Created";
+ case 202:
+ return "Accepted";
+ case 203:
+ return "Non Authoritative Information";
+ case 204:
+ return "No Content";
+ case 205:
+ return "Reset Content";
+ case 206:
+ return "Partial Content";
+ case 207:
+ return "Multi-Status";
+ case 208:
+ return "Already Reported";
+ case 226:
+ return "IM Used";
+ case 300:
+ return "Multiple Choices";
+ case 301:
+ return "Moved Permanently";
+ case 302:
+ return "Moved Temporarily";
+ case 303:
+ return "See Other";
+ case 304:
+ return "Not Modified";
+ case 305:
+ return "Use Proxy";
+ case 307:
+ return "Temporary Redirect";
+ case 308:
+ return "Permanent Redirect";
+ case 400:
+ return "Bad Request";
+ case 401:
+ return "Unauthorized";
+ case 402:
+ return "Payment Required";
+ case 403:
+ return "Forbidden";
+ case 404:
+ return "Not Found";
+ case 405:
+ return "Method Not Allowed";
+ case 406:
+ return "Not Acceptable";
+ case 407:
+ return "Proxy Authentication Required";
+ case 408:
+ return "Request Timeout";
+ case 409:
+ return "Conflict";
+ case 410:
+ return "Gone";
+ case 411:
+ return "Length Required";
+ case 412:
+ return "Precondition Failed";
+ case 413:
+ return "Request Too Long";
+ case 414:
+ return "Request-URI Too Long";
+ case 415:
+ return "Unsupported Media Type";
+ case 416:
+ return "Requested Range Not Satisfiable";
+ case 417:
+ return "Expectation Failed";
+ case 421:
+ return "Misdirected Request";
+ case 422:
+ return "Unprocessable Content";
+ case 423:
+ return "Locked";
+ case 424:
+ return "Failed Dependency";
+ case 425:
+ return "Too Early";
+ case 426:
+ return "Upgrade Required";
+ case 428:
+ return "Precondition Required";
+ case 429:
+ return "Too Many Requests";
+ case 431:
+ return "Request Header Fields Too Large";
+ case 451:
+ return "Unavailable For Legal Reasons";
+ case 500:
+ return "Internal Server Error";
+ case 501:
+ return "Not Implemented";
+ case 502:
+ return "Bad Gateway";
+ case 503:
+ return "Service Unavailable";
+ case 504:
+ return "Gateway Timeout";
+ case 505:
+ return "Http Version Not Supported";
+ case 506:
+ return "Variant Also Negotiates";
+ case 507:
+ return "Insufficient Storage";
+ case 508:
+ return "Loop Detected";
+ case 510:
+ return "Not Extended";
+ case 511:
+ return "Network Authentication Required";
+ default:
+ return std::format("HTTP {}", status_code);
+ }
+}
+
} // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/rest_util.h
b/src/iceberg/catalog/rest/rest_util.h
index 895bb2fb..fde67a84 100644
--- a/src/iceberg/catalog/rest/rest_util.h
+++ b/src/iceberg/catalog/rest/rest_util.h
@@ -81,4 +81,13 @@ ICEBERG_REST_EXPORT std::unordered_map<std::string,
std::string> MergeConfigs(
const std::unordered_map<std::string, std::string>& client_configs,
const std::unordered_map<std::string, std::string>& server_overrides);
+/// \brief Get the standard HTTP reason phrase for a status code.
+///
+/// \details Returns the standard English reason phrase for common HTTP status
codes.
+/// For unknown status codes, returns a generic "HTTP {code}" message.
+/// \param status_code The HTTP status code (e.g., 200, 404, 500).
+/// \return The standard reason phrase string (e.g., "OK", "Not Found",
"Internal Server
+/// Error").
+ICEBERG_REST_EXPORT std::string GetStandardReasonPhrase(int32_t status_code);
+
} // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/types.h b/src/iceberg/catalog/rest/types.h
index dbc772ec..7760e178 100644
--- a/src/iceberg/catalog/rest/types.h
+++ b/src/iceberg/catalog/rest/types.h
@@ -53,9 +53,9 @@ struct ICEBERG_REST_EXPORT CatalogConfig {
/// \brief JSON error payload returned in a response with further details on
the error.
struct ICEBERG_REST_EXPORT ErrorResponse {
- std::string message; // required
- std::string type; // required
uint32_t code; // required
+ std::string type; // required
+ std::string message; // required
std::vector<std::string> stack;
/// \brief Validates the ErrorResponse.
diff --git a/src/iceberg/test/rest_catalog_test.cc
b/src/iceberg/test/rest_catalog_test.cc
index f91782a0..49c527f6 100644
--- a/src/iceberg/test/rest_catalog_test.cc
+++ b/src/iceberg/test/rest_catalog_test.cc
@@ -148,4 +148,155 @@ TEST_F(RestCatalogIntegrationTest, ListNamespaces) {
EXPECT_TRUE(result->empty());
}
+TEST_F(RestCatalogIntegrationTest, CreateNamespace) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create a simple namespace
+ Namespace ns{.levels = {"test_ns"}};
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Verify it was created by listing
+ Namespace root{.levels = {}};
+ auto list_result = catalog->ListNamespaces(root);
+ ASSERT_THAT(list_result, IsOk());
+ EXPECT_EQ(list_result->size(), 1);
+ EXPECT_EQ(list_result->at(0).levels, std::vector<std::string>{"test_ns"});
+}
+
+TEST_F(RestCatalogIntegrationTest, CreateNamespaceWithProperties) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace with properties
+ Namespace ns{.levels = {"test_ns_props"}};
+ std::unordered_map<std::string, std::string> properties{
+ {"owner", "test_user"}, {"description", "Test namespace with
properties"}};
+ auto status = catalog->CreateNamespace(ns, properties);
+ EXPECT_THAT(status, IsOk());
+
+ // Verify properties were set
+ auto props_result = catalog->GetNamespaceProperties(ns);
+ ASSERT_THAT(props_result, IsOk());
+ EXPECT_EQ(props_result->at("owner"), "test_user");
+ EXPECT_EQ(props_result->at("description"), "Test namespace with properties");
+}
+
+TEST_F(RestCatalogIntegrationTest, CreateNestedNamespace) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create parent namespace
+ Namespace parent{.levels = {"parent"}};
+ auto status = catalog->CreateNamespace(parent, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Create nested namespace
+ Namespace child{.levels = {"parent", "child"}};
+ status = catalog->CreateNamespace(child, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Verify nested namespace exists
+ auto list_result = catalog->ListNamespaces(parent);
+ ASSERT_THAT(list_result, IsOk());
+ EXPECT_EQ(list_result->size(), 1);
+ EXPECT_EQ(list_result->at(0).levels, (std::vector<std::string>{"parent",
"child"}));
+}
+
+TEST_F(RestCatalogIntegrationTest, GetNamespaceProperties) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace with properties
+ Namespace ns{.levels = {"test_get_props"}};
+ std::unordered_map<std::string, std::string> properties{{"key1", "value1"},
+ {"key2", "value2"}};
+ auto status = catalog->CreateNamespace(ns, properties);
+ EXPECT_THAT(status, IsOk());
+
+ // Get properties
+ auto props_result = catalog->GetNamespaceProperties(ns);
+ ASSERT_THAT(props_result, IsOk());
+ EXPECT_EQ(props_result->at("key1"), "value1");
+ EXPECT_EQ(props_result->at("key2"), "value2");
+}
+
+TEST_F(RestCatalogIntegrationTest, NamespaceExists) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Check non-existent namespace
+ Namespace ns{.levels = {"non_existent"}};
+ auto exists_result = catalog->NamespaceExists(ns);
+ ASSERT_THAT(exists_result, IsOk());
+ EXPECT_FALSE(*exists_result);
+
+ // Create namespace
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Check it now exists
+ exists_result = catalog->NamespaceExists(ns);
+ ASSERT_THAT(exists_result, IsOk());
+ EXPECT_TRUE(*exists_result);
+}
+
+TEST_F(RestCatalogIntegrationTest, UpdateNamespaceProperties) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace with initial properties
+ Namespace ns{.levels = {"test_update"}};
+ std::unordered_map<std::string, std::string> initial_props{{"key1",
"value1"},
+ {"key2",
"value2"}};
+ auto status = catalog->CreateNamespace(ns, initial_props);
+ EXPECT_THAT(status, IsOk());
+
+ // Update properties: modify key1, add key3, remove key2
+ std::unordered_map<std::string, std::string> updates{{"key1",
"updated_value1"},
+ {"key3", "value3"}};
+ std::unordered_set<std::string> removals{"key2"};
+ status = catalog->UpdateNamespaceProperties(ns, updates, removals);
+ EXPECT_THAT(status, IsOk());
+
+ // Verify updated properties
+ auto props_result = catalog->GetNamespaceProperties(ns);
+ ASSERT_THAT(props_result, IsOk());
+ EXPECT_EQ(props_result->at("key1"), "updated_value1");
+ EXPECT_EQ(props_result->at("key3"), "value3");
+ EXPECT_EQ(props_result->count("key2"), 0); // Should be removed
+}
+
+TEST_F(RestCatalogIntegrationTest, DropNamespace) {
+ auto catalog_result = CreateCatalog();
+ ASSERT_THAT(catalog_result, IsOk());
+ auto& catalog = catalog_result.value();
+
+ // Create namespace
+ Namespace ns{.levels = {"test_drop"}};
+ auto status = catalog->CreateNamespace(ns, {});
+ EXPECT_THAT(status, IsOk());
+
+ // Verify it exists
+ auto exists_result = catalog->NamespaceExists(ns);
+ ASSERT_THAT(exists_result, IsOk());
+ EXPECT_TRUE(*exists_result);
+
+ // Drop namespace
+ status = catalog->DropNamespace(ns);
+ EXPECT_THAT(status, IsOk());
+
+ // Verify it no longer exists
+ exists_result = catalog->NamespaceExists(ns);
+ ASSERT_THAT(exists_result, IsOk());
+ EXPECT_FALSE(*exists_result);
+}
+
} // namespace iceberg::rest
diff --git a/src/iceberg/test/rest_json_internal_test.cc
b/src/iceberg/test/rest_json_internal_test.cc
index 2cae201f..ca2671fa 100644
--- a/src/iceberg/test/rest_json_internal_test.cc
+++ b/src/iceberg/test/rest_json_internal_test.cc
@@ -870,26 +870,26 @@ INSTANTIATE_TEST_SUITE_P(
.test_name = "WithoutStack",
.expected_json_str =
R"({"error":{"message":"The given namespace does not
exist","type":"NoSuchNamespaceException","code":404}})",
- .model = {.message = "The given namespace does not exist",
+ .model = {.code = 404,
.type = "NoSuchNamespaceException",
- .code = 404}},
+ .message = "The given namespace does not exist"}},
// Error with stack trace
ErrorResponseParam{
.test_name = "WithStack",
.expected_json_str =
R"({"error":{"message":"The given namespace does not
exist","type":"NoSuchNamespaceException","code":404,"stack":["a","b"]}})",
- .model = {.message = "The given namespace does not exist",
+ .model = {.code = 404,
.type = "NoSuchNamespaceException",
- .code = 404,
+ .message = "The given namespace does not exist",
.stack = {"a", "b"}}},
// Different error type
ErrorResponseParam{
.test_name = "DifferentError",
.expected_json_str =
R"({"error":{"message":"Internal server
error","type":"InternalServerError","code":500,"stack":["line1","line2","line3"]}})",
- .model = {.message = "Internal server error",
+ .model = {.code = 500,
.type = "InternalServerError",
- .code = 500,
+ .message = "Internal server error",
.stack = {"line1", "line2", "line3"}}}),
[](const ::testing::TestParamInfo<ErrorResponseParam>& info) {
return info.param.test_name;
@@ -905,17 +905,17 @@ INSTANTIATE_TEST_SUITE_P(
.test_name = "NullStack",
.json_str =
R"({"error":{"message":"The given namespace does not
exist","type":"NoSuchNamespaceException","code":404,"stack":null}})",
- .expected_model = {.message = "The given namespace does not exist",
+ .expected_model = {.code = 404,
.type = "NoSuchNamespaceException",
- .code = 404}},
+ .message = "The given namespace does not
exist"}},
// Stack field is missing (should deserialize to empty vector)
ErrorResponseDeserializeParam{
.test_name = "MissingStack",
.json_str =
R"({"error":{"message":"The given namespace does not
exist","type":"NoSuchNamespaceException","code":404}})",
- .expected_model = {.message = "The given namespace does not exist",
+ .expected_model = {.code = 404,
.type = "NoSuchNamespaceException",
- .code = 404}}),
+ .message = "The given namespace does not
exist"}}),
[](const ::testing::TestParamInfo<ErrorResponseDeserializeParam>& info) {
return info.param.test_name;
});