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 ad5dadc  feat: add config, error and validation for rest types (#292)
ad5dadc is described below

commit ad5dadcf50e793cf538472a2e9dffd53cadf2076
Author: Li Feiyang <[email protected]>
AuthorDate: Thu Nov 6 11:17:34 2025 +0800

    feat: add config, error and validation for rest types (#292)
---
 meson.build                                 |   4 +
 src/iceberg/catalog/rest/CMakeLists.txt     |   2 +-
 src/iceberg/catalog/rest/json_internal.cc   |  79 +++++++++-
 src/iceberg/catalog/rest/json_internal.h    |   6 +
 src/iceberg/catalog/rest/meson.build        |  11 +-
 src/iceberg/catalog/rest/types.h            |  21 ++-
 src/iceberg/catalog/rest/validator.cc       | 142 ++++++++++++++++++
 src/iceberg/catalog/rest/validator.h        |  87 +++++++++++
 src/iceberg/test/rest_json_internal_test.cc | 223 +++++++++++++++++++++++++++-
 9 files changed, 569 insertions(+), 6 deletions(-)

diff --git a/meson.build b/meson.build
index eaeeda4..68dcaa0 100644
--- a/meson.build
+++ b/meson.build
@@ -30,6 +30,10 @@ project(
     ],
 )
 
+cpp = meson.get_compiler('cpp')
+args = cpp.get_supported_arguments(['/bigobj'])
+add_project_arguments(args, language: 'cpp')
+
 subdir('src')
 
 install_data(
diff --git a/src/iceberg/catalog/rest/CMakeLists.txt 
b/src/iceberg/catalog/rest/CMakeLists.txt
index 38d8972..0244051 100644
--- a/src/iceberg/catalog/rest/CMakeLists.txt
+++ b/src/iceberg/catalog/rest/CMakeLists.txt
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-set(ICEBERG_REST_SOURCES rest_catalog.cc json_internal.cc)
+set(ICEBERG_REST_SOURCES rest_catalog.cc json_internal.cc validator.cc)
 
 set(ICEBERG_REST_STATIC_BUILD_INTERFACE_LIBS)
 set(ICEBERG_REST_SHARED_BUILD_INTERFACE_LIBS)
diff --git a/src/iceberg/catalog/rest/json_internal.cc 
b/src/iceberg/catalog/rest/json_internal.cc
index 452de7a..404fe21 100644
--- a/src/iceberg/catalog/rest/json_internal.cc
+++ b/src/iceberg/catalog/rest/json_internal.cc
@@ -20,13 +20,13 @@
 #include "iceberg/catalog/rest/json_internal.h"
 
 #include <string>
-#include <unordered_map>
 #include <utility>
 #include <vector>
 
 #include <nlohmann/json.hpp>
 
 #include "iceberg/catalog/rest/types.h"
+#include "iceberg/catalog/rest/validator.h"
 #include "iceberg/json_internal.h"
 #include "iceberg/table_identifier.h"
 #include "iceberg/util/json_util_internal.h"
@@ -59,9 +59,76 @@ constexpr std::string_view kDestination = "destination";
 constexpr std::string_view kMetadata = "metadata";
 constexpr std::string_view kConfig = "config";
 constexpr std::string_view kIdentifiers = "identifiers";
+constexpr std::string_view kOverrides = "overrides";
+constexpr std::string_view kDefaults = "defaults";
+constexpr std::string_view kEndpoints = "endpoints";
+constexpr std::string_view kMessage = "message";
+constexpr std::string_view kType = "type";
+constexpr std::string_view kCode = "code";
+constexpr std::string_view kStack = "stack";
+constexpr std::string_view kError = "error";
 
 }  // namespace
 
+nlohmann::json ToJson(const CatalogConfig& config) {
+  nlohmann::json json;
+  json[kOverrides] = config.overrides;
+  json[kDefaults] = config.defaults;
+  SetContainerField(json, kEndpoints, config.endpoints);
+  return json;
+}
+
+Result<CatalogConfig> CatalogConfigFromJson(const nlohmann::json& json) {
+  CatalogConfig config;
+  ICEBERG_ASSIGN_OR_RAISE(
+      config.overrides,
+      GetJsonValueOrDefault<decltype(config.overrides)>(json, kOverrides));
+  ICEBERG_ASSIGN_OR_RAISE(
+      config.defaults, GetJsonValueOrDefault<decltype(config.defaults)>(json, 
kDefaults));
+  ICEBERG_ASSIGN_OR_RAISE(
+      config.endpoints,
+      GetJsonValueOrDefault<std::vector<std::string>>(json, kEndpoints));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(config));
+  return config;
+}
+
+nlohmann::json ToJson(const ErrorModel& error) {
+  nlohmann::json json;
+  json[kMessage] = error.message;
+  json[kType] = error.type;
+  json[kCode] = error.code;
+  SetContainerField(json, kStack, error.stack);
+  return json;
+}
+
+Result<ErrorModel> ErrorModelFromJson(const nlohmann::json& json) {
+  ErrorModel error;
+  // NOTE: Iceberg's Java implementation allows missing required fields 
(message, type,
+  // code) during deserialization, which deviates from the REST spec. We 
enforce strict
+  // validation here.
+  ICEBERG_ASSIGN_OR_RAISE(error.message, GetJsonValue<std::string>(json, 
kMessage));
+  ICEBERG_ASSIGN_OR_RAISE(error.type, GetJsonValue<std::string>(json, kType));
+  ICEBERG_ASSIGN_OR_RAISE(error.code, GetJsonValue<uint32_t>(json, kCode));
+  ICEBERG_ASSIGN_OR_RAISE(error.stack,
+                          
GetJsonValueOrDefault<std::vector<std::string>>(json, kStack));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(error));
+  return error;
+}
+
+nlohmann::json ToJson(const ErrorResponse& response) {
+  nlohmann::json json;
+  json[kError] = ToJson(response.error);
+  return json;
+}
+
+Result<ErrorResponse> ErrorResponseFromJson(const nlohmann::json& json) {
+  ErrorResponse response;
+  ICEBERG_ASSIGN_OR_RAISE(auto error_json, GetJsonValue<nlohmann::json>(json, 
kError));
+  ICEBERG_ASSIGN_OR_RAISE(response.error, ErrorModelFromJson(error_json));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
+  return response;
+}
+
 nlohmann::json ToJson(const CreateNamespaceRequest& request) {
   nlohmann::json json;
   json[kNamespace] = request.namespace_.levels;
@@ -77,6 +144,7 @@ Result<CreateNamespaceRequest> 
CreateNamespaceRequestFromJson(
   ICEBERG_ASSIGN_OR_RAISE(
       request.properties,
       GetJsonValueOrDefault<decltype(request.properties)>(json, kProperties));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
   return request;
 }
 
@@ -94,6 +162,7 @@ Result<UpdateNamespacePropertiesRequest> 
UpdateNamespacePropertiesRequestFromJso
       request.removals, GetJsonValueOrDefault<std::vector<std::string>>(json, 
kRemovals));
   ICEBERG_ASSIGN_OR_RAISE(
       request.updates, GetJsonValueOrDefault<decltype(request.updates)>(json, 
kUpdates));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
   return request;
 }
 
