Skip to main content

iceberg_catalog_rest/
types.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Request and response types for the Iceberg REST API.
19
20use std::collections::HashMap;
21
22use iceberg::spec::{Schema, SortOrder, TableMetadata, UnboundPartitionSpec};
23use iceberg::{
24    Error, ErrorKind, Namespace, NamespaceIdent, TableIdent, TableRequirement, TableUpdate,
25};
26use serde_derive::{Deserialize, Serialize};
27
28use crate::endpoint::Endpoint;
29
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub(super) struct CatalogConfig {
32    pub(super) overrides: HashMap<String, String>,
33    pub(super) defaults: HashMap<String, String>,
34    /// Endpoints the server advertises support for; `None` when the field is
35    /// absent.
36    pub(super) endpoints: Option<Vec<Endpoint>>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40/// Wrapper for all non-2xx error responses from the REST API
41pub struct ErrorResponse {
42    error: ErrorModel,
43}
44
45impl From<ErrorResponse> for Error {
46    fn from(resp: ErrorResponse) -> Error {
47        resp.error.into()
48    }
49}
50
51#[derive(Debug, Serialize, Deserialize)]
52/// Error payload returned in a response with further details on the error
53pub struct ErrorModel {
54    /// Human-readable error message
55    pub message: String,
56    /// Internal type definition of the error
57    pub r#type: String,
58    /// HTTP response code
59    pub code: u16,
60    /// Optional error stack / context
61    pub stack: Option<Vec<String>>,
62}
63
64impl From<ErrorModel> for Error {
65    fn from(value: ErrorModel) -> Self {
66        let mut error = Error::new(ErrorKind::DataInvalid, value.message)
67            .with_context("type", value.r#type)
68            .with_context("code", format!("{}", value.code));
69
70        if let Some(stack) = value.stack {
71            error = error.with_context("stack", stack.join("\n"));
72        }
73
74        error
75    }
76}
77
78#[derive(Debug, Serialize, Deserialize)]
79pub(super) struct OAuthError {
80    pub(super) error: String,
81    pub(super) error_description: Option<String>,
82    pub(super) error_uri: Option<String>,
83}
84
85impl From<OAuthError> for Error {
86    fn from(value: OAuthError) -> Self {
87        let mut error = Error::new(
88            ErrorKind::DataInvalid,
89            format!("OAuthError: {}", value.error),
90        );
91
92        if let Some(desc) = value.error_description {
93            error = error.with_context("description", desc);
94        }
95
96        if let Some(uri) = value.error_uri {
97            error = error.with_context("uri", uri);
98        }
99
100        error
101    }
102}
103
104#[derive(Debug, Serialize, Deserialize)]
105pub(super) struct TokenResponse {
106    pub(super) access_token: String,
107    pub(super) token_type: String,
108    pub(super) expires_in: Option<u64>,
109    pub(super) issued_token_type: Option<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113/// Namespace response
114pub struct NamespaceResponse {
115    /// Namespace identifier
116    pub namespace: NamespaceIdent,
117    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
118    /// Properties stored on the namespace, if supported by the server.
119    pub properties: HashMap<String, String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123/// Create namespace request
124pub struct CreateNamespaceRequest {
125    /// Name of the namespace to create
126    pub namespace: NamespaceIdent,
127    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
128    /// Properties to set on the namespace
129    pub properties: HashMap<String, String>,
130}
131
132impl From<&Namespace> for NamespaceResponse {
133    fn from(value: &Namespace) -> Self {
134        Self {
135            namespace: value.name().clone(),
136            properties: value.properties().clone(),
137        }
138    }
139}
140
141impl From<NamespaceResponse> for Namespace {
142    fn from(value: NamespaceResponse) -> Self {
143        Namespace::with_properties(value.namespace, value.properties)
144    }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(rename_all = "kebab-case")]
149/// Response containing a list of namespace identifiers, with optional pagination support.
150pub struct ListNamespaceResponse {
151    /// List of namespace identifiers returned by the server
152    pub namespaces: Vec<NamespaceIdent>,
153    /// Opaque token for pagination. If present, indicates there are more results available.
154    /// Use this value in subsequent requests to retrieve the next page.
155    pub next_page_token: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159/// Request to update properties on a namespace.
160///
161/// Properties that are not in the request are not modified or removed by this call.
162/// Server implementations are not required to support namespace properties.
163pub struct UpdateNamespacePropertiesRequest {
164    /// List of property keys to remove from the namespace
165    pub removals: Option<Vec<String>>,
166    /// Map of property keys to values to set or update on the namespace
167    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
168    pub updates: HashMap<String, String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
172/// Response from updating namespace properties, indicating which properties were changed.
173pub struct UpdateNamespacePropertiesResponse {
174    /// List of property keys that were added or updated
175    pub updated: Vec<String>,
176    /// List of properties that were removed
177    pub removed: Vec<String>,
178    /// List of properties requested for removal that were not found in the namespace's properties.
179    /// Represents a partial success response. Servers do not need to implement this.
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub missing: Option<Vec<String>>,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
185#[serde(rename_all = "kebab-case")]
186/// Response containing a list of table identifiers, with optional pagination support.
187pub struct ListTablesResponse {
188    /// List of table identifiers under the requested namespace
189    pub identifiers: Vec<TableIdent>,
190    /// Opaque token for pagination. If present, indicates there are more results available.
191    /// Use this value in subsequent requests to retrieve the next page.
192    #[serde(default)]
193    pub next_page_token: Option<String>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
197/// Request to rename a table from one identifier to another.
198///
199/// It's valid to move a table across namespaces, but the server implementation
200/// is not required to support it.
201pub struct RenameTableRequest {
202    /// Current table identifier to rename
203    pub source: TableIdent,
204    /// New table identifier to rename to
205    pub destination: TableIdent,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(rename_all = "kebab-case")]
210/// Result returned when a table is successfully loaded or created.
211///
212/// The table metadata JSON is returned in the `metadata` field. The corresponding file location
213/// of table metadata should be returned in the `metadata_location` field, unless the metadata
214/// is not yet committed. For example, a create transaction may return metadata that is staged
215/// but not committed.
216///
217/// The `config` map returns table-specific configuration for the table's resources, including
218/// its HTTP client and FileIO. For example, config may contain a specific FileIO implementation
219/// class for the table depending on its underlying storage.
220pub struct LoadTableResult {
221    /// May be null if the table is staged as part of a transaction
222    pub metadata_location: Option<String>,
223    /// The table's full metadata
224    pub metadata: TableMetadata,
225    /// Table-specific configuration overriding catalog configuration
226    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
227    pub config: HashMap<String, String>,
228    /// Storage credentials for accessing table data. Clients should check this field
229    /// before falling back to credentials in the `config` field.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub storage_credentials: Option<Vec<StorageCredential>>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235/// Storage credential for a specific location prefix.
236///
237/// Indicates a storage location prefix where the credential is relevant. Clients should
238/// choose the most specific prefix (by selecting the longest prefix) if several credentials
239/// of the same type are available.
240pub struct StorageCredential {
241    /// Storage location prefix where this credential is relevant
242    pub prefix: String,
243    /// Configuration map containing credential information
244    pub config: HashMap<String, String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "kebab-case")]
249/// Request to create a new table in a namespace.
250///
251/// If `stage_create` is false, the table is created immediately.
252/// If `stage_create` is true, the table is not created, but table metadata is initialized
253/// and returned. The service should prepare as needed for a commit to the table commit
254/// endpoint to complete the create transaction.
255pub struct CreateTableRequest {
256    /// Name of the table to create
257    pub name: String,
258    /// Optional table location. If not provided, the server will choose a location.
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub location: Option<String>,
261    /// Table schema
262    pub schema: Schema,
263    /// Optional partition specification. If not provided, the table will be unpartitioned.
264    #[serde(skip_serializing_if = "Option::is_none")]
265    pub partition_spec: Option<UnboundPartitionSpec>,
266    /// Optional sort order for the table
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub write_order: Option<SortOrder>,
269    /// Whether to stage the create for a transaction (true) or create immediately (false)
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub stage_create: Option<bool>,
272    /// Optional properties to set on the table
273    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
274    pub properties: HashMap<String, String>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
278/// Request to commit updates to a table.
279///
280/// Commits have two parts: requirements and updates. Requirements are assertions that will
281/// be validated before attempting to make and commit changes. Updates are changes to make
282/// to table metadata.
283///
284/// Create table transactions that are started by createTable with `stage-create` set to true
285/// are committed using this request. Transactions should include all changes to the table,
286/// including table initialization, like AddSchemaUpdate and SetCurrentSchemaUpdate.
287pub struct CommitTableRequest {
288    /// Table identifier to update; must be present for CommitTransactionRequest
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub identifier: Option<TableIdent>,
291    /// List of requirements that must be satisfied before committing changes
292    pub requirements: Vec<TableRequirement>,
293    /// List of updates to apply to the table metadata
294    pub updates: Vec<TableUpdate>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
298#[serde(rename_all = "kebab-case")]
299/// Response returned when a table is successfully updated.
300///
301/// The table metadata JSON is returned in the metadata field. The corresponding file location
302/// of table metadata must be returned in the metadata-location field. Clients can check whether
303/// metadata has changed by comparing metadata locations.
304pub struct CommitTableResponse {
305    /// Location of the updated table metadata file
306    pub metadata_location: String,
307    /// The table's updated metadata
308    pub metadata: TableMetadata,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "kebab-case")]
313/// Request to register a table using an existing metadata file location.
314pub struct RegisterTableRequest {
315    /// Name of the table to register
316    pub name: String,
317    /// Location of the metadata file for the table
318    pub metadata_location: String,
319    /// Whether to overwrite table metadata if the table already exists
320    pub overwrite: Option<bool>,
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_namespace_response_serde() {
329        let json = serde_json::json!({
330            "namespace": ["nested", "ns"],
331            "properties": {
332                "key1": "value1",
333                "key2": "value2"
334            }
335        });
336        let ns_response: NamespaceResponse =
337            serde_json::from_value(json.clone()).expect("Deserialization failed");
338        assert_eq!(ns_response, NamespaceResponse {
339            namespace: NamespaceIdent::from_vec(vec!["nested".to_string(), "ns".to_string()])
340                .unwrap(),
341            properties: HashMap::from([
342                ("key1".to_string(), "value1".to_string()),
343                ("key2".to_string(), "value2".to_string()),
344            ]),
345        });
346        assert_eq!(
347            serde_json::to_value(&ns_response).expect("Serialization failed"),
348            json
349        );
350
351        // Without properties
352        let json_no_props = serde_json::json!({
353            "namespace": ["db", "schema"]
354        });
355        let ns_response_no_props: NamespaceResponse =
356            serde_json::from_value(json_no_props.clone()).expect("Deserialization failed");
357        assert_eq!(ns_response_no_props, NamespaceResponse {
358            namespace: NamespaceIdent::from_vec(vec!["db".to_string(), "schema".to_string()])
359                .unwrap(),
360            properties: HashMap::new(),
361        });
362        assert_eq!(
363            serde_json::to_value(&ns_response_no_props).expect("Serialization failed"),
364            json_no_props
365        );
366    }
367
368    fn test_create_table_request_schema() -> Schema {
369        serde_json::from_value(serde_json::json!({
370            "type": "struct",
371            "schema-id": 1,
372            "fields": [
373                {
374                    "id": 1,
375                    "name": "foo",
376                    "required": false,
377                    "type": "string"
378                },
379                {
380                    "id": 2,
381                    "name": "bar",
382                    "required": true,
383                    "type": "int"
384                }
385            ],
386            "identifier-field-ids": [2]
387        }))
388        .expect("Failed to deserialize test schema")
389    }
390
391    #[test]
392    fn test_create_table_request_minimal_serialization() {
393        let request = CreateTableRequest {
394            name: "tbl1".to_string(),
395            location: None,
396            schema: test_create_table_request_schema(),
397            partition_spec: None,
398            write_order: None,
399            stage_create: None,
400            properties: HashMap::new(),
401        };
402
403        let serialized = serde_json::to_value(&request).expect("Serialization failed");
404        let object = serialized.as_object().expect("Expected a JSON object");
405        assert!(object.contains_key("name"));
406        assert!(object.contains_key("schema"));
407        assert!(!object.contains_key("location"));
408        assert!(!object.contains_key("partition-spec"));
409        assert!(!object.contains_key("write-order"));
410        assert!(!object.contains_key("stage-create"));
411        assert!(!object.contains_key("properties"));
412    }
413
414    #[test]
415    fn test_create_table_request_full_serialization() {
416        let request: CreateTableRequest = serde_json::from_value(serde_json::json!({
417            "name": "tbl1",
418            "location": "s3://warehouse/tbl1",
419            "schema": test_create_table_request_schema(),
420            "partition-spec": {
421                "spec-id": 1,
422                "fields": [
423                    {
424                        "source-id": 2,
425                        "field-id": 1000,
426                        "name": "bar",
427                        "transform": "identity"
428                    }
429                ]
430            },
431            "write-order": {
432                "order-id": 1,
433                "fields": [
434                    {
435                        "transform": "identity",
436                        "source-id": 2,
437                        "direction": "asc",
438                        "null-order": "nulls-first"
439                    }
440                ]
441            },
442            "stage-create": true,
443            "properties": {
444                "owner": "test"
445            }
446        }))
447        .expect("Deserialization failed");
448
449        let serialized = serde_json::to_value(&request).expect("Serialization failed");
450        let object = serialized.as_object().expect("Expected a JSON object");
451        assert_eq!(
452            object.get("location"),
453            Some(&serde_json::json!("s3://warehouse/tbl1"))
454        );
455        assert!(object.contains_key("partition-spec"));
456        assert!(object.contains_key("write-order"));
457        assert_eq!(object.get("stage-create"), Some(&serde_json::json!(true)));
458        assert!(object.contains_key("properties"));
459    }
460
461    #[test]
462    fn test_create_table_request_deserialize_explicit_nulls() {
463        let request: CreateTableRequest = serde_json::from_value(serde_json::json!({
464            "name": "tbl1",
465            "location": null,
466            "schema": test_create_table_request_schema(),
467            "partition-spec": null,
468            "write-order": null,
469            "stage-create": null
470        }))
471        .expect("Deserialization failed");
472
473        assert_eq!(request.name, "tbl1");
474        assert_eq!(request.location, None);
475        assert_eq!(request.partition_spec, None);
476        assert_eq!(request.write_order, None);
477        assert_eq!(request.stage_create, None);
478        assert!(request.properties.is_empty());
479    }
480
481    #[test]
482    fn config_parses_advertised_endpoints() {
483        let json = r#"{"overrides":{},"defaults":{},
484            "endpoints":["GET /v1/{prefix}/namespaces","POST /v1/{prefix}/namespaces/{namespace}/tables"]}"#;
485        let config: CatalogConfig = serde_json::from_str(json).unwrap();
486        let endpoints = config.endpoints.expect("endpoints should be present");
487        assert_eq!(endpoints.len(), 2);
488        assert!(endpoints.contains(&"GET /v1/{prefix}/namespaces".parse().unwrap()));
489    }
490
491    #[test]
492    fn config_without_endpoints_field_deserializes_to_none() {
493        // `Option<Vec<Endpoint>>` defaults to `None` for a missing field without
494        // an explicit `#[serde(default)]`.
495        let config: CatalogConfig =
496            serde_json::from_str(r#"{"overrides":{},"defaults":{}}"#).unwrap();
497        assert!(config.endpoints.is_none());
498    }
499
500    #[test]
501    fn malformed_endpoint_fails_config_parse() {
502        // A single malformed endpoint string fails the whole config parse,
503        // matching the Java reference (`ConfigResponseParser` rejects it rather
504        // than silently dropping it).
505        let json = r#"{"overrides":{},"defaults":{},"endpoints":["GET_v1/namespaces"]}"#;
506        assert!(serde_json::from_str::<CatalogConfig>(json).is_err());
507    }
508}