Skip to content

Commit 9d0bfa3

Browse files
committed
feat: add config, error and validation for rest types
1 parent dc23f76 commit 9d0bfa3

File tree

8 files changed

+658
-5
lines changed

8 files changed

+658
-5
lines changed

src/iceberg/catalog/rest/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
set(ICEBERG_REST_SOURCES rest_catalog.cc json_internal.cc)
18+
set(ICEBERG_REST_SOURCES rest_catalog.cc json_internal.cc validator.cc)
1919

2020
set(ICEBERG_REST_STATIC_BUILD_INTERFACE_LIBS)
2121
set(ICEBERG_REST_SHARED_BUILD_INTERFACE_LIBS)

src/iceberg/catalog/rest/json_internal.cc

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
#include <nlohmann/json.hpp>
2828

2929
#include "iceberg/catalog/rest/types.h"
30+
#include "iceberg/catalog/rest/validator.h"
3031
#include "iceberg/json_internal.h"
3132
#include "iceberg/table_identifier.h"
3233
#include "iceberg/util/json_util_internal.h"
@@ -59,9 +60,77 @@ constexpr std::string_view kDestination = "destination";
5960
constexpr std::string_view kMetadata = "metadata";
6061
constexpr std::string_view kConfig = "config";
6162
constexpr std::string_view kIdentifiers = "identifiers";
63+
constexpr std::string_view kOverrides = "overrides";
64+
constexpr std::string_view kDefaults = "defaults";
65+
constexpr std::string_view kEndpoints = "endpoints";
66+
constexpr std::string_view kMessage = "message";
67+
constexpr std::string_view kType = "type";
68+
constexpr std::string_view kCode = "code";
69+
constexpr std::string_view kStack = "stack";
70+
constexpr std::string_view kError = "error";
6271

6372
} // namespace
6473

74+
nlohmann::json ToJson(const CatalogConfig& config) {
75+
nlohmann::json json;
76+
json[kOverrides] = config.overrides;
77+
json[kDefaults] = config.defaults;
78+
if (!config.endpoints.empty()) {
79+
json[kEndpoints] = config.endpoints;
80+
}
81+
return json;
82+
}
83+
84+
Result<CatalogConfig> CatalogConfigFromJson(const nlohmann::json& json) {
85+
CatalogConfig config;
86+
ICEBERG_ASSIGN_OR_RAISE(
87+
config.overrides,
88+
GetJsonValueOrDefault<decltype(config.overrides)>(json, kOverrides));
89+
ICEBERG_ASSIGN_OR_RAISE(
90+
config.defaults, GetJsonValueOrDefault<decltype(config.defaults)>(json, kDefaults));
91+
ICEBERG_ASSIGN_OR_RAISE(
92+
config.endpoints,
93+
GetJsonValueOrDefault<std::vector<std::string>>(json, kEndpoints));
94+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(config));
95+
return config;
96+
}
97+
98+
nlohmann::json ToJson(const ErrorModel& error) {
99+
nlohmann::json json;
100+
json[kMessage] = error.message;
101+
json[kType] = error.type;
102+
json[kCode] = error.code;
103+
if (!error.stack.empty()) {
104+
json[kStack] = error.stack;
105+
}
106+
return json;
107+
}
108+
109+
Result<ErrorModel> ErrorModelFromJson(const nlohmann::json& json) {
110+
ErrorModel error;
111+
ICEBERG_ASSIGN_OR_RAISE(error.message, GetJsonValue<std::string>(json, kMessage));
112+
ICEBERG_ASSIGN_OR_RAISE(error.type, GetJsonValue<std::string>(json, kType));
113+
ICEBERG_ASSIGN_OR_RAISE(error.code, GetJsonValue<uint16_t>(json, kCode));
114+
ICEBERG_ASSIGN_OR_RAISE(error.stack,
115+
GetJsonValueOrDefault<std::vector<std::string>>(json, kStack));
116+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(error));
117+
return error;
118+
}
119+
120+
nlohmann::json ToJson(const ErrorResponse& response) {
121+
nlohmann::json json;
122+
json[kError] = ToJson(response.error);
123+
return json;
124+
}
125+
126+
Result<ErrorResponse> ErrorResponseFromJson(const nlohmann::json& json) {
127+
ErrorResponse response;
128+
ICEBERG_ASSIGN_OR_RAISE(auto error_json, GetJsonValue<nlohmann::json>(json, kError));
129+
ICEBERG_ASSIGN_OR_RAISE(response.error, ErrorModelFromJson(error_json));
130+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
131+
return response;
132+
}
133+
65134
nlohmann::json ToJson(const CreateNamespaceRequest& request) {
66135
nlohmann::json json;
67136
json[kNamespace] = request.namespace_.levels;
@@ -79,6 +148,7 @@ Result<CreateNamespaceRequest> CreateNamespaceRequestFromJson(
79148
ICEBERG_ASSIGN_OR_RAISE(
80149
request.properties,
81150
GetJsonValueOrDefault<decltype(request.properties)>(json, kProperties));
151+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
82152
return request;
83153
}
84154

@@ -102,6 +172,7 @@ Result<UpdateNamespacePropertiesRequest> UpdateNamespacePropertiesRequestFromJso
102172
request.removals, GetJsonValueOrDefault<std::vector<std::string>>(json, kRemovals));
103173
ICEBERG_ASSIGN_OR_RAISE(
104174
request.updates, GetJsonValueOrDefault<decltype(request.updates)>(json, kUpdates));
175+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
105176
return request;
106177
}
107178

