@@ -81,6 +81,12 @@ pub fn validate(def: RawModuleDefV10) -> Result<ModuleDef> {
8181 . cloned ( )
8282 . map ( ExplicitNamesLookup :: new)
8383 . unwrap_or_default ( ) ;
84+ let mounts = def
85+ . mounts ( )
86+ . into_iter ( )
87+ . flat_map ( |mounts| mounts. iter ( ) . cloned ( ) )
88+ . map ( validate_mount)
89+ . collect_all_errors :: < Vec < _ > > ( ) ;
8490
8591 // Original `typespace` needs to be preserved to be assign `accesor_name`s to columns.
8692 let typespace_with_accessor_names = typespace. clone ( ) ;
@@ -263,8 +269,12 @@ pub fn validate(def: RawModuleDefV10) -> Result<ModuleDef> {
263269 . map ( |rls| ( rls. sql . clone ( ) , rls. to_owned ( ) ) )
264270 . collect ( ) ;
265271
266- let ( tables, types, reducers, procedures, views) =
267- ( tables_types_reducers_procedures_views) . map_err ( |errors| errors. sort_deduplicate ( ) ) ?;
272+ let ( tables, types, reducers, procedures, views, mounts) = ( tables_types_reducers_procedures_views, mounts)
273+ . combine_errors ( )
274+ . and_then ( |( ( tables, types, reducers, procedures, views) , mounts) | {
275+ validate_mount_names_are_unique ( mounts) . map ( |mounts| ( tables, types, reducers, procedures, views, mounts) )
276+ } )
277+ . map_err ( |errors : ValidationErrors | errors. sort_deduplicate ( ) ) ?;
268278
269279 let typespace_for_generate = typespace_for_generate. finish ( ) ;
270280
@@ -281,9 +291,32 @@ pub fn validate(def: RawModuleDefV10) -> Result<ModuleDef> {
281291 lifecycle_reducers,
282292 procedures,
283293 raw_module_def_version : RawModuleDefVersion :: V10 ,
294+ mounts,
284295 } )
285296}
286297
298+ fn validate_mount ( mount : RawModuleMountV10 ) -> Result < ( String , ModuleDef ) > {
299+ Identifier :: new ( mount. namespace . clone ( ) . into ( ) )
300+ . map_err ( |error| ValidationErrors :: from ( ValidationError :: IdentifierError { error } ) ) ?;
301+
302+ Ok ( ( mount. namespace , validate ( mount. module ) ?) )
303+ }
304+
305+ fn validate_mount_names_are_unique ( mounts : Vec < ( String , ModuleDef ) > ) -> Result < IndexMap < String , ModuleDef > > {
306+ let mut errors = vec ! [ ] ;
307+ let mut map = IndexMap :: with_capacity ( mounts. len ( ) ) ;
308+
309+ for ( namespace, def) in mounts {
310+ if map. contains_key ( & namespace) {
311+ errors. push ( ValidationError :: DuplicateName { name : namespace. into ( ) } ) ;
312+ } else {
313+ map. insert ( namespace, def) ;
314+ }
315+ }
316+
317+ ValidationErrors :: add_extra_errors ( Ok ( map) , errors)
318+ }
319+
287320/// Change the visibility of scheduled functions and lifecycle reducers to Internal.
288321///
289322fn change_scheduled_functions_and_lifetimes_visibility (
@@ -882,11 +915,14 @@ mod tests {
882915
883916 use itertools:: Itertools ;
884917 use spacetimedb_data_structures:: expect_error_matching;
885- use spacetimedb_lib:: db:: raw_def:: v10:: { CaseConversionPolicy , RawModuleDefV10Builder } ;
918+ use spacetimedb_lib:: db:: raw_def:: v10:: {
919+ CaseConversionPolicy , RawModuleDefV10 , RawModuleDefV10Builder , RawModuleDefV10Section , RawModuleMountV10 ,
920+ } ;
886921 use spacetimedb_lib:: db:: raw_def:: v9:: { btree, direct, hash} ;
887922 use spacetimedb_lib:: db:: raw_def:: * ;
888923 use spacetimedb_lib:: ScheduleAt ;
889924 use spacetimedb_primitives:: { ColId , ColList , ColSet } ;
925+ use spacetimedb_sats:: raw_identifier:: RawIdentifier ;
890926 use spacetimedb_sats:: { AlgebraicType , AlgebraicTypeRef , AlgebraicValue , ProductType , SumValue } ;
891927 use v9:: { Lifecycle , TableAccess , TableType } ;
892928
@@ -1261,6 +1297,66 @@ mod tests {
12611297 } ) ;
12621298 }
12631299
1300+ #[ test]
1301+ fn validates_mounted_submodules_recursively ( ) {
1302+ let mut mounted_builder = RawModuleDefV10Builder :: new ( ) ;
1303+ mounted_builder
1304+ . build_table_with_new_type ( "Sessions" , ProductType :: from ( [ ( "id" , AlgebraicType :: U64 ) ] ) , true )
1305+ . finish ( ) ;
1306+
1307+ let raw = RawModuleDefV10 {
1308+ sections : vec ! [ RawModuleDefV10Section :: Mounts ( vec![ RawModuleMountV10 {
1309+ namespace: "authlib" . to_string( ) ,
1310+ module: mounted_builder. finish( ) ,
1311+ } ] ) ] ,
1312+ } ;
1313+
1314+ let def: ModuleDef = raw. try_into ( ) . expect ( "mounted module should validate" ) ;
1315+ let mounts = def. mounts ( ) ;
1316+
1317+ assert_eq ! ( mounts. len( ) , 1 ) ;
1318+ let mounted = mounts. get ( "authlib" ) . expect ( "authlib mount should exist" ) ;
1319+ assert ! ( mounted. table( & expect_identifier( "sessions" ) ) . is_some( ) ) ;
1320+ }
1321+
1322+ #[ test]
1323+ fn invalid_mount_namespace ( ) {
1324+ let raw = RawModuleDefV10 {
1325+ sections : vec ! [ RawModuleDefV10Section :: Mounts ( vec![ RawModuleMountV10 {
1326+ namespace: "" . to_string( ) ,
1327+ module: RawModuleDefV10 :: default ( ) ,
1328+ } ] ) ] ,
1329+ } ;
1330+
1331+ let result: Result < ModuleDef > = raw. try_into ( ) ;
1332+
1333+ expect_error_matching ! ( result, ValidationError :: IdentifierError { error } => {
1334+ error == & IdentifierError :: Empty { }
1335+ } ) ;
1336+ }
1337+
1338+ #[ test]
1339+ fn duplicate_mount_namespace ( ) {
1340+ let raw = RawModuleDefV10 {
1341+ sections : vec ! [ RawModuleDefV10Section :: Mounts ( vec![
1342+ RawModuleMountV10 {
1343+ namespace: "authlib" . to_string( ) ,
1344+ module: RawModuleDefV10 :: default ( ) ,
1345+ } ,
1346+ RawModuleMountV10 {
1347+ namespace: "authlib" . to_string( ) ,
1348+ module: RawModuleDefV10 :: default ( ) ,
1349+ } ,
1350+ ] ) ] ,
1351+ } ;
1352+
1353+ let result: Result < ModuleDef > = raw. try_into ( ) ;
1354+
1355+ expect_error_matching ! ( result, ValidationError :: DuplicateName { name } => {
1356+ name == & RawIdentifier :: from( "authlib" )
1357+ } ) ;
1358+ }
1359+
12641360 #[ test]
12651361 fn invalid_unique_constraint_column_ref ( ) {
12661362 let mut builder = RawModuleDefV10Builder :: new ( ) ;
0 commit comments