Skip to content

Commit 9f15a10

Browse files
committed
feat: add config, error and validation for rest types
1 parent 0dbb593 commit 9f15a10

File tree

8 files changed

+558
-5
lines changed

8 files changed

+558
-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: 79 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;
@@ -77,6 +146,7 @@ Result<CreateNamespaceRequest> CreateNamespaceRequestFromJson(
77146
ICEBERG_ASSIGN_OR_RAISE(
78147
request.properties,
79148
GetJsonValueOrDefault<decltype(request.properties)>(json, kProperties));
149+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
80150
return request;
81151
}
82152

@@ -94,6 +164,7 @@ Result<UpdateNamespacePropertiesRequest> UpdateNamespacePropertiesRequestFromJso
94164
request.removals, GetJsonValueOrDefault<std::vector<std::string>>(json, kRemovals));
95165
ICEBERG_ASSIGN_OR_RAISE(
96166
request.updates, GetJsonValueOrDefault<decltype(request.updates)>(json, kUpdates));
167+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
97168
return request;
98169
}
99170

@@ -114,6 +185,7 @@ Result<RegisterTableRequest> RegisterTableRequestFromJson(const nlohmann::json&
114185
GetJsonValue<std::string>(json, kMetadataLocation));
115186
ICEBERG_ASSIGN_OR_RAISE(request.overwrite,
116187
GetJsonValueOrDefault<bool>(json, kOverwrite, false));
188+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
117189
return request;
118190
}
119191

@@ -131,6 +203,7 @@ Result<RenameTableRequest> RenameTableRequestFromJson(const nlohmann::json& json
131203
ICEBERG_ASSIGN_OR_RAISE(auto dest_json,
132204
GetJsonValue<nlohmann::json>(json, kDestination));
133205
ICEBERG_ASSIGN_OR_RAISE(request.destination, TableIdentifierFromJson(dest_json));
206+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(request));
134207
return request;
135208
}
136209

@@ -177,6 +250,7 @@ Result<ListNamespacesResponse> ListNamespacesResponseFromJson(
177250
ICEBERG_ASSIGN_OR_RAISE(auto ns, NamespaceFromJson(ns_json));
178251
response.namespaces.push_back(std::move(ns));
179252
}
253+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
180254
return response;
181255
}
182256

@@ -232,6 +306,7 @@ Result<UpdateNamespacePropertiesResponse> UpdateNamespacePropertiesResponseFromJ
232306
response.removed, GetJsonValueOrDefault<std::vector<std::string>>(json, kRemoved));
233307
ICEBERG_ASSIGN_OR_RAISE(
234308
response.missing, GetJsonValueOrDefault<std::vector<std::string>>(json, kMissing));
309+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
235310
return response;
236311
}
237312

@@ -256,6 +331,7 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const nlohmann::json& json
256331
ICEBERG_ASSIGN_OR_RAISE(auto identifier, TableIdentifierFromJson(id_json));
257332
response.identifiers.push_back(std::move(identifier));
258333
}
334+
ICEBERG_RETURN_UNEXPECTED(Validator::Validate(response));
259335
return response;
260336
}
261337

@@ -265,6 +341,9 @@ Result<ListTablesResponse> ListTablesResponseFromJson(const nlohmann::json& json
265341
return Model##FromJson(json); \
266342
}
267343

344+
ICEBERG_DEFINE_FROM_JSON(CatalogConfig)
345+
ICEBERG_DEFINE_FROM_JSON(ErrorModel)
346+
ICEBERG_DEFINE_FROM_JSON(ErrorResponse)
268347
ICEBERG_DEFINE_FROM_JSON(ListNamespacesResponse)
269348
ICEBERG_DEFINE_FROM_JSON(CreateNamespaceRequest)
270349
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> 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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
#include <utility>
26+
27+
#include "iceberg/catalog/rest/types.h"
28+
#include "iceberg/result.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()) [[unlikely]] {
44+
return Invalid("Invalid error model: missing required fields");
45+
}
46+
47+
if (error.code < 400 || error.code > 600) [[unlikely]] {
48+
return Invalid("Invalid error model: code must be between 400 and 600");
49+
}
50+
51+
// stack is optional, no validation needed
52+
return {};
53+
}
54+
55+
Status Validator::Validate(const ErrorResponse& response) { return {}; }
56+
57+
// Namespace operations
58+
59+
Status Validator::Validate(const ListNamespacesResponse& response) { return {}; }
60+
61+
Status Validator::Validate(const CreateNamespaceRequest& request) { return {}; }
62+
63+
Status Validator::Validate(const CreateNamespaceResponse& response) { return {}; }
64+
65+
Status Validator::Validate(const GetNamespaceResponse& response) { return {}; }
66+
67+
Status Validator::Validate(const UpdateNamespacePropertiesRequest& request) {
68+
// keys in updates and removals must not overlap
69+
if (request.removals.empty() || request.updates.empty()) [[unlikely]] {
70+
return {};
71+
}
72+
73+
std::unordered_set<std::string> remove_set(request.removals.begin(),
74+
request.removals.end());
75+
std::vector<std::string> common;
76+
77+
for (const std::string& k : request.updates | std::views::keys) {
78+
if (remove_set.contains(k)) {
79+
common.push_back(k);
80+
}
81+
}
82+
83+
if (!common.empty()) {
84+
std::string keys;
85+
bool first = true;
86+
for (const std::string& s : common) {
87+
if (!std::exchange(first, false)) keys += ", ";
88+
keys += s;
89+
}
90+
91+
return Invalid(
92+
"Invalid namespace properties update: cannot simultaneously set and remove keys: "
93+
"[{}]",
94+
keys);
95+
}
96+
return {};
97+
}
98+
99+
Status Validator::Validate(const UpdateNamespacePropertiesResponse& response) {
100+
return {};
101+
}
102+
103+
// Table operations
104+
105+
Status Validator::Validate(const ListTablesResponse& response) { return {}; }
106+
107+
Status Validator::Validate(const LoadTableResult& result) {
108+
if (!result.metadata) [[unlikely]] {
109+
return Invalid("Invalid metadata: null");
110+
}
111+
return {};
112+
}
113+
114+
Status Validator::Validate(const RegisterTableRequest& request) {
115+
if (request.name.empty()) [[unlikely]] {
116+
return Invalid("Invalid table name: empty");
117+
}
118+
119+
if (request.metadata_location.empty()) [[unlikely]] {
120+
return Invalid("Invalid metadata location: empty");
121+
}
122+
123+
return {};
124+
}
125+
126+
Status Validator::Validate(const RenameTableRequest& request) {
127+
if (request.source.ns.levels.empty() || request.source.name.empty()) [[unlikely]] {
128+
return Invalid("Invalid source identifier");
129+
}
130+
131+
if (request.destination.ns.levels.empty() || request.destination.name.empty())
132+
[[unlikely]] {
133+
return Invalid("Invalid destination identifier");
134+
}
135+
136+
return {};
137+
}
138+
139+
} // namespace iceberg::rest

0 commit comments

Comments
 (0)