Skip to content

Commit ad5dadc

Browse files
authored
feat: add config, error and validation for rest types (#292)
1 parent 1c431b6 commit ad5dadc

File tree

9 files changed

+569
-6
lines changed

9 files changed

+569
-6
lines changed

meson.build

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ project(
3030
],
3131
)
3232

33+
cpp = meson.get_compiler('cpp')
34+
args = cpp.get_supported_arguments(['/bigobj'])
35+
add_project_arguments(args, language: 'cpp')
36+
3337
subdir('src')
3438

3539
install_data(

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: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@
2020
#include "iceberg/catalog/rest/json_internal.h"
2121

2222
#include <string>
23-
#include <unordered_map>
2423
#include <utility>
2524
#include <vector>
2625

2726
#include <nlohmann/json.hpp>
2827

2928
#include "iceberg/catalog/rest/types.h"
29+
#include "iceberg/catalog/rest/validator.h"
3030
#include "iceberg/json_internal.h"
3131
#include "iceberg/table_identifier.h"
3232
#include "iceberg/util/json_util_internal.h"
@@ -59,9 +59,76 @@ constexpr std::string_view kDestination = "destination";
5959
constexpr std::string_view kMetadata = "metadata";
6060
constexpr std::string_view kConfig = "config";
6161
constexpr std::string_view kIdentifiers = "identifiers";
62+
constexpr std::string_view kOverrides = "overrides";
63+
constexpr std::string_view kDefaults = "defaults";
64+
constexpr std::string_view kEndpoints = "endpoints";
65+
constexpr std::string_view kMessage = "message";
66+
constexpr std::string_view kType = "type";
67+
constexpr std::string_view kCode = "code";
68+
constexpr std::string_view kStack = "stack";
69+
constexpr std::string_view kError = "error";
6270

6371
} // namespace
6472

73+
nlohmann::json ToJson(const CatalogConfig& config) {
74+
nlohmann::json json;
75+
json[kOverrides] = config.overrides;
76+
json[kDefaults] = config.defaults;
77+
SetContainerField(json, kEndpoints, config.endpoints);
78+
return json;
79+
}
80+
81+
Result<CatalogConfig> CatalogConfigFromJson(const nlohmann::json& json) {
82+
CatalogConfig config;
83+
ICEBERG_ASSIGN_OR_RAISE(
84+
config.overrides,
85+
GetJsonValueOrDefault<decltype(config.overrides)>(json, kOverrides));
86+
ICEBERG_ASSIGN_OR_RAISE(
87+
config.defaults, GetJsonValueOrDefault<decltype(config.defaults)>(json, kDefaults));
88+
ICEBERG_ASSIGN_OR_RAISE(
89+
config.endpoints,
90+
GetJsonValueOrDefault<std::vector<std::string>>(json, kEndpoints));
91+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(config));
92+
return config;
93+
}
94+
95+
nlohmann::json ToJson(const ErrorModel& error) {
96+
nlohmann::json json;
97+
json[kMessage] = error.message;
98+
json[kType] = error.type;
99+
json[kCode] = error.code;
100+
SetContainerField(json, kStack, error.stack);
101+
return json;
102+
}
103+
104+
Result<ErrorModel> ErrorModelFromJson(const nlohmann::json& json) {
105+
ErrorModel error;
106+
// NOTE: Iceberg's Java implementation allows missing required fields (message, type,
107+
// code) during deserialization, which deviates from the REST spec. We enforce strict
108+
// validation here.
109+
ICEBERG_ASSIGN_OR_RAISE(error.message, GetJsonValue<std::string>(json, kMessage));
110+
ICEBERG_ASSIGN_OR_RAISE(error.type, GetJsonValue<std::string>(json, kType));
111+
ICEBERG_ASSIGN_OR_RAISE(error.code, GetJsonValue<uint32_t>(json, kCode));
112+
ICEBERG_ASSIGN_OR_RAISE(error.stack,
113+
GetJsonValueOrDefault<std::vector<std::string>>(json, kStack));
114+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(error));
115+
return error;
116+
}
117+
118+
nlohmann::json ToJson(const ErrorResponse& response) {
119+
nlohmann::json json;
120+
json[kError] = ToJson(response.error);
121+
return json;
122+
}
123+
124+
Result<ErrorResponse> ErrorResponseFromJson(const nlohmann::json& json) {
125+
ErrorResponse response;
126+
ICEBERG_ASSIGN_OR_RAISE(auto error_json, GetJsonValue<nlohmann::json>(json, kError));
127+
ICEBERG_ASSIGN_OR_RAISE(response.error, ErrorModelFromJson(error_json));
128+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
129+
return response;
130+
}
131+
65132
nlohmann::json ToJson(const CreateNamespaceRequest& request) {
66133
nlohmann::json json;
67134
json[kNamespace] = request.namespace_.levels;
@@ -77,6 +144,7 @@ Result<CreateNamespaceRequest> CreateNamespaceRequestFromJson(
77144
ICEBERG_ASSIGN_OR_RAISE(
78145
request.properties,
79146
GetJsonValueOrDefault<decltype(request.properties)>(json, kProperties));
147+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
80148
return request;
81149
}
82150

@@ -94,6 +162,7 @@ Result<UpdateNamespacePropertiesRequest> UpdateNamespacePropertiesRequestFromJso
94162
request.removals, GetJsonValueOrDefault<std::vector<std::string>>(json, kRemovals));
95163
ICEBERG_ASSIGN_OR_RAISE(
96164
request.updates, GetJsonValueOrDefault<decltype(request.updates)>(json, kUpdates));
165+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
97166
return request;
98167
}
99168