@@ -114,6 +183,7 @@ Result<RegisterTableRequest> 
RegisterTableRequestFromJson(const nlohmann::json&
                           GetJsonValue<std::string>(json, kMetadataLocation));
   ICEBERG_ASSIGN_OR_RAISE(request.overwrite,
                           GetJsonValueOrDefault<bool>(json, kOverwrite, 
false));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
   return request;
 }
 
@@ -131,6 +201,7 @@ Result<RenameTableRequest> RenameTableRequestFromJson(const 
nlohmann::json& json
   ICEBERG_ASSIGN_OR_RAISE(auto dest_json,
                           GetJsonValue<nlohmann::json>(json, kDestination));
   ICEBERG_ASSIGN_OR_RAISE(request.destination, 
TableIdentifierFromJson(dest_json));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
   return request;
 }
 
@@ -177,6 +248,7 @@ Result<ListNamespacesResponse> 
ListNamespacesResponseFromJson(
     ICEBERG_ASSIGN_OR_RAISE(auto ns, NamespaceFromJson(ns_json));
     response.namespaces.push_back(std::move(ns));
   }
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
   return response;
 }
 
@@ -232,6 +304,7 @@ Result<UpdateNamespacePropertiesResponse> 
UpdateNamespacePropertiesResponseFromJ
       response.removed, GetJsonValueOrDefault<std::vector<std::string>>(json, 
kRemoved));
   ICEBERG_ASSIGN_OR_RAISE(
       response.missing, GetJsonValueOrDefault<std::vector<std::string>>(json, 
kMissing));
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
   return response;
 }
 
