1use 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
28#[derive(Clone, Debug, Serialize, Deserialize)]
29pub(super) struct CatalogConfig {
30 pub(super) overrides: HashMap<String, String>,
31 pub(super) defaults: HashMap<String, String>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35pub struct ErrorResponse {
37 error: ErrorModel,
38}
39
40impl From<ErrorResponse> for Error {
41 fn from(resp: ErrorResponse) -> Error {
42 resp.error.into()
43 }
44}
45
46#[derive(Debug, Serialize, Deserialize)]
47pub struct ErrorModel {
49 pub message: String,
51 pub r#type: String,
53 pub code: u16,
55 pub stack: Option<Vec<String>>,
57}
58
59impl From<ErrorModel> for Error {
60 fn from(value: ErrorModel) -> Self {
61 let mut error = Error::new(ErrorKind::DataInvalid, value.message)
62 .with_context("type", value.r#type)
63 .with_context("code", format!("{}", value.code));
64
65 if let Some(stack) = value.stack {
66 error = error.with_context("stack", stack.join("\n"));
67 }
68
69 error
70 }
71}
72
73#[derive(Debug, Serialize, Deserialize)]
74pub(super) struct OAuthError {
75 pub(super) error: String,
76 pub(super) error_description: Option<String>,
77 pub(super) error_uri: Option<String>,
78}
79
80impl From<OAuthError> for Error {
81 fn from(value: OAuthError) -> Self {
82 let mut error = Error::new(
83 ErrorKind::DataInvalid,
84 format!("OAuthError: {}", value.error),
85 );
86
87 if let Some(desc) = value.error_description {
88 error = error.with_context("description", desc);
89 }
90
91 if let Some(uri) = value.error_uri {
92 error = error.with_context("uri", uri);
93 }
94
95 error
96 }
97}
98
99#[derive(Debug, Serialize, Deserialize)]
100pub(super) struct TokenResponse {
101 pub(super) access_token: String,
102 pub(super) token_type: String,
103 pub(super) expires_in: Option<u64>,
104 pub(super) issued_token_type: Option<String>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
108pub struct NamespaceResponse {
110 pub namespace: NamespaceIdent,
112 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
113 pub properties: HashMap<String, String>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118pub struct CreateNamespaceRequest {
120 pub namespace: NamespaceIdent,
122 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
123 pub properties: HashMap<String, String>,
125}
126
127impl From<&Namespace> for NamespaceResponse {
128 fn from(value: &Namespace) -> Self {
129 Self {
130 namespace: value.name().clone(),
131 properties: value.properties().clone(),
132 }
133 }
134}
135
136impl From<NamespaceResponse> for Namespace {
137 fn from(value: NamespaceResponse) -> Self {
138 Namespace::with_properties(value.namespace, value.properties)
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
143#[serde(rename_all = "kebab-case")]
144pub struct ListNamespaceResponse {
146 pub namespaces: Vec<NamespaceIdent>,
148 pub next_page_token: Option<String>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
154pub struct UpdateNamespacePropertiesRequest {
159 pub removals: Option<Vec<String>>,
161 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
163 pub updates: HashMap<String, String>,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
167pub struct UpdateNamespacePropertiesResponse {
169 pub updated: Vec<String>,
171 pub removed: Vec<String>,
173 #[serde(skip_serializing_if = "Option::is_none")]
176 pub missing: Option<Vec<String>>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180#[serde(rename_all = "kebab-case")]
181pub struct ListTablesResponse {
183 pub identifiers: Vec<TableIdent>,
185 #[serde(default)]
188 pub next_page_token: Option<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
192pub struct RenameTableRequest {
197 pub source: TableIdent,
199 pub destination: TableIdent,
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
204#[serde(rename_all = "kebab-case")]
205pub struct LoadTableResult {
216 pub metadata_location: Option<String>,
218 pub metadata: TableMetadata,
220 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
222 pub config: HashMap<String, String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
226 pub storage_credentials: Option<Vec<StorageCredential>>,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
230pub struct StorageCredential {
236 pub prefix: String,
238 pub config: HashMap<String, String>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
243#[serde(rename_all = "kebab-case")]
244pub struct CreateTableRequest {
251 pub name: String,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub location: Option<String>,
256 pub schema: Schema,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub partition_spec: Option<UnboundPartitionSpec>,
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub write_order: Option<SortOrder>,
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub stage_create: Option<bool>,
267 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
269 pub properties: HashMap<String, String>,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
273pub struct CommitTableRequest {
283 #[serde(skip_serializing_if = "Option::is_none")]
285 pub identifier: Option<TableIdent>,
286 pub requirements: Vec<TableRequirement>,
288 pub updates: Vec<TableUpdate>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
293#[serde(rename_all = "kebab-case")]
294pub struct CommitTableResponse {
300 pub metadata_location: String,
302 pub metadata: TableMetadata,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
307#[serde(rename_all = "kebab-case")]
308pub struct RegisterTableRequest {
310 pub name: String,
312 pub metadata_location: String,
314 pub overwrite: Option<bool>,
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_namespace_response_serde() {
324 let json = serde_json::json!({
325 "namespace": ["nested", "ns"],
326 "properties": {
327 "key1": "value1",
328 "key2": "value2"
329 }
330 });
331 let ns_response: NamespaceResponse =
332 serde_json::from_value(json.clone()).expect("Deserialization failed");
333 assert_eq!(ns_response, NamespaceResponse {
334 namespace: NamespaceIdent::from_vec(vec!["nested".to_string(), "ns".to_string()])
335 .unwrap(),
336 properties: HashMap::from([
337 ("key1".to_string(), "value1".to_string()),
338 ("key2".to_string(), "value2".to_string()),
339 ]),
340 });
341 assert_eq!(
342 serde_json::to_value(&ns_response).expect("Serialization failed"),
343 json
344 );
345
346 let json_no_props = serde_json::json!({
348 "namespace": ["db", "schema"]
349 });
350 let ns_response_no_props: NamespaceResponse =
351 serde_json::from_value(json_no_props.clone()).expect("Deserialization failed");
352 assert_eq!(ns_response_no_props, NamespaceResponse {
353 namespace: NamespaceIdent::from_vec(vec!["db".to_string(), "schema".to_string()])
354 .unwrap(),
355 properties: HashMap::new(),
356 });
357 assert_eq!(
358 serde_json::to_value(&ns_response_no_props).expect("Serialization failed"),
359 json_no_props
360 );
361 }
362
363 fn test_create_table_request_schema() -> Schema {
364 serde_json::from_value(serde_json::json!({
365 "type": "struct",
366 "schema-id": 1,
367 "fields": [
368 {
369 "id": 1,
370 "name": "foo",
371 "required": false,
372 "type": "string"
373 },
374 {
375 "id": 2,
376 "name": "bar",
377 "required": true,
378 "type": "int"
379 }
380 ],
381 "identifier-field-ids": [2]
382 }))
383 .expect("Failed to deserialize test schema")
384 }
385
386 #[test]
387 fn test_create_table_request_minimal_serialization() {
388 let request = CreateTableRequest {
389 name: "tbl1".to_string(),
390 location: None,
391 schema: test_create_table_request_schema(),
392 partition_spec: None,
393 write_order: None,
394 stage_create: None,
395 properties: HashMap::new(),
396 };
397
398 let serialized = serde_json::to_value(&request).expect("Serialization failed");
399 let object = serialized.as_object().expect("Expected a JSON object");
400 assert!(object.contains_key("name"));
401 assert!(object.contains_key("schema"));
402 assert!(!object.contains_key("location"));
403 assert!(!object.contains_key("partition-spec"));
404 assert!(!object.contains_key("write-order"));
405 assert!(!object.contains_key("stage-create"));
406 assert!(!object.contains_key("properties"));
407 }
408
409 #[test]
410 fn test_create_table_request_full_serialization() {
411 let request: CreateTableRequest = serde_json::from_value(serde_json::json!({
412 "name": "tbl1",
413 "location": "s3://warehouse/tbl1",
414 "schema": test_create_table_request_schema(),
415 "partition-spec": {
416 "spec-id": 1,
417 "fields": [
418 {
419 "source-id": 2,
420 "field-id": 1000,
421 "name": "bar",
422 "transform": "identity"
423 }
424 ]
425 },
426 "write-order": {
427 "order-id": 1,
428 "fields": [
429 {
430 "transform": "identity",
431 "source-id": 2,
432 "direction": "asc",
433 "null-order": "nulls-first"
434 }
435 ]
436 },
437 "stage-create": true,
438 "properties": {
439 "owner": "test"
440 }
441 }))
442 .expect("Deserialization failed");
443
444 let serialized = serde_json::to_value(&request).expect("Serialization failed");
445 let object = serialized.as_object().expect("Expected a JSON object");
446 assert_eq!(
447 object.get("location"),
448 Some(&serde_json::json!("s3://warehouse/tbl1"))
449 );
450 assert!(object.contains_key("partition-spec"));
451 assert!(object.contains_key("write-order"));
452 assert_eq!(object.get("stage-create"), Some(&serde_json::json!(true)));
453 assert!(object.contains_key("properties"));
454 }
455
456 #[test]
457 fn test_create_table_request_deserialize_explicit_nulls() {
458 let request: CreateTableRequest = serde_json::from_value(serde_json::json!({
459 "name": "tbl1",
460 "location": null,
461 "schema": test_create_table_request_schema(),
462 "partition-spec": null,
463 "write-order": null,
464 "stage-create": null
465 }))
466 .expect("Deserialization failed");
467
468 assert_eq!(request.name, "tbl1");
469 assert_eq!(request.location, None);
470 assert_eq!(request.partition_spec, None);
471 assert_eq!(request.write_order, None);
472 assert_eq!(request.stage_create, None);
473 assert!(request.properties.is_empty());
474 }
475}