@@ -114,6 +183,7 @@ Result<RegisterTableRequest> RegisterTableRequestFromJson(const nlohmann::json&
114183
GetJsonValue<std::string>(json, kMetadataLocation));
115184
ICEBERG_ASSIGN_OR_RAISE(request.overwrite,
116185
GetJsonValueOrDefault<bool>(json, kOverwrite, false));
186+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
117187
return request;
118188
}
119189

@@ -131,6 +201,7 @@ Result<RenameTableRequest> RenameTableRequestFromJson(const nlohmann::json& json
131201
ICEBERG_ASSIGN_OR_RAISE(auto dest_json,
132202
GetJsonValue<nlohmann::json>(json, kDestination));
133203
ICEBERG_ASSIGN_OR_RAISE(request.destination, TableIdentifierFromJson(dest_json));
204+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
134205
return request;
135206
}
136207

@@ -177,6 +248,7 @@ Result<ListNamespacesResponse> ListNamespacesResponseFromJson(
177248
ICEBERG_ASSIGN_OR_RAISE(auto ns, NamespaceFromJson(ns_json));
178249
response.namespaces.push_back(std::move(ns));
179250
}
251+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
180252
return response;
181253
}
182254

@@ -232,6 +304,7 @@ Result<UpdateNamespacePropertiesResponse> UpdateNamespacePropertiesResponseFromJ
232304
response.removed, GetJsonValueOrDefault<std::vector<std::string>>(json, kRemoved));
233305
ICEBERG_ASSIGN_OR_RAISE(
234306
response.missing, GetJsonValueOrDefault<std::vector<std::string>>(json, kMissing));
307+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
235308
return response;
236309
}
237310

@@ -256,6 +329,7 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const nlohmann::json& json
256329
ICEBERG_ASSIGN_OR_RAISE(auto identifier, TableIdentifierFromJson(id_json));
257330
response.identifiers.push_back(std::move(identifier));
258331
}
332+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
259333
return response;
260334
}
261335

@@ -265,6 +339,9 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const nlohmann::json& json
265339
return Model##FromJson(json); \
266340
}
267341

342+
ICEBERG_DEFINE_FROM_JSON(CatalogConfig)
343+
ICEBERG_DEFINE_FROM_JSON(ErrorModel)
344+
ICEBERG_DEFINE_FROM_JSON(ErrorResponse)
268345
ICEBERG_DEFINE_FROM_JSON(ListNamespacesResponse)
269346
ICEBERG_DEFINE_FROM_JSON(CreateNamespaceRequest)
270347
ICEBERG_DEFINE_FROM_JSON(CreateNamespaceResponse)

src/iceberg/catalog/rest/json_internal.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
#include "iceberg/catalog/rest/types.h"
2626
#include "iceberg/result.h"
2727

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

3033
template <typename Model>
@@ -40,6 +43,9 @@ Result<Model> FromJson(const nlohmann::json& json);
4043

