@@ -22,6 +22,22 @@ use async_trait::async_trait;
2222use iceberg:: { Catalog , CatalogBuilder , Error , ErrorKind , Result } ;
2323use iceberg_catalog_glue:: GlueCatalogBuilder ;
2424use 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+ }
2541
2642#[ async_trait]
2743pub trait BoxedCatalogBuilder {
@@ -44,22 +60,74 @@ impl<T: CatalogBuilder + 'static> BoxedCatalogBuilder for T {
4460 }
4561}
4662
63+ /// Load a catalog from a string.
4764pub 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 (
5273 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
55102 }
56103}
57104
58105#[ cfg( test) ]
59106mod tests {
60107 use std:: collections:: HashMap ;
61108
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+ }
63131
64132 #[ tokio:: test]
65133 async fn test_load_rest_catalog ( ) {
@@ -83,17 +151,22 @@ mod tests {
83151 }
84152
85153 #[ 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+ }
88158
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" )
91164 . load (
92- "glue " . to_string ( ) ,
165+ "rest " . to_string ( ) ,
93166 HashMap :: from ( [
94167 (
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 ( ) ,
97170 ) ,
98171 ( "key" . to_string ( ) , "value" . to_string ( ) ) ,
99172 ] ) ,
@@ -102,4 +175,36 @@ mod tests {
102175
103176 assert ! ( catalog. is_ok( ) ) ;
104177 }
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+ }
105210}
0 commit comments