@@ -122,6 +193,7 @@ Result<RegisterTableRequest> RegisterTableRequestFromJson(const nlohmann::json&
122193
GetJsonValue<std::string>(json, kMetadataLocation));
123194
ICEBERG_ASSIGN_OR_RAISE(request.overwrite,
124195
GetJsonValueOrDefault<bool>(json, kOverwrite, false));
196+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
125197
return request;
126198
}
127199

@@ -139,6 +211,7 @@ Result<RenameTableRequest> RenameTableRequestFromJson(const nlohmann::json& json
139211
ICEBERG_ASSIGN_OR_RAISE(auto dest_json,
140212
GetJsonValue<nlohmann::json>(json, kDestination));
141213
ICEBERG_ASSIGN_OR_RAISE(request.destination, TableIdentifierFromJson(dest_json));
214+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
142215
return request;
143216
}
144217

@@ -165,6 +238,7 @@ Result<LoadTableResult> LoadTableResultFromJson(const nlohmann::json& json) {
165238
ICEBERG_ASSIGN_OR_RAISE(
166239
result.config, (GetJsonValueOrDefault<std::unordered_map<std::string, std::string>>(
167240
json, kConfig)));
241+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(result));
168242
return result;
169243
}
170244

@@ -192,6 +266,7 @@ Result<ListNamespacesResponse> ListNamespacesResponseFromJson(
192266
ICEBERG_ASSIGN_OR_RAISE(auto ns, NamespaceFromJson(ns_json));
193267
response.namespaces.push_back(std::move(ns));
194268
}
269+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
195270
return response;
196271
}
197272

@@ -253,6 +328,7 @@ Result<UpdateNamespacePropertiesResponse> UpdateNamespacePropertiesResponseFromJ
253328
GetJsonValue<std::vector<std::string>>(json, kRemoved));
254329
ICEBERG_ASSIGN_OR_RAISE(
255330
response.missing, GetJsonValueOrDefault<std::vector<std::string>>(json, kMissing));
331+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
256332
return response;
257333
}
258334

@@ -279,6 +355,7 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const nlohmann::json& json
279355
ICEBERG_ASSIGN_OR_RAISE(auto identifier, TableIdentifierFromJson(id_json));
280356
response.identifiers.push_back(std::move(identifier));
281357
}
358+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
282359
return response;
283360
}
284361

src/iceberg/catalog/rest/json_internal.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,31 @@
2424
#include "iceberg/catalog/rest/types.h"
2525
#include "iceberg/result.h"
2626