4144
/// \note Don't forget to add `ICEBERG_DEFINE_FROM_JSON` to the end of
4245
/// `json_internal.cc` to define the `FromJson` function for the model.
46+
ICEBERG_DECLARE_JSON_SERDE(CatalogConfig)
47+
ICEBERG_DECLARE_JSON_SERDE(ErrorModel)
48+
ICEBERG_DECLARE_JSON_SERDE(ErrorResponse)
4349
ICEBERG_DECLARE_JSON_SERDE(ListNamespacesResponse)
4450
ICEBERG_DECLARE_JSON_SERDE(CreateNamespaceRequest)
4551
ICEBERG_DECLARE_JSON_SERDE(CreateNamespaceResponse)

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> defaults; // required
39+
std::unordered_map<std::string, std::string> overrides; // 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+
uint32_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: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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 <algorithm>
23+
#include <format>
24+
25+
#include "iceberg/catalog/rest/types.h"
26+
#include "iceberg/result.h"
27+
#include "iceberg/util/formatter_internal.h"
28+
#include "iceberg/util/macros.h"
29+
30+
namespace iceberg::rest {
31+
32+
// Configuration and Error types
33+
34+
Status Validator::Validate(const CatalogConfig& config) {
35+
// TODO(Li Feiyang): Add an invalidEndpoint test that validates endpoint format.
36+
// See:
37+
// https://github.com/apache/iceberg/blob/main/core/src/test/java/org/apache/iceberg/rest/responses/TestConfigResponseParser.java#L164
38+
// for reference.
39+
return {};
40+
}
41+
42+
Status Validator::Validate(const ErrorModel& error) {
43+
if (error.message.empty() || error.type.empty()) {
44+
return Invalid("Invalid error model: missing required fields");
45+
}
46+
47+
if (error.code < 400 || error.code > 600) {
48+
return Invalid("Invalid error model: code {} is out of range [400, 600]", error.code);
49+
}
50+
51+
// stack is optional, no validation needed
52+
return {};
53+
}
54+
55+
// We don't validate the error field because ErrorModel::Validate has been called in the
56+
// FromJson.
57+
Status Validator::Validate(const ErrorResponse& response) { return {}; }
58+
59+
// Namespace operations
60+
61+
Status Validator::Validate(const ListNamespacesResponse& response) { return {}; }
62+
63+
Status Validator::Validate(const CreateNamespaceRequest& request) { return {}; }
64+
65+
Status Validator::Validate(const CreateNamespaceResponse& response) { return {}; }
66+
67+
Status Validator::Validate(const GetNamespaceResponse& response) { return {}; }
68+
69+
Status Validator::Validate(const UpdateNamespacePropertiesRequest& request) {
70+
// keys in updates and removals must not overlap
71+
if (request.removals.empty() || request.updates.empty()) {
72+
return {};
73+
}
74+
75+
auto extract_and_sort = [](const auto& container, auto key_extractor) {
76+
std::vector<std::string_view> result;
77+
result.reserve(container.size());
78+
for (const auto& item : container) {
79+
result.push_back(std::string_view{key_extractor(item)});
80+
}
81+
std::ranges::sort(result);
82+
return result;
83+
};
84+
85+
auto sorted_removals =
86+
extract_and_sort(request.removals, [](const auto& s) -> const auto& { return s; });
87+
auto sorted_update_keys = extract_and_sort(
88+
request.updates, [](const auto& pair) -> const auto& { return pair.first; });
89+
90+
std::vector<std::string_view> common;
91+
std::ranges::set_intersection(sorted_removals, sorted_update_keys,
92+
std::back_inserter(common));
93+
94+
if (!common.empty()) {
95+
return Invalid(
96+
"Invalid namespace update: cannot simultaneously set and remove keys: {}",
97+
common);
98+
}
99+
return {};
100+
}
101+
102+
Status Validator::Validate(const UpdateNamespacePropertiesResponse& response) {
103+
return {};
104+
}
105+
106+
// Table operations
107+
108+
Status Validator::Validate(const ListTablesResponse& response) { return {}; }
109+
110+
Status Validator::Validate(const LoadTableResult& result) {
111+
if (!result.metadata) {
112+
return Invalid("Invalid metadata: null");
113+
}
114+
return {};
115+
}
116+
117+
Status Validator::Validate(const RegisterTableRequest& request) {
118+
if (request.name.empty()) {
119+
return Invalid("Missing table name");
120+
}
121+
122+
if (request.metadata_location.empty()) {
123+
return Invalid("Empty metadata location");
124+
}
125+
126+
return {};
127+
}
128+
129+
Status Validator::Validate(const RenameTableRequest& request) {
130+
ICEBERG_RETURN_UNEXPECTED(Validate(request.source));
131+
ICEBERG_RETURN_UNEXPECTED(Validate(request.destination));
132+
return {};
133+
}
134+
135+
Status Validator::Validate(const TableIdentifier& identifier) {
136+
if (identifier.name.empty()) {
137+
return Invalid("Invalid table identifier: missing table name");
138+
}
139+
return {};
140+
}
141+
142+
} // namespace iceberg::rest

0 commit comments

Comments
 (0)