@@ -256,6 +329,7 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const 
nlohmann::json& json
     ICEBERG_ASSIGN_OR_RAISE(auto identifier, TableIdentifierFromJson(id_json));
     response.identifiers.push_back(std::move(identifier));
   }
+  ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
   return response;
 }
 
@@ -265,6 +339,9 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const 
nlohmann::json& json
     return Model##FromJson(json);                             \
   }
 
+ICEBERG_DEFINE_FROM_JSON(CatalogConfig)
+ICEBERG_DEFINE_FROM_JSON(ErrorModel)
+ICEBERG_DEFINE_FROM_JSON(ErrorResponse)
 ICEBERG_DEFINE_FROM_JSON(ListNamespacesResponse)
 ICEBERG_DEFINE_FROM_JSON(CreateNamespaceRequest)
 ICEBERG_DEFINE_FROM_JSON(CreateNamespaceResponse)
diff --git a/src/iceberg/catalog/rest/json_internal.h 
b/src/iceberg/catalog/rest/json_internal.h
index 129e883..986066f 100644
--- a/src/iceberg/catalog/rest/json_internal.h
+++ b/src/iceberg/catalog/rest/json_internal.h
@@ -25,6 +25,9 @@
 #include "iceberg/catalog/rest/types.h"
 #include "iceberg/result.h"
 
