@@ -20,112 +20,92 @@ use std::collections::HashMap;
2020use std:: path:: Path ;
2121use std:: sync:: Arc ;
2222
23- use anyhow:: anyhow;
23+ use anyhow:: { anyhow, Context } ;
2424use datafusion:: catalog:: { CatalogProvider , CatalogProviderList } ;
2525use fs_err:: read_to_string;
2626use iceberg_catalog_rest:: { RestCatalog , RestCatalogConfig } ;
2727use iceberg_datafusion:: IcebergCatalogProvider ;
28+ use serde:: Deserialize ;
2829use toml:: { Table as TomlTable , Value } ;
2930
3031const CONFIG_NAME_CATALOGS : & str = "catalogs" ;
3132
32- #[ derive( Debug ) ]
33- pub struct IcebergCatalogList {
34- catalogs : HashMap < String , Arc < IcebergCatalogProvider > > ,
33+ #[ derive( Deserialize , Debug , PartialEq ) ]
34+ #[ serde( tag = "type" ) ]
35+ pub enum CatalogConfig {
36+ #[ serde( rename = "rest" ) ]
37+ Rest ( RestCatalogConfig ) ,
3538}
3639
37- impl IcebergCatalogList {
38- pub async fn parse ( path : & Path ) -> anyhow:: Result < Self > {
39- let toml_table: TomlTable = toml:: from_str ( & read_to_string ( path) ?) ?;
40- Self :: parse_table ( & toml_table) . await
40+ #[ derive( Deserialize , Debug , PartialEq ) ]
41+ pub struct CatalogConfigDef {
42+ name : String ,
43+ #[ serde( flatten) ]
44+ config : CatalogConfig ,
45+ }
46+
47+ impl CatalogConfigDef {
48+ async fn try_into_catalog ( self ) -> anyhow:: Result < Arc < IcebergCatalogProvider > > {
49+ match self . config {
50+ CatalogConfig :: Rest ( config) => {
51+ let catalog = RestCatalog :: new ( config) ;
52+ Ok ( Arc :: new (
53+ IcebergCatalogProvider :: try_new ( Arc :: new ( catalog) ) . await ?,
54+ ) )
55+ }
56+ }
4157 }
4258
43- pub async fn parse_table ( configs : & TomlTable ) -> anyhow:: Result < Self > {
44- if let Value :: Array ( catalogs_config ) =
45- configs . get ( CONFIG_NAME_CATALOGS ) . ok_or_else ( || {
46- anyhow :: Error :: msg ( format ! ( "{CONFIG_NAME_CATALOGS} entry not found in config" ) )
47- } ) ?
48- {
49- let mut catalogs = HashMap :: with_capacity ( catalogs_config . len ( ) ) ;
50- for config in catalogs_config {
51- if let Value :: Table ( table_config ) = config {
52- let ( name, catalog_provider ) =
53- IcebergCatalogList :: parse_one ( table_config ) . await ? ;
54- catalogs. insert ( name, catalog_provider ) ;
59+ pub fn parse ( root : & TomlTable ) -> anyhow:: Result < HashMap < String , Self > > {
60+ if let Value :: Array ( catalog_configs ) = root . get ( CONFIG_NAME_CATALOGS ) . ok_or_else ( || {
61+ anyhow :: Error :: msg ( format ! ( "{CONFIG_NAME_CATALOGS} entry not found in config" ) )
62+ } ) ? {
63+ let mut catalogs = HashMap :: with_capacity ( catalog_configs . len ( ) ) ;
64+ for value in catalog_configs {
65+ if let Value :: Table ( table ) = value {
66+ let catalog : CatalogConfigDef = table . clone ( ) . try_into ( ) ? ;
67+ if catalogs . contains_key ( & catalog . name ) {
68+ return Err ( anyhow ! ( "Duplicate catalog name: {}" , catalog . name ) ) ;
69+ }
70+ catalogs. insert ( catalog . name . clone ( ) , catalog ) ;
5571 } else {
5672 return Err ( anyhow ! ( "{CONFIG_NAME_CATALOGS} entry must be a table" ) ) ;
5773 }
5874 }
59- Ok ( Self { catalogs } )
75+ Ok ( catalogs)
6076 } else {
6177 Err ( anyhow ! ( "{CONFIG_NAME_CATALOGS} must be an array of table!" ) )
6278 }
6379 }
80+ }
6481
65- async fn parse_one (
66- config : & TomlTable ,
67- ) -> anyhow:: Result < ( String , Arc < IcebergCatalogProvider > ) > {
68- let name = config
69- . get ( "name" )
70- . ok_or_else ( || anyhow:: anyhow!( "name not found for catalog" ) ) ?
71- . as_str ( )
72- . ok_or_else ( || anyhow:: anyhow!( "name is not string" ) ) ?;
73-
74- let r#type = config
75- . get ( "type" )
76- . ok_or_else ( || anyhow:: anyhow!( "type not found for catalog" ) ) ?
77- . as_str ( )
78- . ok_or_else ( || anyhow:: anyhow!( "type is not string" ) ) ?;
79-
80- if r#type != "rest" {
81- return Err ( anyhow:: anyhow!( "Only rest catalog is supported for now!" ) ) ;
82- }
82+ impl TryFrom < TomlTable > for CatalogConfigDef {
83+ type Error = anyhow:: Error ;
8384
84- let catalog_config = config
85- . get ( "config" )
86- . ok_or_else ( || anyhow:: anyhow!( "config not found for catalog {name}" ) ) ?
87- . as_table ( )
88- . ok_or_else ( || anyhow:: anyhow!( "config is not table for catalog {name}" ) ) ?;
89-
90- let uri = catalog_config
91- . get ( "uri" )
92- . ok_or_else ( || anyhow:: anyhow!( "uri not found for catalog {name}" ) ) ?
93- . as_str ( )
94- . ok_or_else ( || anyhow:: anyhow!( "uri is not string" ) ) ?;
95-
96- let warehouse = catalog_config
97- . get ( "warehouse" )
98- . ok_or_else ( || anyhow:: anyhow!( "warehouse not found for catalog {name}" ) ) ?
99- . as_str ( )
100- . ok_or_else ( || anyhow:: anyhow!( "warehouse is not string for catalog {name}" ) ) ?;
101-
102- let props_table = catalog_config
103- . get ( "props" )
104- . ok_or_else ( || anyhow:: anyhow!( "props not found for catalog {name}" ) ) ?
105- . as_table ( )
106- . ok_or_else ( || anyhow:: anyhow!( "props is not table for catalog {name}" ) ) ?;
107-
108- let mut props = HashMap :: with_capacity ( props_table. len ( ) ) ;
109- for ( key, value) in props_table {
110- let value_str = value
111- . as_str ( )
112- . ok_or_else ( || anyhow:: anyhow!( "props {key} is not string" ) ) ?;
113- props. insert ( key. to_string ( ) , value_str. to_string ( ) ) ;
85+ fn try_from ( table : TomlTable ) -> Result < Self , Self :: Error > {
86+ table
87+ . try_into :: < CatalogConfigDef > ( )
88+ . with_context ( || "Failed to parse catalog config" . to_string ( ) )
89+ }
90+ }
91+
92+ #[ derive( Debug ) ]
93+ pub struct IcebergCatalogList {
94+ catalogs : HashMap < String , Arc < IcebergCatalogProvider > > ,
95+ }
96+
97+ impl IcebergCatalogList {
98+ pub async fn parse ( path : & Path ) -> anyhow:: Result < Self > {
99+ let root_config: TomlTable = toml:: from_str ( & read_to_string ( path) ?) ?;
100+ let catalog_configs = CatalogConfigDef :: parse ( & root_config) ?;
101+
102+ let mut catalogs = HashMap :: with_capacity ( catalog_configs. len ( ) ) ;
103+ for ( name, config) in catalog_configs {
104+ let catalog = config. try_into_catalog ( ) . await ?;
105+ catalogs. insert ( name, catalog) ;
114106 }
115107
116- let rest_catalog_config = RestCatalogConfig :: builder ( )
117- . uri ( uri. to_string ( ) )
118- . warehouse ( warehouse. to_string ( ) )
119- . props ( props)
120- . build ( ) ;
121-
122- Ok ( (
123- name. to_string ( ) ,
124- Arc :: new (
125- IcebergCatalogProvider :: try_new ( Arc :: new ( RestCatalog :: new ( rest_catalog_config) ) )
126- . await ?,
127- ) ,
128- ) )
108+ Ok ( Self { catalogs } )
129109 }
130110}
131111
@@ -153,3 +133,66 @@ impl CatalogProviderList for IcebergCatalogList {
153133 . map ( |c| c. clone ( ) as Arc < dyn CatalogProvider > )
154134 }
155135}
136+
137+ #[ cfg( test) ]
138+ mod tests {
139+ use std:: collections:: HashMap ;
140+
141+ use fs_err:: read_to_string;
142+ use iceberg_catalog_rest:: RestCatalogConfig ;
143+ use toml:: Table as TomlTable ;
144+
145+ use crate :: { CatalogConfig , CatalogConfigDef } ;
146+
147+ #[ test]
148+ fn test_parse_config ( ) {
149+ let config_file_path = format ! ( "{}/testdata/catalogs.toml" , env!( "CARGO_MANIFEST_DIR" ) ) ;
150+
151+ let root_config: TomlTable =
152+ toml:: from_str ( & read_to_string ( config_file_path) . unwrap ( ) ) . unwrap ( ) ;
153+
154+ let catalog_configs = CatalogConfigDef :: parse ( & root_config) . unwrap ( ) ;
155+
156+ assert_eq ! ( catalog_configs. len( ) , 2 ) ;
157+
158+ let catalog1 = catalog_configs. get ( "demo" ) . unwrap ( ) ;
159+ let expected_catalog1 = CatalogConfigDef {
160+ name : "demo" . to_string ( ) ,
161+ config : CatalogConfig :: Rest (
162+ RestCatalogConfig :: builder ( )
163+ . uri ( "http://localhost:8080" . to_string ( ) )
164+ . warehouse ( "s3://iceberg-demo" . to_string ( ) )
165+ . props ( HashMap :: from ( [
166+ (
167+ "s3.endpoint" . to_string ( ) ,
168+ "http://localhost:9000" . to_string ( ) ,
169+ ) ,
170+ ( "s3.access_key_id" . to_string ( ) , "admin" . to_string ( ) ) ,
171+ ] ) )
172+ . build ( ) ,
173+ ) ,
174+ } ;
175+
176+ assert_eq ! ( catalog1, & expected_catalog1) ;
177+
178+ let catalog2 = catalog_configs. get ( "demo2" ) . unwrap ( ) ;
179+ let expected_catalog2 = CatalogConfigDef {
180+ name : "demo2" . to_string ( ) ,
181+ config : CatalogConfig :: Rest (
182+ RestCatalogConfig :: builder ( )
183+ . uri ( "http://localhost2:8080" . to_string ( ) )
184+ . warehouse ( "s3://iceberg-demo2" . to_string ( ) )
185+ . props ( HashMap :: from ( [
186+ (
187+ "s3.endpoint" . to_string ( ) ,
188+ "http://localhost2:9090" . to_string ( ) ,
189+ ) ,
190+ ( "s3.access_key_id" . to_string ( ) , "admin2" . to_string ( ) ) ,
191+ ] ) )
192+ . build ( ) ,
193+ ) ,
194+ } ;
195+
196+ assert_eq ! ( catalog2, & expected_catalog2) ;
197+ }
198+ }
0 commit comments