27+
/// \file iceberg/catalog/rest/json_internal.h
28+
/// JSON serialization and deserialization for Iceberg REST Catalog API types.
29+
2730
namespace iceberg::rest {
2831

32+
/// \brief Serializes a `CatalogConfig` object to JSON.
33+
ICEBERG_REST_EXPORT nlohmann::json ToJson(const CatalogConfig& config);
34+
35+
/// \brief Deserializes a JSON object into a `CatalogConfig` object.
36+
ICEBERG_REST_EXPORT Result<CatalogConfig> CatalogConfigFromJson(
37+
const nlohmann::json& json);
38+
39+
/// \brief Serializes a `ErrorModel` object to JSON.
40+
ICEBERG_REST_EXPORT nlohmann::json ToJson(const ErrorModel& error);
41+
42+
/// \brief Deserializes a JSON object into a `ErrorModel` object.
43+
ICEBERG_REST_EXPORT Result<ErrorModel> ErrorModelFromJson(const nlohmann::json& json);
44+
45+
/// \brief Serializes a `ErrorResponse` object to JSON.
46+
ICEBERG_REST_EXPORT nlohmann::json ToJson(const ErrorResponse& response);
47+
48+
/// \brief Deserializes a JSON object into a `ErrorResponse` object.
49+
ICEBERG_REST_EXPORT Result<ErrorResponse> ErrorResponseFromJson(
50+
const nlohmann::json& json);
51+
2952
/// \brief Serializes a `ListNamespacesResponse` object to JSON.
3053
ICEBERG_REST_EXPORT nlohmann::json ToJson(const ListNamespacesResponse& response);
3154

src/iceberg/catalog/rest/meson.build

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
iceberg_rest_sources = files('json_internal.cc', 'rest_catalog.cc')
18+
iceberg_rest_sources = files(
19+
'json_internal.cc',
20+
'rest_catalog.cc',
21+
'validator.cc',
22+
)
1923
# cpr does not export symbols, so on Windows it must
2024
# be used as a static lib
2125
cpr_needs_static = (
@@ -46,4 +50,7 @@ iceberg_rest_dep = declare_dependency(
4650
meson.override_dependency('iceberg-rest', iceberg_rest_dep)
4751
pkg.generate(iceberg_rest_lib)
4852

49-
install_headers(['rest_catalog.h', 'types.h'], subdir: 'iceberg/catalog/rest')
53+
install_headers(
54+
['rest_catalog.h', 'types.h', 'json_internal.h', 'validator.h'],
55+
subdir: 'iceberg/catalog/rest',
56+
)

src/iceberg/catalog/rest/types.h

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
#pragma once
2121

2222
#include <memory>
23-
#include <optional>
2423
#include <string>
2524
#include <unordered_map>
2625
#include <vector>
@@ -34,6 +33,26 @@
3433

3534
namespace iceberg::rest {
3635

36+
/// \brief Server-provided configuration for the catalog.
37+
struct ICEBERG_REST_EXPORT CatalogConfig {
38+
std::unordered_map<std::string, std::string> overrides; // required
39+
std::unordered_map<std::string, std::string> defaults; // required
40+
std::vector<std::string> endpoints;
41+
};
42+
43+
/// \brief JSON error payload returned in a response with further details on the error.
44+
struct ICEBERG_REST_EXPORT ErrorModel {
45+
std::string message; // required
46+
std::string type; // required
47+
uint16_t code; // required
48+
std::vector<std::string> stack;
49+
};
50+
51+
/// \brief Error response body returned in a response.
52+
struct ICEBERG_REST_EXPORT ErrorResponse {
53+
ErrorModel error; // required
54+
};
55+
3756
/// \brief Request to create a namespace.
3857
struct ICEBERG_REST_EXPORT CreateNamespaceRequest {
3958
Namespace namespace_; // required
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#include "iceberg/catalog/rest/validator.h"
21+
22+
#include <format>
23+
#include <ranges>
24+
#include <unordered_set>
25+
26+
#include "iceberg/catalog/rest/types.h"
27+
#include "iceberg/result.h"
28+
29+
namespace iceberg::rest {
30+
31+
// Configuration and Error types
32+
33+
Status Validator::Validate(const CatalogConfig& config) {
34+
// TODO(Li Feiyang): Add an invalidEndpoint test that validates endpoint format.
35+
// See:
36+
// https://github.com/apache/iceberg/blob/main/core/src/test/java/org/apache/iceberg/rest/responses/TestConfigResponseParser.java#L164
37+
// for reference.
38+
return {};
39+
}
40+
41+
Status Validator::Validate(const ErrorModel& error) {
42+
if (error.message.empty() || error.type.empty()) [[unlikely]] {
43+
return Invalid("Invalid error model: missing required fields");
44+
}
45+
46+
if (error.code < 400 || error.code > 600) [[unlikely]] {
47+
return Invalid("Invalid error model: code must be between 400 and 600");
48+
}
49+
50+
// stack is optional, no validation needed
51+
return {};
52+
}
53+
54+
Status Validator::Validate(const ErrorResponse& response) { return {}; }
55+
56+
// Namespace operations
57+
58+
Status Validator::Validate(const ListNamespacesResponse& response) { return {}; }
59+
60+
Status Validator::Validate(const CreateNamespaceRequest& request) { return {}; }
61+
62+
Status Validator::Validate(const CreateNamespaceResponse& response) { return {}; }
63+
64+
Status Validator::Validate(const GetNamespaceResponse& response) { return {}; }
65+
66+
Status Validator::Validate(const UpdateNamespacePropertiesRequest& request) {
67+
// keys in updates and removals must not overlap
68+
if (request.removals.empty() || request.updates.empty()) [[unlikely]] {
69+
return {};
70+
}
71+
72+
std::unordered_set<std::string> remove_set(std::from_range, request.removals);
73+
74+
auto common =
75+
request.updates | std::views::keys |
76+
std::views::filter([&](const std::string& k) { return remove_set.contains(k); }) |
77+
std::ranges::to<std::vector<std::string>>();
78+
79+
if (!common.empty()) {
80+
std::string keys;
81+
bool first = true;
82+
std::ranges::for_each(common, [&](const std::string& s) {
83+
if (!std::exchange(first, false)) keys += ", ";
84+
keys += s;
85+
});
86+
87+
return Invalid(
88+
"Invalid namespace properties update: cannot simultaneously set and remove keys: "
89+
"[{}]",
90+
keys);
91+
}
92+
return {};
93+
}
94+
95+
Status Validator::Validate(const UpdateNamespacePropertiesResponse& response) {
96+
return {};
97+
}
98+
99+
// Table operations
100+
101+
Status Validator::Validate(const ListTablesResponse& response) { return {}; }
102+
103+
Status Validator::Validate(const LoadTableResult& result) {
104+
if (!result.metadata) [[unlikely]] {
105+
return Invalid("Invalid metadata: null");
106+
}
107+
return {};
108+
}
109+
110+
Status Validator::Validate(const RegisterTableRequest& request) {
111+
if (request.name.empty()) [[unlikely]] {
112+
return Invalid("Invalid table name: empty");
113+
}
114+
115+
if (request.metadata_location.empty()) [[unlikely]] {
116+
return Invalid("Invalid metadata location: empty");
117+
}
118+
119+
return {};
120+
}
121+
122+
Status Validator::Validate(const RenameTableRequest& request) {
123+
if (request.source.ns.levels.empty() || request.source.name.empty()) [[unlikely]] {
124+
return Invalid("Invalid source identifier");
125+
}
126+
127+
if (request.destination.ns.levels.empty() || request.destination.name.empty())
128+
[[unlikely]] {
129+
return Invalid("Invalid destination identifier");
130+
}
131+
132+
return {};
133+
}
134+
135+
} // namespace iceberg::rest

0 commit comments

Comments
 (0)