+/// \file iceberg/catalog/rest/json_internal.h
+/// JSON serialization and deserialization for Iceberg REST Catalog API types.
+
 namespace iceberg::rest {
 
 template <typename Model>
@@ -40,6 +43,9 @@ Result<Model> FromJson(const nlohmann::json& json);
 
 /// \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(CatalogConfig)
+ICEBERG_DECLARE_JSON_SERDE(ErrorModel)
+ICEBERG_DECLARE_JSON_SERDE(ErrorResponse)
 ICEBERG_DECLARE_JSON_SERDE(ListNamespacesResponse)
 ICEBERG_DECLARE_JSON_SERDE(CreateNamespaceRequest)
 ICEBERG_DECLARE_JSON_SERDE(CreateNamespaceResponse)
diff --git a/src/iceberg/catalog/rest/meson.build 
b/src/iceberg/catalog/rest/meson.build
index 5f1f635..e8edc35 100644
--- a/src/iceberg/catalog/rest/meson.build
+++ b/src/iceberg/catalog/rest/meson.build
@@ -15,7 +15,11 @@
 # specific language governing permissions and limitations
 # under the License.
 
-iceberg_rest_sources = files('json_internal.cc', 'rest_catalog.cc')
+iceberg_rest_sources = files(
+    'json_internal.cc',
+    'rest_catalog.cc',
+    'validator.cc',
+)
 # cpr does not export symbols, so on Windows it must
 # be used as a static lib
 cpr_needs_static = (
@@ -46,4 +50,7 @@ iceberg_rest_dep = declare_dependency(
 meson.override_dependency('iceberg-rest', iceberg_rest_dep)
 pkg.generate(iceberg_rest_lib)
 
-install_headers(['rest_catalog.h', 'types.h'], subdir: 'iceberg/catalog/rest')
+install_headers(
+    ['rest_catalog.h', 'types.h', 'json_internal.h', 'validator.h'],
+    subdir: 'iceberg/catalog/rest',
+)
diff --git a/src/iceberg/catalog/rest/types.h b/src/iceberg/catalog/rest/types.h
index 11411cd..15e9831 100644
--- a/src/iceberg/catalog/rest/types.h
+++ b/src/iceberg/catalog/rest/types.h
@@ -20,7 +20,6 @@
 #pragma once
 
 #include <memory>
-#include <optional>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -34,6 +33,26 @@
 
 namespace iceberg::rest {
 
+/// \brief Server-provided configuration for the catalog.
+struct ICEBERG_REST_EXPORT CatalogConfig {
+  std::unordered_map<std::string, std::string> defaults;   // required
+  std::unordered_map<std::string, std::string> overrides;  // required
+  std::vector<std::string> endpoints;
+};
+
+/// \brief JSON error payload returned in a response with further details on 
the error.
+struct ICEBERG_REST_EXPORT ErrorModel {
+  std::string message;  // required
+  std::string type;     // required
+  uint32_t code;        // required
+  std::vector<std::string> stack;
+};
+
+/// \brief Error response body returned in a response.
+struct ICEBERG_REST_EXPORT ErrorResponse {
+  ErrorModel error;  // required
+};
+
 /// \brief Request to create a namespace.
 struct ICEBERG_REST_EXPORT CreateNamespaceRequest {
   Namespace namespace_;  // required
diff --git a/src/iceberg/catalog/rest/validator.cc 
b/src/iceberg/catalog/rest/validator.cc
new file mode 100644
index 0000000..de25fa3
--- /dev/null
+++ b/src/iceberg/catalog/rest/validator.cc
@@ -0,0 +1,142 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#include "iceberg/catalog/rest/validator.h"
+
+#include <algorithm>
+#include <format>
+
+#include "iceberg/catalog/rest/types.h"
+#include "iceberg/result.h"
+#include "iceberg/util/formatter_internal.h"
+#include "iceberg/util/macros.h"
+
+namespace iceberg::rest {
+
+// Configuration and Error types
+
+Status Validator::Validate(const CatalogConfig& config) {
+  // TODO(Li Feiyang): Add an invalidEndpoint test that validates endpoint 
format.
+  // See:
+  // 
https://github.com/apache/iceberg/blob/main/core/src/test/java/org/apache/iceberg/rest/responses/TestConfigResponseParser.java#L164
+  // for reference.
+  return {};
+}
+
+Status Validator::Validate(const ErrorModel& error) {
+  if (error.message.empty() || error.type.empty()) {
+    return Invalid("Invalid error model: missing required fields");
+  }
+
+  if (error.code < 400 || error.code > 600) {
+    return Invalid("Invalid error model: code {} is out of range [400, 600]", 
error.code);
+  }
+
+  // stack is optional, no validation needed
+  return {};
+}
+
+// We don't validate the error field because ErrorModel::Validate has been 
called in the
+// FromJson.
+Status Validator::Validate(const ErrorResponse& response) { return {}; }
+
+// Namespace operations
+
+Status Validator::Validate(const ListNamespacesResponse& response) { return 
{}; }
+
+Status Validator::Validate(const CreateNamespaceRequest& request) { return {}; 
}
+
+Status Validator::Validate(const CreateNamespaceResponse& response) { return 
{}; }
+
+Status Validator::Validate(const GetNamespaceResponse& response) { return {}; }
+
+Status Validator::Validate(const UpdateNamespacePropertiesRequest& request) {
+  // keys in updates and removals must not overlap
+  if (request.removals.empty() || request.updates.empty()) {
+    return {};
+  }
+
+  auto extract_and_sort = [](const auto& container, auto key_extractor) {
+    std::vector<std::string_view> result;
+    result.reserve(container.size());
+    for (const auto& item : container) {
+      result.push_back(std::string_view{key_extractor(item)});
+    }
+    std::ranges::sort(result);
+    return result;
+  };
+
+  auto sorted_removals =
+      extract_and_sort(request.removals, [](const auto& s) -> const auto& { 
return s; });
+  auto sorted_update_keys = extract_and_sort(
+      request.updates, [](const auto& pair) -> const auto& { return 
pair.first; });
+
+  std::vector<std::string_view> common;
+  std::ranges::set_intersection(sorted_removals, sorted_update_keys,
+                                std::back_inserter(common));
+
+  if (!common.empty()) {
+    return Invalid(
+        "Invalid namespace update: cannot simultaneously set and remove keys: 
{}",
+        common);
+  }
+  return {};
+}
+
+Status Validator::Validate(const UpdateNamespacePropertiesResponse& response) {
+  return {};
+}
+
+// Table operations
+
+Status Validator::Validate(const ListTablesResponse& response) { return {}; }
+
+Status Validator::Validate(const LoadTableResult& result) {
+  if (!result.metadata) {
+    return Invalid("Invalid metadata: null");
+  }
+  return {};
+}
+
+Status Validator::Validate(const RegisterTableRequest& request) {
+  if (request.name.empty()) {
+    return Invalid("Missing table name");
+  }
+
+  if (request.metadata_location.empty()) {
+    return Invalid("Empty metadata location");
+  }
+
+  return {};
+}
+
+Status Validator::Validate(const RenameTableRequest& request) {
+  ICEBERG_RETURN_UNEXPECTED(Validate(request.source));
+  ICEBERG_RETURN_UNEXPECTED(Validate(request.destination));
+  return {};
+}
+
+Status Validator::Validate(const TableIdentifier& identifier) {
+  if (identifier.name.empty()) {
+    return Invalid("Invalid table identifier: missing table name");
+  }
+  return {};
+}
+
+}  // namespace iceberg::rest
diff --git a/src/iceberg/catalog/rest/validator.h 
b/src/iceberg/catalog/rest/validator.h
new file mode 100644
index 0000000..2c80cab
--- /dev/null
+++ b/src/iceberg/catalog/rest/validator.h
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#pragma once
+
+#include "iceberg/catalog/rest/iceberg_rest_export.h"
+#include "iceberg/catalog/rest/types.h"
+#include "iceberg/result.h"
+
+/// \file iceberg/catalog/rest/validator.h
+/// Validator for REST Catalog API types.
+
+namespace iceberg::rest {
+
+/// \brief Validator for REST Catalog API types. Validation should be called 
after
+/// deserializing objects from external sources to ensure data integrity 
before the
+/// objects are used.
+class ICEBERG_REST_EXPORT Validator {
+ public:
+  // Configuration and Error types
+
+  /// \brief Validates a CatalogConfig object.
+  static Status Validate(const CatalogConfig& config);
+
+  /// \brief Validates an ErrorModel object.
+  static Status Validate(const ErrorModel& error);
+
+  /// \brief Validates an ErrorResponse object.
+  static Status Validate(const ErrorResponse& response);
+
+  // Namespace operations
+
+  /// \brief Validates a ListNamespacesResponse object.
+  static Status Validate(const ListNamespacesResponse& response);
+
+  /// \brief Validates a CreateNamespaceRequest object.
+  static Status Validate(const CreateNamespaceRequest& request);
+
+  /// \brief Validates a CreateNamespaceResponse object.
+  static Status Validate(const CreateNamespaceResponse& response);
+
+  /// \brief Validates a GetNamespaceResponse object.
+  static Status Validate(const GetNamespaceResponse& response);
+
+  /// \brief Validates an UpdateNamespacePropertiesRequest object.
+  static Status Validate(const UpdateNamespacePropertiesRequest& request);
+
+  /// \brief Validates an UpdateNamespacePropertiesResponse object.
+  static Status Validate(const UpdateNamespacePropertiesResponse& response);
+
+  // Table operations
+
+  /// \brief Validates a ListTablesResponse object.
+  static Status Validate(const ListTablesResponse& response);
+
+  /// \brief Validates a LoadTableResult object.
+  static Status Validate(const LoadTableResult& result);
+
+  /// \brief Validates a RegisterTableRequest object.
+  static Status Validate(const RegisterTableRequest& request);
+
+  /// \brief Validates a RenameTableRequest object.
+  static Status Validate(const RenameTableRequest& request);
+
+  // Other types
+
+  /// \brief Validates a TableIdentifier object.
+  static Status Validate(const TableIdentifier& identifier);
+};
+
+}  // namespace iceberg::rest
diff --git a/src/iceberg/test/rest_json_internal_test.cc 
b/src/iceberg/test/rest_json_internal_test.cc
index d95f6a2..eba669a 100644
--- a/src/iceberg/test/rest_json_internal_test.cc
+++ b/src/iceberg/test/rest_json_internal_test.cc
@@ -17,7 +17,6 @@
  * under the License.
  */
 
-#include <algorithm>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -92,6 +91,20 @@ bool operator==(const RenameTableRequest& lhs, const 
RenameTableRequest& rhs) {
          lhs.destination.name == rhs.destination.name;
 }
 
+bool operator==(const CatalogConfig& lhs, const CatalogConfig& rhs) {
+  return lhs.overrides == rhs.overrides && lhs.defaults == rhs.defaults &&
+         lhs.endpoints == rhs.endpoints;
+}
+
+bool operator==(const ErrorModel& lhs, const ErrorModel& rhs) {
+  return lhs.message == rhs.message && lhs.type == rhs.type && lhs.code == 
rhs.code &&
+         lhs.stack == rhs.stack;
+}
+
+bool operator==(const ErrorResponse& lhs, const ErrorResponse& rhs) {
+  return lhs.error == rhs.error;
+}
+
 // Test parameter structure for roundtrip tests
 template <typename Model>
 struct JsonRoundTripParam {
@@ -747,4 +760,212 @@ INSTANTIATE_TEST_SUITE_P(
       return info.param.test_name;
     });
 
+DECLARE_ROUNDTRIP_TEST(CatalogConfig)
+
+INSTANTIATE_TEST_SUITE_P(
+    CatalogConfigCases, CatalogConfigTest,
+    ::testing::Values(
+        // Full config with both defaults and overrides
+        CatalogConfigParam{
+            .test_name = "FullConfig",
+            .expected_json_str =
+                
R"({"defaults":{"warehouse":"s3://bucket/warehouse"},"overrides":{"clients":"5"}})",
+            .model = {.defaults = {{"warehouse", "s3://bucket/warehouse"}},
+                      .overrides = {{"clients", "5"}}}},
+        // Only defaults
+        CatalogConfigParam{
+            .test_name = "OnlyDefaults",
+            .expected_json_str =
+                
R"({"defaults":{"warehouse":"s3://bucket/warehouse"},"overrides":{}})",
+            .model = {.defaults = {{"warehouse", "s3://bucket/warehouse"}}}},
+        // Only overrides
+        CatalogConfigParam{
+            .test_name = "OnlyOverrides",
+            .expected_json_str = 
R"({"defaults":{},"overrides":{"clients":"5"}})",
+            .model = {.overrides = {{"clients", "5"}}}},
+        // Both empty
+        CatalogConfigParam{.test_name = "BothEmpty",
+                           .expected_json_str = 
R"({"defaults":{},"overrides":{}})",
+                           .model = {}},
+        // With endpoints
+        CatalogConfigParam{
+            .test_name = "WithEndpoints",
+            .expected_json_str =
+                
R"({"defaults":{"warehouse":"s3://bucket/warehouse"},"overrides":{"clients":"5"},"endpoints":["GET
 /v1/config","POST /v1/tables"]})",
+            .model = {.defaults = {{"warehouse", "s3://bucket/warehouse"}},
+                      .overrides = {{"clients", "5"}},
+
+                      .endpoints = {"GET /v1/config", "POST /v1/tables"}}},
+        // Only endpoints
+        CatalogConfigParam{
+            .test_name = "OnlyEndpoints",
+            .expected_json_str =
+                R"({"defaults":{},"overrides":{},"endpoints":["GET 
/v1/config"]})",
+            .model = {.endpoints = {"GET /v1/config"}}}),
+    [](const ::testing::TestParamInfo<CatalogConfigParam>& info) {
+      return info.param.test_name;
+    });
+
+DECLARE_DESERIALIZE_TEST(CatalogConfig)
+
+INSTANTIATE_TEST_SUITE_P(
+    CatalogConfigDeserializeCases, CatalogConfigDeserializeTest,
+    ::testing::Values(
+        // Missing overrides field
+        CatalogConfigDeserializeParam{
+            .test_name = "MissingOverrides",
+            .json_str = 
R"({"defaults":{"warehouse":"s3://bucket/warehouse"}})",
+            .expected_model = {.defaults = {{"warehouse", 
"s3://bucket/warehouse"}}}},
+        // Null overrides field
+        CatalogConfigDeserializeParam{
+            .test_name = "NullOverrides",
+            .json_str =
+                
R"({"defaults":{"warehouse":"s3://bucket/warehouse"},"overrides":null})",
+            .expected_model = {.defaults = {{"warehouse", 
"s3://bucket/warehouse"}}}},
+        // Missing defaults field
+        CatalogConfigDeserializeParam{
+            .test_name = "MissingDefaults",
+            .json_str = R"({"overrides":{"clients":"5"}})",
+            .expected_model = {.overrides = {{"clients", "5"}}}},
+        // Null defaults field
+        CatalogConfigDeserializeParam{
+            .test_name = "NullDefaults",
+            .json_str = R"({"defaults":null,"overrides":{"clients":"5"}})",
+            .expected_model = {.overrides = {{"clients", "5"}}}},
+        // Empty JSON object
+        CatalogConfigDeserializeParam{
+            .test_name = "EmptyJson", .json_str = R"({})", .expected_model = 
{}},
+        // Both fields null
+        CatalogConfigDeserializeParam{.test_name = "BothNull",
+                                      .json_str = 
R"({"defaults":null,"overrides":null})",
+                                      .expected_model = {}}),
+    [](const ::testing::TestParamInfo<CatalogConfigDeserializeParam>& info) {
+      return info.param.test_name;
+    });
+
+DECLARE_INVALID_TEST(CatalogConfig)
+
+INSTANTIATE_TEST_SUITE_P(
+    CatalogConfigInvalidCases, CatalogConfigInvalidTest,
+    ::testing::Values(
+        // Defaults has wrong type (array instead of object)
+        CatalogConfigInvalidParam{
+            .test_name = "WrongDefaultsType",
+            .invalid_json_str =
+                
R"({"defaults":["warehouse","s3://bucket/warehouse"],"overrides":{"clients":"5"}})",
+            .expected_error_message = "type must be object, but is array"},
+        // Overrides has wrong type (string instead of object)
+        CatalogConfigInvalidParam{
+            .test_name = "WrongOverridesType",
+            .invalid_json_str =
+                
R"({"defaults":{"warehouse":"s3://bucket/warehouse"},"overrides":"clients"})",
+            .expected_error_message = "type must be object, but is string"}),
+    [](const ::testing::TestParamInfo<CatalogConfigInvalidParam>& info) {
+      return info.param.test_name;
+    });
+
+DECLARE_ROUNDTRIP_TEST(ErrorResponse)
+
+INSTANTIATE_TEST_SUITE_P(
+    ErrorResponseCases, ErrorResponseTest,
+    ::testing::Values(
+        // Error without stack trace
+        ErrorResponseParam{
+            .test_name = "WithoutStack",
+            .expected_json_str =
+                R"({"error":{"message":"The given namespace does not 
exist","type":"NoSuchNamespaceException","code":404}})",
+            .model = {.error = {.message = "The given namespace does not 
exist",
+                                .type = "NoSuchNamespaceException",
+                                .code = 404}}},
+        // 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 = {.error = {.message = "The given namespace does not 
exist",
+                                .type = "NoSuchNamespaceException",
+                                .code = 404,
+                                .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 = {.error = {.message = "Internal server error",
+                                .type = "InternalServerError",
+                                .code = 500,
+                                .stack = {"line1", "line2", "line3"}}}}),
+    [](const ::testing::TestParamInfo<ErrorResponseParam>& info) {
+      return info.param.test_name;
+    });
+
+DECLARE_DESERIALIZE_TEST(ErrorResponse)
+
+INSTANTIATE_TEST_SUITE_P(
+    ErrorResponseDeserializeCases, ErrorResponseDeserializeTest,
+    ::testing::Values(
+        // Stack field is null (should deserialize to empty vector)
+        ErrorResponseDeserializeParam{
+            .test_name = "NullStack",
+            .json_str =
+                R"({"error":{"message":"The given namespace does not 
exist","type":"NoSuchNamespaceException","code":404,"stack":null}})",
+            .expected_model = {.error = {.message = "The given namespace does 
not exist",
+                                         .type = "NoSuchNamespaceException",
+                                         .code = 404}}},
+        // 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 = {.error = {.message = "The given namespace does 
not exist",
+                                         .type = "NoSuchNamespaceException",
+                                         .code = 404}}}),
+    [](const ::testing::TestParamInfo<ErrorResponseDeserializeParam>& info) {
+      return info.param.test_name;
+    });
+
+DECLARE_INVALID_TEST(ErrorResponse)
+
+INSTANTIATE_TEST_SUITE_P(
+    ErrorResponseInvalidCases, ErrorResponseInvalidTest,
+    ::testing::Values(
+        // Missing error field
+        ErrorResponseInvalidParam{.test_name = "MissingError",
+                                  .invalid_json_str = R"({})",
+                                  .expected_error_message = "Missing 'error'"},
+        // Null error field
+        ErrorResponseInvalidParam{.test_name = "NullError",
+                                  .invalid_json_str = R"({"error":null})",
+                                  .expected_error_message = "Missing 
'error'"}),
+    [](const ::testing::TestParamInfo<ErrorResponseInvalidParam>& info) {
+      return info.param.test_name;
+    });
+
+DECLARE_INVALID_TEST(ErrorModel)
+
+INSTANTIATE_TEST_SUITE_P(
+    ErrorModelInvalidCases, ErrorModelInvalidTest,
+    ::testing::Values(
+        // Missing required type field
+        ErrorModelInvalidParam{
+            .test_name = "MissingType",
+            .invalid_json_str =
+                R"({"message":"The given namespace does not 
exist","code":404})",
+            .expected_error_message = "Missing 'type'"},
+        // Missing required code field
+        ErrorModelInvalidParam{
+            .test_name = "MissingCode",
+            .invalid_json_str =
+                R"({"message":"The given namespace does not 
exist","type":"NoSuchNamespaceException"})",
+            .expected_error_message = "Missing 'code'"},
+        // Wrong type for message field
+        ErrorModelInvalidParam{
+            .test_name = "WrongMessageType",
+            .invalid_json_str =
+                
R"({"message":123,"type":"NoSuchNamespaceException","code":404})",
+            .expected_error_message = "type must be string, but is number"}),
+    [](const ::testing::TestParamInfo<ErrorModelInvalidParam>& info) {
+      return info.param.test_name;
+    });
+
 }  // namespace iceberg::rest

Reply via email to