This is an automated email from the ASF dual-hosted git repository.

liurenjie1024 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new d973fefb0 feat: Make `rest` types public, add documentation (#1901)
d973fefb0 is described below

commit d973fefb06bfb3555890492fad853dabd85fb186
Author: Christian <[email protected]>
AuthorDate: Tue Dec 9 14:15:01 2025 +0100

    feat: Make `rest` types public, add documentation (#1901)
---
 crates/catalog/rest/src/catalog.rs |  52 +++----
 crates/catalog/rest/src/lib.rs     |   1 +
 crates/catalog/rest/src/types.rs   | 307 ++++++++++++++++++++++++++++---------
 3 files changed, 253 insertions(+), 107 deletions(-)

diff --git a/crates/catalog/rest/src/catalog.rs 
b/crates/catalog/rest/src/catalog.rs
index 39553f755..ddbf6a4e0 100644
--- a/crates/catalog/rest/src/catalog.rs
+++ b/crates/catalog/rest/src/catalog.rs
@@ -41,9 +41,9 @@ use crate::client::{
     HttpClient, deserialize_catalog_response, 
deserialize_unexpected_catalog_error,
 };
 use crate::types::{
-    CatalogConfig, CommitTableRequest, CommitTableResponse, CreateTableRequest,
-    ListNamespaceResponse, ListTableResponse, LoadTableResponse, 
NamespaceSerde,
-    RegisterTableRequest, RenameTableRequest,
+    CatalogConfig, CommitTableRequest, CommitTableResponse, 
CreateNamespaceRequest,
+    CreateTableRequest, ListNamespaceResponse, ListTablesResponse, 
LoadTableResult,
+    NamespaceResponse, RegisterTableRequest, RenameTableRequest,
 };
 
 /// REST catalog URI
@@ -466,13 +466,7 @@ impl Catalog for RestCatalog {
                         
deserialize_catalog_response::<ListNamespaceResponse>(http_response)
                             .await?;
 
-                    let ns_identifiers = response
-                        .namespaces
-                        .into_iter()
-                        .map(NamespaceIdent::from_vec)
-                        .collect::<Result<Vec<NamespaceIdent>>>()?;
-
-                    namespaces.extend(ns_identifiers);
+                    namespaces.extend(response.namespaces);
 
                     match response.next_page_token {
                         Some(token) => next_token = Some(token),
@@ -502,9 +496,9 @@ impl Catalog for RestCatalog {
         let request = context
             .client
             .request(Method::POST, context.config.namespaces_endpoint())
-            .json(&NamespaceSerde {
-                namespace: namespace.as_ref().clone(),
-                properties: Some(properties),
+            .json(&CreateNamespaceRequest {
+                namespace: namespace.clone(),
+                properties,
             })
             .build()?;
 
@@ -513,8 +507,8 @@ impl Catalog for RestCatalog {
         match http_response.status() {
             StatusCode::OK => {
                 let response =
-                    
deserialize_catalog_response::<NamespaceSerde>(http_response).await?;
-                Namespace::try_from(response)
+                    
deserialize_catalog_response::<NamespaceResponse>(http_response).await?;
+                Ok(Namespace::from(response))
             }
             StatusCode::CONFLICT => Err(Error::new(
                 ErrorKind::Unexpected,
@@ -537,8 +531,8 @@ impl Catalog for RestCatalog {
         match http_response.status() {
             StatusCode::OK => {
                 let response =
-                    
deserialize_catalog_response::<NamespaceSerde>(http_response).await?;
-                Namespace::try_from(response)
+                    
deserialize_catalog_response::<NamespaceResponse>(http_response).await?;
+                Ok(Namespace::from(response))
             }
             StatusCode::NOT_FOUND => Err(Error::new(
                 ErrorKind::Unexpected,
@@ -614,7 +608,7 @@ impl Catalog for RestCatalog {
             match http_response.status() {
                 StatusCode::OK => {
                     let response =
-                        
deserialize_catalog_response::<ListTableResponse>(http_response).await?;
+                        
deserialize_catalog_response::<ListTablesResponse>(http_response).await?;
 
                     identifiers.extend(response.identifiers);
 
@@ -661,11 +655,7 @@ impl Catalog for RestCatalog {
                 partition_spec: creation.partition_spec,
                 write_order: creation.sort_order,
                 stage_create: Some(false),
-                properties: if creation.properties.is_empty() {
-                    None
-                } else {
-                    Some(creation.properties)
-                },
+                properties: creation.properties,
             })
             .build()?;
 
@@ -673,7 +663,7 @@ impl Catalog for RestCatalog {
 
         let response = match http_response.status() {
             StatusCode::OK => {
-                
deserialize_catalog_response::<LoadTableResponse>(http_response).await?
+                
deserialize_catalog_response::<LoadTableResult>(http_response).await?
             }
             StatusCode::NOT_FOUND => {
                 return Err(Error::new(
@@ -697,7 +687,6 @@ impl Catalog for RestCatalog {
 
         let config = response
             .config
-            .unwrap_or_default()
             .into_iter()
             .chain(self.user_config.props.clone())
             .collect();
@@ -735,7 +724,7 @@ impl Catalog for RestCatalog {
 
         let response = match http_response.status() {
             StatusCode::OK | StatusCode::NOT_MODIFIED => {
-                
deserialize_catalog_response::<LoadTableResponse>(http_response).await?
+                
deserialize_catalog_response::<LoadTableResult>(http_response).await?
             }
             StatusCode::NOT_FOUND => {
                 return Err(Error::new(
@@ -748,7 +737,6 @@ impl Catalog for RestCatalog {
 
         let config = response
             .config
-            .unwrap_or_default()
             .into_iter()
             .chain(self.user_config.props.clone())
             .collect();
@@ -861,9 +849,9 @@ impl Catalog for RestCatalog {
 
         let http_response = context.client.query_catalog(request).await?;
 
-        let response: LoadTableResponse = match http_response.status() {
+        let response: LoadTableResult = match http_response.status() {
             StatusCode::OK => {
-                
deserialize_catalog_response::<LoadTableResponse>(http_response).await?
+                
deserialize_catalog_response::<LoadTableResult>(http_response).await?
             }
             StatusCode::NOT_FOUND => {
                 return Err(Error::new(
@@ -905,7 +893,7 @@ impl Catalog for RestCatalog {
                 context.config.table_endpoint(commit.identifier()),
             )
             .json(&CommitTableRequest {
-                identifier: commit.identifier().clone(),
+                identifier: Some(commit.identifier().clone()),
                 requirements: commit.take_requirements(),
                 updates: commit.take_updates(),
             })
@@ -2428,7 +2416,7 @@ mod tests {
             ))
             .unwrap();
             let reader = BufReader::new(file);
-            let resp = serde_json::from_reader::<_, 
LoadTableResponse>(reader).unwrap();
+            let resp = serde_json::from_reader::<_, 
LoadTableResult>(reader).unwrap();
 
             Table::builder()
                 .metadata(resp.metadata)
@@ -2568,7 +2556,7 @@ mod tests {
             ))
             .unwrap();
             let reader = BufReader::new(file);
-            let resp = serde_json::from_reader::<_, 
LoadTableResponse>(reader).unwrap();
+            let resp = serde_json::from_reader::<_, 
LoadTableResult>(reader).unwrap();
 
             Table::builder()
                 .metadata(resp.metadata)
diff --git a/crates/catalog/rest/src/lib.rs b/crates/catalog/rest/src/lib.rs
index 70cdeaabd..6bee95097 100644
--- a/crates/catalog/rest/src/lib.rs
+++ b/crates/catalog/rest/src/lib.rs
@@ -56,3 +56,4 @@ mod client;
 mod types;
 
 pub use catalog::*;
+pub use types::*;
diff --git a/crates/catalog/rest/src/types.rs b/crates/catalog/rest/src/types.rs
index 70ed72051..ab44c40ee 100644
--- a/crates/catalog/rest/src/types.rs
+++ b/crates/catalog/rest/src/types.rs
@@ -15,6 +15,8 @@
 // specific language governing permissions and limitations
 // under the License.
 
+//! Request and response types for the Iceberg REST API.
+
 use std::collections::HashMap;
 
 use iceberg::spec::{Schema, SortOrder, TableMetadata, UnboundPartitionSpec};
@@ -30,7 +32,8 @@ pub(super) struct CatalogConfig {
 }
 
 #[derive(Debug, Serialize, Deserialize)]
-pub(super) struct ErrorResponse {
+/// Wrapper for all non-2xx error responses from the REST API
+pub struct ErrorResponse {
     error: ErrorModel,
 }
 
@@ -41,11 +44,16 @@ impl From<ErrorResponse> for Error {
 }
 
 #[derive(Debug, Serialize, Deserialize)]
-pub(super) struct ErrorModel {
-    pub(super) message: String,
-    pub(super) r#type: String,
-    pub(super) code: u16,
-    pub(super) stack: Option<Vec<String>>,
+/// Error payload returned in a response with further details on the error
+pub struct ErrorModel {
+    /// Human-readable error message
+    pub message: String,
+    /// Internal type definition of the error
+    pub r#type: String,
+    /// HTTP response code
+    pub code: u16,
+    /// Optional error stack / context
+    pub stack: Option<Vec<String>>,
 }
 
 impl From<ErrorModel> for Error {
@@ -96,106 +104,255 @@ pub(super) struct TokenResponse {
     pub(super) issued_token_type: Option<String>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub(super) struct NamespaceSerde {
-    pub(super) namespace: Vec<String>,
-    pub(super) properties: Option<HashMap<String, String>>,
-}
-
-impl TryFrom<NamespaceSerde> for Namespace {
-    type Error = Error;
-    fn try_from(value: NamespaceSerde) -> std::result::Result<Self, 
Self::Error> {
-        Ok(Namespace::with_properties(
-            NamespaceIdent::from_vec(value.namespace)?,
-            value.properties.unwrap_or_default(),
-        ))
-    }
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Namespace response
+pub struct NamespaceResponse {
+    /// Namespace identifier
+    pub namespace: NamespaceIdent,
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    /// Properties stored on the namespace, if supported by the server.
+    pub properties: HashMap<String, String>,
 }
 
-impl From<&Namespace> for NamespaceSerde {
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Create namespace request
+pub struct CreateNamespaceRequest {
+    /// Name of the namespace to create
+    pub namespace: NamespaceIdent,
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    /// Properties to set on the namespace
+    pub properties: HashMap<String, String>,
+}
+
+impl From<&Namespace> for NamespaceResponse {
     fn from(value: &Namespace) -> Self {
         Self {
-            namespace: value.name().as_ref().clone(),
-            properties: Some(value.properties().clone()),
+            namespace: value.name().clone(),
+            properties: value.properties().clone(),
         }
     }
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+impl From<NamespaceResponse> for Namespace {
+    fn from(value: NamespaceResponse) -> Self {
+        Namespace::with_properties(value.namespace, value.properties)
+    }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
-pub(super) struct ListNamespaceResponse {
-    pub(super) namespaces: Vec<Vec<String>>,
-    #[serde(default)]
-    pub(super) next_page_token: Option<String>,
+/// Response containing a list of namespace identifiers, with optional 
pagination support.
+pub struct ListNamespaceResponse {
+    /// List of namespace identifiers returned by the server
+    pub namespaces: Vec<NamespaceIdent>,
+    /// Opaque token for pagination. If present, indicates there are more 
results available.
+    /// Use this value in subsequent requests to retrieve the next page.
+    pub next_page_token: Option<String>,
 }
 
-#[allow(dead_code)]
-#[derive(Debug, Serialize, Deserialize)]
-pub(super) struct UpdateNamespacePropsRequest {
-    removals: Option<Vec<String>>,
-    updates: Option<HashMap<String, String>>,
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Request to update properties on a namespace.
+///
+/// Properties that are not in the request are not modified or removed by this 
call.
+/// Server implementations are not required to support namespace properties.
+pub struct UpdateNamespacePropertiesRequest {
+    /// List of property keys to remove from the namespace
+    pub removals: Option<Vec<String>>,
+    /// Map of property keys to values to set or update on the namespace
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub updates: HashMap<String, String>,
 }
 
-#[allow(dead_code)]
-#[derive(Debug, Serialize, Deserialize)]
-pub(super) struct UpdateNamespacePropsResponse {
-    updated: Vec<String>,
-    removed: Vec<String>,
-    missing: Option<Vec<String>>,
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Response from updating namespace properties, indicating which properties 
were changed.
+pub struct UpdateNamespacePropertiesResponse {
+    /// List of property keys that were added or updated
+    pub updated: Vec<String>,
+    /// List of properties that were removed
+    pub removed: Vec<String>,
+    /// List of properties requested for removal that were not found in the 
namespace's properties.
+    /// Represents a partial success response. Servers do not need to 
implement this.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub missing: Option<Vec<String>>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
-pub(super) struct ListTableResponse {
-    pub(super) identifiers: Vec<TableIdent>,
+/// Response containing a list of table identifiers, with optional pagination 
support.
+pub struct ListTablesResponse {
+    /// List of table identifiers under the requested namespace
+    pub identifiers: Vec<TableIdent>,
+    /// Opaque token for pagination. If present, indicates there are more 
results available.
+    /// Use this value in subsequent requests to retrieve the next page.
     #[serde(default)]
-    pub(super) next_page_token: Option<String>,
+    pub next_page_token: Option<String>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub(super) struct RenameTableRequest {
-    pub(super) source: TableIdent,
-    pub(super) destination: TableIdent,
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Request to rename a table from one identifier to another.
+///
+/// It's valid to move a table across namespaces, but the server implementation
+/// is not required to support it.
+pub struct RenameTableRequest {
+    /// Current table identifier to rename
+    pub source: TableIdent,
+    /// New table identifier to rename to
+    pub destination: TableIdent,
 }
 
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
-pub(super) struct LoadTableResponse {
-    pub(super) metadata_location: Option<String>,
-    pub(super) metadata: TableMetadata,
-    pub(super) config: Option<HashMap<String, String>>,
+/// Result returned when a table is successfully loaded or created.
+///
+/// The table metadata JSON is returned in the `metadata` field. The 
corresponding file location
+/// of table metadata should be returned in the `metadata_location` field, 
unless the metadata
+/// is not yet committed. For example, a create transaction may return 
metadata that is staged
+/// but not committed.
+///
+/// The `config` map returns table-specific configuration for the table's 
resources, including
+/// its HTTP client and FileIO. For example, config may contain a specific 
FileIO implementation
+/// class for the table depending on its underlying storage.
+pub struct LoadTableResult {
+    /// May be null if the table is staged as part of a transaction
+    pub metadata_location: Option<String>,
+    /// The table's full metadata
+    pub metadata: TableMetadata,
+    /// Table-specific configuration overriding catalog configuration
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub config: HashMap<String, String>,
+    /// Storage credentials for accessing table data. Clients should check 
this field
+    /// before falling back to credentials in the `config` field.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub storage_credentials: Option<Vec<StorageCredential>>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+/// Storage credential for a specific location prefix.
+///
+/// Indicates a storage location prefix where the credential is relevant. 
Clients should
+/// choose the most specific prefix (by selecting the longest prefix) if 
several credentials
+/// of the same type are available.
+pub struct StorageCredential {
+    /// Storage location prefix where this credential is relevant
+    pub prefix: String,
+    /// Configuration map containing credential information
+    pub config: HashMap<String, String>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
-pub(super) struct CreateTableRequest {
-    pub(super) name: String,
-    pub(super) location: Option<String>,
-    pub(super) schema: Schema,
-    pub(super) partition_spec: Option<UnboundPartitionSpec>,
-    pub(super) write_order: Option<SortOrder>,
-    pub(super) stage_create: Option<bool>,
-    pub(super) properties: Option<HashMap<String, String>>,
+/// Request to create a new table in a namespace.
+///
+/// If `stage_create` is false, the table is created immediately.
+/// If `stage_create` is true, the table is not created, but table metadata is 
initialized
+/// and returned. The service should prepare as needed for a commit to the 
table commit
+/// endpoint to complete the create transaction.
+pub struct CreateTableRequest {
+    /// Name of the table to create
+    pub name: String,
+    /// Optional table location. If not provided, the server will choose a 
location.
+    pub location: Option<String>,
+    /// Table schema
+    pub schema: Schema,
+    /// Optional partition specification. If not provided, the table will be 
unpartitioned.
+    pub partition_spec: Option<UnboundPartitionSpec>,
+    /// Optional sort order for the table
+    pub write_order: Option<SortOrder>,
+    /// Whether to stage the create for a transaction (true) or create 
immediately (false)
+    pub stage_create: Option<bool>,
+    /// Optional properties to set on the table
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub properties: HashMap<String, String>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
-pub(super) struct CommitTableRequest {
-    pub(super) identifier: TableIdent,
-    pub(super) requirements: Vec<TableRequirement>,
-    pub(super) updates: Vec<TableUpdate>,
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+/// Request to commit updates to a table.
+///
+/// Commits have two parts: requirements and updates. Requirements are 
assertions that will
+/// be validated before attempting to make and commit changes. Updates are 
changes to make
+/// to table metadata.
+///
+/// Create table transactions that are started by createTable with 
`stage-create` set to true
+/// are committed using this request. Transactions should include all changes 
to the table,
+/// including table initialization, like AddSchemaUpdate and 
SetCurrentSchemaUpdate.
+pub struct CommitTableRequest {
+    /// Table identifier to update; must be present for 
CommitTransactionRequest
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub identifier: Option<TableIdent>,
+    /// List of requirements that must be satisfied before committing changes
+    pub requirements: Vec<TableRequirement>,
+    /// List of updates to apply to the table metadata
+    pub updates: Vec<TableUpdate>,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
-pub(super) struct CommitTableResponse {
-    pub(super) metadata_location: String,
-    pub(super) metadata: TableMetadata,
+/// Response returned when a table is successfully updated.
+///
+/// The table metadata JSON is returned in the metadata field. The 
corresponding file location
+/// of table metadata must be returned in the metadata-location field. Clients 
can check whether
+/// metadata has changed by comparing metadata locations.
+pub struct CommitTableResponse {
+    /// Location of the updated table metadata file
+    pub metadata_location: String,
+    /// The table's updated metadata
+    pub metadata: TableMetadata,
 }
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
-pub(super) struct RegisterTableRequest {
-    pub(super) name: String,
-    pub(super) metadata_location: String,
-    pub(super) overwrite: Option<bool>,
+/// Request to register a table using an existing metadata file location.
+pub struct RegisterTableRequest {
+    /// Name of the table to register
+    pub name: String,
+    /// Location of the metadata file for the table
+    pub metadata_location: String,
+    /// Whether to overwrite table metadata if the table already exists
+    pub overwrite: Option<bool>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_namespace_response_serde() {
+        let json = serde_json::json!({
+            "namespace": ["nested", "ns"],
+            "properties": {
+                "key1": "value1",
+                "key2": "value2"
+            }
+        });
+        let ns_response: NamespaceResponse =
+            serde_json::from_value(json.clone()).expect("Deserialization 
failed");
+        assert_eq!(ns_response, NamespaceResponse {
+            namespace: NamespaceIdent::from_vec(vec!["nested".to_string(), 
"ns".to_string()])
+                .unwrap(),
+            properties: HashMap::from([
+                ("key1".to_string(), "value1".to_string()),
+                ("key2".to_string(), "value2".to_string()),
+            ]),
+        });
+        assert_eq!(
+            serde_json::to_value(&ns_response).expect("Serialization failed"),
+            json
+        );
+
+        // Without properties
+        let json_no_props = serde_json::json!({
+            "namespace": ["db", "schema"]
+        });
+        let ns_response_no_props: NamespaceResponse =
+            
serde_json::from_value(json_no_props.clone()).expect("Deserialization failed");
+        assert_eq!(ns_response_no_props, NamespaceResponse {
+            namespace: NamespaceIdent::from_vec(vec!["db".to_string(), 
"schema".to_string()])
+                .unwrap(),
+            properties: HashMap::new(),
+        });
+        assert_eq!(
+            serde_json::to_value(&ns_response_no_props).expect("Serialization 
failed"),
+            json_no_props
+        );
+    }
 }

Reply via email to