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
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 pub(super) endpoints: Option<Vec<Endpoint>>,
37}
38
39#[derive(Debug, Serialize, Deserialize)]
40pub 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)]
52pub struct ErrorModel {
54 pub message: String,
56 pub r#type: String,
58 pub code: u16,
60 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)]
113pub struct NamespaceResponse {
115 pub namespace: NamespaceIdent,
117 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
118 pub properties: HashMap<String, String>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123pub struct CreateNamespaceRequest {
125 pub namespace: NamespaceIdent,
127 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
128 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")]
149pub struct ListNamespaceResponse {
151 pub namespaces: Vec<NamespaceIdent>,
153 pub next_page_token: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159pub struct UpdateNamespacePropertiesRequest {
164 pub removals: Option<Vec<String>>,
166 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
168 pub updates: HashMap<String, String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
172pub struct UpdateNamespacePropertiesResponse {
174 pub updated: Vec<String>,
176 pub removed: Vec<String>,
178 #[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")]
186pub struct ListTablesResponse {
188 pub identifiers: Vec<TableIdent>,
190 #[serde(default)]
193 pub next_page_token: Option<String>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
197pub struct RenameTableRequest {
202 pub source: TableIdent,
204 pub destination: TableIdent,
206}
207
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
209#[serde(rename_all = "kebab-case")]
210pub struct LoadTableResult {
221 pub metadata_location: Option<String>,
223 pub metadata: TableMetadata,
225 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
227 pub config: HashMap<String, String>,
228 #[serde(skip_serializing_if = "Option::is_none")]
231 pub storage_credentials: Option<Vec<StorageCredential>>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
235pub struct StorageCredential {
241 pub prefix: String,
243 pub config: HashMap<String, String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
248#[serde(rename_all = "kebab-case")]
249pub struct CreateTableRequest {
256 pub name: String,
258 #[serde(skip_serializing_if = "Option::is_none")]
260 pub location: Option<String>,
261 pub schema: Schema,
263 #[serde(skip_serializing_if = "Option::is_none")]
265 pub partition_spec: Option<UnboundPartitionSpec>,
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub write_order: Option<SortOrder>,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub stage_create: Option<bool>,
272 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
274 pub properties: HashMap<String, String>,
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
278pub struct CommitTableRequest {
288 #[serde(skip_serializing_if = "Option::is_none")]
290 pub identifier: Option<TableIdent>,
291 pub requirements: Vec<TableRequirement>,
293 pub updates: Vec<TableUpdate>,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
298#[serde(rename_all = "kebab-case")]
299pub struct CommitTableResponse {
305 pub metadata_location: String,
307 pub metadata: TableMetadata,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
312#[serde(rename_all = "kebab-case")]
313pub struct RegisterTableRequest {
315 pub name: String,
317 pub metadata_location: String,
319 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 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 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 let json = r#"{"overrides":{},"defaults":{},"endpoints":["GET_v1/namespaces"]}"#;
506 assert!(serde_json::from_str::<CatalogConfig>(json).is_err());
507 }
508}