Skip to content

Commit a3c0d3c

Browse files
authored
feat(rest): respect server-provided endpoints (#406)
1 parent 1c67e4c commit a3c0d3c

16 files changed

+717
-50
lines changed

src/iceberg/catalog/rest/CMakeLists.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@
1616
# under the License.
1717

1818
set(ICEBERG_REST_SOURCES
19-
rest_catalog.cc
2019
catalog_properties.cc
20+
endpoint.cc
2121
error_handlers.cc
2222
http_client.cc
2323
json_internal.cc
2424
resource_paths.cc
25+
rest_catalog.cc
2526
rest_util.cc)
2627

2728
set(ICEBERG_REST_STATIC_BUILD_INTERFACE_LIBS)
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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/endpoint.h"
21+
22+
#include <format>
23+
#include <string_view>
24+
25+
namespace iceberg::rest {
26+
27+
constexpr std::string_view ToString(HttpMethod method) {
28+
switch (method) {
29+
case HttpMethod::kGet:
30+
return "GET";
31+
case HttpMethod::kPost:
32+
return "POST";
33+
case HttpMethod::kPut:
34+
return "PUT";
35+
case HttpMethod::kDelete:
36+
return "DELETE";
37+
case HttpMethod::kHead:
38+
return "HEAD";
39+
}
40+
return "UNKNOWN";
41+
}
42+
43+
Result<Endpoint> Endpoint::Make(HttpMethod method, std::string_view path) {
44+
if (path.empty()) {
45+
return InvalidArgument("Endpoint cannot have empty path");
46+
}
47+
return Endpoint(method, path);
48+
}
49+
50+
Result<Endpoint> Endpoint::FromString(std::string_view str) {
51+
auto space_pos = str.find(' ');
52+
if (space_pos == std::string_view::npos ||
53+
str.find(' ', space_pos + 1) != std::string_view::npos) {
54+
return InvalidArgument(
55+
"Invalid endpoint format (must consist of two elements separated by a single "
56+
"space): '{}'",
57+
str);
58+
}
59+
60+
auto method_str = str.substr(0, space_pos);
61+
auto path_str = str.substr(space_pos + 1);
62+
63+
if (path_str.empty()) {
64+
return InvalidArgument("Invalid endpoint format: path is empty");
65+
}
66+
67+
// Parse HTTP method
68+
HttpMethod method;
69+
if (method_str == "GET") {
70+
method = HttpMethod::kGet;
71+
} else if (method_str == "POST") {
72+
method = HttpMethod::kPost;
73+
} else if (method_str == "PUT") {
74+
method = HttpMethod::kPut;
75+
} else if (method_str == "DELETE") {
76+
method = HttpMethod::kDelete;
77+
} else if (method_str == "HEAD") {
78+
method = HttpMethod::kHead;
79+
} else {
80+
return InvalidArgument("Invalid HTTP method: '{}'", method_str);
81+
}
82+
83+
return Make(method, std::string(path_str));
84+
}
85+
86+
std::string Endpoint::ToString() const {
87+
return std::format("{} {}", rest::ToString(method_), path_);
88+
}
89+
90+
} // namespace iceberg::rest
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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+
#pragma once
21+
22+
#include <string>
23+
#include <string_view>
24+
25+
#include "iceberg/catalog/rest/iceberg_rest_export.h"
26+
#include "iceberg/result.h"
27+
28+
/// \file iceberg/catalog/rest/endpoint.h
29+
/// Endpoint definitions for Iceberg REST API operations.
30+
31+
namespace iceberg::rest {
32+
33+
/// \brief HTTP method enumeration.
34+
enum class HttpMethod : uint8_t { kGet, kPost, kPut, kDelete, kHead };
35+
36+
/// \brief Convert HttpMethod to string representation.
37+
constexpr std::string_view ToString(HttpMethod method);
38+
39+
/// \brief An Endpoint is an immutable value object identifying a specific REST API
40+
/// operation. It consists of:
41+
/// - HTTP method (GET, POST, DELETE, etc.)
42+
/// - Path template (e.g., "/v1/{prefix}/namespaces/{namespace}")
43+
class ICEBERG_REST_EXPORT Endpoint {
44+
public:
45+
/// \brief Make an endpoint with method and path template.
46+
///
47+
/// \param method HTTP method (GET, POST, etc.)
48+
/// \param path Path template with placeholders (e.g., "/v1/{prefix}/tables")
49+
/// \return Endpoint instance or error if invalid
50+
static Result<Endpoint> Make(HttpMethod method, std::string_view path);
51+
52+
/// \brief Parse endpoint from string representation. "METHOD" have to be all
53+
/// upper-cased.
54+
///
55+
/// \param str String in format "METHOD /path/template" (e.g., "GET /v1/namespaces")
56+
/// \return Endpoint instance or error if malformed.
57+
static Result<Endpoint> FromString(std::string_view str);
58+
59+
/// \brief Get the HTTP method.
60+
constexpr HttpMethod method() const { return method_; }
61+
62+
/// \brief Get the path template.
63+
std::string_view path() const { return path_; }
64+
65+
/// \brief Serialize to "METHOD /path" format.
66+
std::string ToString() const;
67+
68+
constexpr bool operator==(const Endpoint& other) const {
69+
return method_ == other.method_ && path_ == other.path_;
70+
}
71+
72+
// Namespace endpoints
73+
static Endpoint ListNamespaces() {
74+
return {HttpMethod::kGet, "/v1/{prefix}/namespaces"};
75+
}
76+
static Endpoint GetNamespaceProperties() {
77+
return {HttpMethod::kGet, "/v1/{prefix}/namespaces/{namespace}"};
78+
}
79+
static Endpoint NamespaceExists() {
80+
return {HttpMethod::kHead, "/v1/{prefix}/namespaces/{namespace}"};
81+
}
82+
static Endpoint CreateNamespace() {
83+
return {HttpMethod::kPost, "/v1/{prefix}/namespaces"};
84+
}
85+
static Endpoint UpdateNamespace() {
86+
return {HttpMethod::kPost, "/v1/{prefix}/namespaces/{namespace}/properties"};
87+
}
88+
static Endpoint DropNamespace() {
89+
return {HttpMethod::kDelete, "/v1/{prefix}/namespaces/{namespace}"};
90+
}
91+
92+
// Table endpoints
93+
static Endpoint ListTables() {
94+
return {HttpMethod::kGet, "/v1/{prefix}/namespaces/{namespace}/tables"};
95+
}
96+
static Endpoint LoadTable() {
97+
return {HttpMethod::kGet, "/v1/{prefix}/namespaces/{namespace}/tables/{table}"};
98+
}
99+
static Endpoint TableExists() {
100+
return {HttpMethod::kHead, "/v1/{prefix}/namespaces/{namespace}/tables/{table}"};
101+
}
102+
static Endpoint CreateTable() {
103+
return {HttpMethod::kPost, "/v1/{prefix}/namespaces/{namespace}/tables"};
104+
}
105+
static Endpoint UpdateTable() {
106+
return {HttpMethod::kPost, "/v1/{prefix}/namespaces/{namespace}/tables/{table}"};
107+
}
108+
static Endpoint DeleteTable() {
109+
return {HttpMethod::kDelete, "/v1/{prefix}/namespaces/{namespace}/tables/{table}"};
110+
}
111+
static Endpoint RenameTable() {
112+
return {HttpMethod::kPost, "/v1/{prefix}/tables/rename"};
113+
}
114+
static Endpoint RegisterTable() {
115+
return {HttpMethod::kPost, "/v1/{prefix}/namespaces/{namespace}/register"};
116+
}
117+
static Endpoint ReportMetrics() {
118+
return {HttpMethod::kPost,
119+
"/v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics"};
120+
}
121+
static Endpoint TableCredentials() {
122+
return {HttpMethod::kGet,
123+
"/v1/{prefix}/namespaces/{namespace}/tables/{table}/credentials"};
124+
}
125+
126+
// Transaction endpoints
127+
static Endpoint CommitTransaction() {
128+
return {HttpMethod::kPost, "/v1/{prefix}/transactions/commit"};
129+
}
130+
131+
private:
132+
Endpoint(HttpMethod method, std::string_view path) : method_(method), path_(path) {}
133+
134+
HttpMethod method_;
135+
std::string path_;
136+
};
137+
138+
} // namespace iceberg::rest
139+
140+
// Specialize std::hash for Endpoint
141+
namespace std {
142+
template <>
143+
struct hash<iceberg::rest::Endpoint> {
144+
std::size_t operator()(const iceberg::rest::Endpoint& endpoint) const noexcept {
145+
std::size_t h1 = std::hash<int32_t>{}(static_cast<int32_t>(endpoint.method()));
146+
std::size_t h2 = std::hash<std::string_view>{}(endpoint.path());
147+
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
148+
}
149+
};
150+
} // namespace std

src/iceberg/catalog/rest/json_internal.cc

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ nlohmann::json ToJson(const CatalogConfig& config) {
7373
nlohmann::json json;
7474
json[kOverrides] = config.overrides;
7575
json[kDefaults] = config.defaults;
76-
SetContainerField(json, kEndpoints, config.endpoints);
76+
for (const auto& endpoint : config.endpoints) {
77+
json[kEndpoints].emplace_back(endpoint.ToString());
78+
}
7779
return json;
7880
}
7981

@@ -85,8 +87,16 @@ Result<CatalogConfig> CatalogConfigFromJson(const nlohmann::json& json) {
8587
ICEBERG_ASSIGN_OR_RAISE(
8688
config.defaults, GetJsonValueOrDefault<decltype(config.defaults)>(json, kDefaults));
8789
ICEBERG_ASSIGN_OR_RAISE(
88-
config.endpoints,
89-
GetJsonValueOrDefault<std::vector<std::string>>(json, kEndpoints));
90+
auto endpoints, GetJsonValueOrDefault<std::vector<std::string>>(json, kEndpoints));
91+
config.endpoints.reserve(endpoints.size());
92+
for (const auto& endpoint_str : endpoints) {
93+
auto endpoint_result = Endpoint::FromString(endpoint_str);
94+
if (!endpoint_result.has_value()) {
95+
// Convert to JsonParseError in JSON deserialization context
96+
return JsonParseError("{}", endpoint_result.error().message);
97+
}
98+
config.endpoints.emplace_back(std::move(endpoint_result.value()));
99+
}
90100
ICEBERG_RETURN_UNEXPECTED(config.Validate());
91101
return config;
92102
}

src/iceberg/catalog/rest/meson.build

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
iceberg_rest_sources = files(
1919
'catalog_properties.cc',
20+
'endpoint.cc',
2021
'error_handlers.cc',
2122
'http_client.cc',
2223
'json_internal.cc',
@@ -58,6 +59,7 @@ install_headers(
5859
[
5960
'catalog_properties.h',
6061
'constant.h',
62+
'endpoint.h',
6163
'error_handlers.h',
6264
'http_client.h',
6365
'iceberg_rest_export.h',

0 commit comments

Comments
 (0)