@@ -22,6 +22,22 @@ use async_trait::async_trait;
22
22
use iceberg:: { Catalog , CatalogBuilder , Error , ErrorKind , Result } ;
23
23
use iceberg_catalog_glue:: GlueCatalogBuilder ;
24
24
use iceberg_catalog_rest:: RestCatalogBuilder ;
25
+ use iceberg_catalog_s3tables:: S3TablesCatalogBuilder ;
26
+
27
+ /// A CatalogBuilderFactory creating a new catalog builder.
28
+ type CatalogBuilderFactory = fn ( ) -> Box < dyn BoxedCatalogBuilder > ;
29
+
30
+ /// A registry of catalog builders.
31
+ static CATALOG_REGISTRY : & [ ( & str , CatalogBuilderFactory ) ] = & [
32
+ ( "rest" , || Box :: new ( RestCatalogBuilder :: default ( ) ) ) ,
33
+ ( "glue" , || Box :: new ( GlueCatalogBuilder :: default ( ) ) ) ,
34
+ ( "s3tables" , || Box :: new ( S3TablesCatalogBuilder :: default ( ) ) ) ,
35
+ ] ;
36
+
37
+ /// Return the list of supported catalog types.
38
+ pub fn supported_types ( ) -> Vec < & ' static str > {
39
+ CATALOG_REGISTRY . iter ( ) . map ( |( k, _) | * k) . collect ( )
40
+ }
25
41
26
42
#[ async_trait]
27
43
pub trait BoxedCatalogBuilder {
@@ -44,22 +60,74 @@ impl<T: CatalogBuilder + 'static> BoxedCatalogBuilder for T {
44
60
}
45
61
}
46
62
63
+ /// Load a catalog from a string.
47
64
pub fn load ( r#type : & str ) -> Result < Box < dyn BoxedCatalogBuilder > > {
48
- match r#type {
49
- "rest" => Ok ( Box :: new ( RestCatalogBuilder :: default ( ) ) as Box < dyn BoxedCatalogBuilder > ) ,
50
- "glue" => Ok ( Box :: new ( GlueCatalogBuilder :: default ( ) ) as Box < dyn BoxedCatalogBuilder > ) ,
51
- _ => Err ( Error :: new (
65
+ let key = r#type. trim ( ) ;
66
+ if let Some ( ( _, factory) ) = CATALOG_REGISTRY
67
+ . iter ( )
68
+ . find ( |( k, _) | k. eq_ignore_ascii_case ( key) )
69
+ {
70
+ Ok ( factory ( ) )
71
+ } else {
72
+ Err ( Error :: new (
52
73
ErrorKind :: FeatureUnsupported ,
53
- format ! ( "Unsupported catalog type: {}" , r#type) ,
54
- ) ) ,
74
+ format ! (
75
+ "Unsupported catalog type: {}. Supported types: {}" ,
76
+ r#type,
77
+ supported_types( ) . join( ", " )
78
+ ) ,
79
+ ) )
80
+ }
81
+ }
82
+
83
+ /// Ergonomic catalog loader builder pattern.
84
+ pub struct CatalogLoader < ' a > {
85
+ catalog_type : & ' a str ,
86
+ }
87
+
88
+ impl < ' a > From < & ' a str > for CatalogLoader < ' a > {
89
+ fn from ( s : & ' a str ) -> Self {
90
+ Self { catalog_type : s }
91
+ }
92
+ }
93
+
94
+ impl CatalogLoader < ' _ > {
95
+ pub async fn load (
96
+ self ,
97
+ name : String ,
98
+ props : HashMap < String , String > ,
99
+ ) -> Result < Arc < dyn Catalog > > {
100
+ let builder = load ( self . catalog_type ) ?;
101
+ builder. load ( name, props) . await
55
102
}
56
103
}
57
104
58
105
#[ cfg( test) ]
59
106
mod tests {
60
107
use std:: collections:: HashMap ;
61
108
62
- use crate :: load;
109
+ use crate :: { CatalogLoader , load} ;
110
+
111
+ #[ tokio:: test]
112
+ async fn test_load_glue_catalog ( ) {
113
+ use iceberg_catalog_glue:: GLUE_CATALOG_PROP_WAREHOUSE ;
114
+
115
+ let catalog_loader = load ( "glue" ) . unwrap ( ) ;
116
+ let catalog = catalog_loader
117
+ . load (
118
+ "glue" . to_string ( ) ,
119
+ HashMap :: from ( [
120
+ (
121
+ GLUE_CATALOG_PROP_WAREHOUSE . to_string ( ) ,
122
+ "s3://test" . to_string ( ) ,
123
+ ) ,
124
+ ( "key" . to_string ( ) , "value" . to_string ( ) ) ,
125
+ ] ) ,
126
+ )
127
+ . await ;
128
+
129
+ assert ! ( catalog. is_ok( ) ) ;
130
+ }
63
131
64
132
#[ tokio:: test]
65
133
async fn test_load_rest_catalog ( ) {
@@ -83,17 +151,22 @@ mod tests {
83
151
}
84
152
85
153
#[ tokio:: test]
86
- async fn test_load_glue_catalog ( ) {
87
- use iceberg_catalog_glue:: GLUE_CATALOG_PROP_WAREHOUSE ;
154
+ async fn test_load_unsupported_catalog ( ) {
155
+ let result = load ( "unsupported" ) ;
156
+ assert ! ( result. is_err( ) ) ;
157
+ }
88
158
89
- let catalog_loader = load ( "glue" ) . unwrap ( ) ;
90
- let catalog = catalog_loader
159
+ #[ tokio:: test]
160
+ async fn test_catalog_loader_pattern ( ) {
161
+ use iceberg_catalog_rest:: REST_CATALOG_PROP_URI ;
162
+
163
+ let catalog = CatalogLoader :: from ( "rest" )
91
164
. load (
92
- "glue " . to_string ( ) ,
165
+ "rest " . to_string ( ) ,
93
166
HashMap :: from ( [
94
167
(
95
- GLUE_CATALOG_PROP_WAREHOUSE . to_string ( ) ,
96
- "s3 ://test " . to_string ( ) ,
168
+ REST_CATALOG_PROP_URI . to_string ( ) ,
169
+ "http ://localhost:8080 " . to_string ( ) ,
97
170
) ,
98
171
( "key" . to_string ( ) , "value" . to_string ( ) ) ,
99
172
] ) ,
@@ -102,4 +175,36 @@ mod tests {
102
175
103
176
assert ! ( catalog. is_ok( ) ) ;
104
177
}
178
+
179
+ #[ tokio:: test]
180
+ async fn test_catalog_loader_pattern_s3tables ( ) {
181
+ use iceberg_catalog_s3tables:: S3TABLES_CATALOG_PROP_TABLE_BUCKET_ARN ;
182
+
183
+ let catalog = CatalogLoader :: from ( "s3tables" )
184
+ . load (
185
+ "s3tables" . to_string ( ) ,
186
+ HashMap :: from ( [
187
+ (
188
+ S3TABLES_CATALOG_PROP_TABLE_BUCKET_ARN . to_string ( ) ,
189
+ "arn:aws:s3tables:us-east-1:123456789012:bucket/test" . to_string ( ) ,
190
+ ) ,
191
+ ( "key" . to_string ( ) , "value" . to_string ( ) ) ,
192
+ ] ) ,
193
+ )
194
+ . await ;
195
+
196
+ assert ! ( catalog. is_ok( ) ) ;
197
+ }
198
+
199
+ #[ tokio:: test]
200
+ async fn test_error_message_includes_supported_types ( ) {
201
+ let err = match load ( "does-not-exist" ) {
202
+ Ok ( _) => panic ! ( "expected error for unsupported type" ) ,
203
+ Err ( e) => e,
204
+ } ;
205
+ let msg = err. message ( ) . to_string ( ) ;
206
+ assert ! ( msg. contains( "Supported types:" ) ) ;
207
+ // Should include at least the built-in type
208
+ assert ! ( msg. contains( "rest" ) ) ;
209
+ }
105
210
}
0 commit comments