@@ -33,6 +33,12 @@ pub async fn extract_wits_into(
3333 Ok ( ( ) )
3434}
3535
36+ enum ImportKind {
37+ Function ( spin_serde:: KebabId ) ,
38+ WholePackage ,
39+ Interface ( spin_serde:: KebabId ) ,
40+ }
41+
3642pub async fn extract_wits (
3743 source : impl Iterator < Item = ( & DependencyName , & ComponentDependency ) > ,
3844 app_root : impl AsRef < Path > ,
@@ -49,10 +55,13 @@ pub async fn extract_wits(
4955
5056 // TODO: figure out what to do if we import two itfs from same dep
5157 for ( index, ( dependency_name, dependency) ) in source. enumerate ( ) {
52- let import_name = match dependency_name {
53- DependencyName :: Plain ( _ ) => None ,
58+ let import_kind = match dependency_name {
59+ DependencyName :: Plain ( name ) => ImportKind :: Function ( name . clone ( ) ) ,
5460 DependencyName :: Package ( dependency_package_name) => {
55- dependency_package_name. interface . as_ref ( )
61+ match & dependency_package_name. interface {
62+ Some ( itf) => ImportKind :: Interface ( itf. clone ( ) ) ,
63+ None => ImportKind :: WholePackage ,
64+ }
5665 }
5766 } ;
5867
@@ -75,9 +84,18 @@ pub async fn extract_wits(
7584 let importised = importize ( decoded, Some ( & impo_world) )
7685 . with_context ( || format ! ( "failed to map importize dependency {dependency_name}" ) ) ?;
7786
78- let imports = match import_name {
79- None => all_imports ( & importised) ,
80- Some ( itf) => one_import ( & importised, itf. as_ref ( ) ) ?,
87+ let imports = match & import_kind {
88+ ImportKind :: WholePackage => all_imports ( & importised) ,
89+ ImportKind :: Interface ( itf) => one_import ( & importised, itf. as_ref ( ) ) ?,
90+ ImportKind :: Function ( _) => Default :: default ( ) ,
91+ } ;
92+ let func_import = match & import_kind {
93+ ImportKind :: Function ( f) => one_func_import ( & importised, f. as_ref ( ) ) ?,
94+ _ => Default :: default ( ) ,
95+ } ;
96+ let type_imports = match & import_kind {
97+ ImportKind :: Function ( _) => world_type_imports ( & importised) ,
98+ _ => Default :: default ( ) ,
8199 } ;
82100
83101 // Capture WITs for all packages used in the importised thing.
@@ -142,6 +160,36 @@ pub async fn extract_wits(
142160 } ) ;
143161 }
144162 }
163+ if let Some ( mut func) = func_import {
164+ // Remap type IDs in the function to reference the aggregating resolve
165+ for param in & mut func. params {
166+ if let wit_parser:: Type :: Id ( id) = & mut param. ty {
167+ * id = remap. map_type ( * id, Span :: default ( ) ) ?;
168+ }
169+ }
170+ if let Some ( wit_parser:: Type :: Id ( id) ) = & mut func. result {
171+ * id = remap. map_type ( * id, Span :: default ( ) ) ?;
172+ }
173+
174+ // Add world-level type definitions that the function depends on
175+ let aggregating_world = aggregating_resolve
176+ . worlds
177+ . get_mut ( aggregating_world_id)
178+ . context ( "aggregated dependency world doesn't exist" ) ?;
179+ for ( name, type_id) in & type_imports {
180+ let mapped_id = remap. map_type ( * type_id, Span :: default ( ) ) ?;
181+ let wk = wit_parser:: WorldKey :: Name ( name. clone ( ) ) ;
182+ let world_item = wit_parser:: WorldItem :: Type {
183+ id : mapped_id,
184+ span : Span :: default ( ) ,
185+ } ;
186+ aggregating_world. imports . insert ( wk, world_item) ;
187+ }
188+
189+ let wk = wit_parser:: WorldKey :: Name ( func. name . clone ( ) ) ;
190+ let world_item = wit_parser:: WorldItem :: Function ( func) ;
191+ aggregating_world. imports . insert ( wk, world_item) ;
192+ }
145193 }
146194
147195 // Text for the root package and world(s)
@@ -321,6 +369,13 @@ fn as_interface(wi: &wit_parser::WorldItem) -> Option<wit_parser::InterfaceId> {
321369 }
322370}
323371
372+ fn as_func ( wi : & wit_parser:: WorldItem ) -> Option < & wit_parser:: Function > {
373+ match wi {
374+ wit_parser:: WorldItem :: Function ( func) => Some ( func) ,
375+ _ => None ,
376+ }
377+ }
378+
324379fn one_import ( wasm : & DecodedWasm , name : & str ) -> anyhow:: Result < Vec < wit_parser:: InterfaceId > > {
325380 let id = wasm
326381 . resolve ( )
@@ -332,6 +387,47 @@ fn one_import(wasm: &DecodedWasm, name: &str) -> anyhow::Result<Vec<wit_parser::
332387 Ok ( vec ! [ id] )
333388}
334389
390+ fn world_type_imports ( wasm : & DecodedWasm ) -> Vec < ( String , wit_parser:: TypeId ) > {
391+ wasm. resolve ( )
392+ . worlds
393+ . iter ( )
394+ . flat_map ( |( _wid, w) | {
395+ w. imports . iter ( ) . filter_map ( |( wk, wi) | {
396+ if let wit_parser:: WorldItem :: Type { id, .. } = wi {
397+ let name = match wk {
398+ wit_parser:: WorldKey :: Name ( n) => n. clone ( ) ,
399+ wit_parser:: WorldKey :: Interface ( _) => return None ,
400+ } ;
401+ Some ( ( name, * id) )
402+ } else {
403+ None
404+ }
405+ } )
406+ } )
407+ . collect ( )
408+ }
409+
410+ fn one_func_import ( wasm : & DecodedWasm , name : & str ) -> anyhow:: Result < Option < wit_parser:: Function > > {
411+ let funcs = wasm
412+ . resolve ( )
413+ . worlds
414+ . iter ( )
415+ . flat_map ( |w| {
416+ w. 1 . imports
417+ . iter ( )
418+ . flat_map ( |( _wk, wi) | as_func ( wi) )
419+ . filter ( |f| f. name == name)
420+ } )
421+ . collect :: < Vec < _ > > ( ) ;
422+
423+ // This shouldn't happen because we are using the compiled Wasm so there should
424+ // be only one world in play. But belt and braces.
425+ if funcs. len ( ) > 1 {
426+ anyhow:: bail!( "Dependency exports more than one function named {name}" ) ;
427+ }
428+ Ok ( funcs. first ( ) . cloned ( ) . cloned ( ) )
429+ }
430+
335431fn read_wasm ( wasm_bytes : & [ u8 ] ) -> anyhow:: Result < DecodedWasm > {
336432 if wasmparser:: Parser :: is_component ( wasm_bytes) {
337433 wit_component:: decode ( wasm_bytes)
@@ -493,4 +589,237 @@ mod test {
493589
494590 Ok ( ( ) )
495591 }
592+
593+ #[ tokio:: test]
594+ async fn world_level_func_extracted ( ) -> anyhow:: Result < ( ) > {
595+ let tempdir = tempfile:: TempDir :: new ( ) ?;
596+ let dep_file = tempdir. path ( ) . join ( "crimes.wasm" ) ;
597+
598+ let dep_wit = "package my:crimes@1.0.0;\n \n world crimes {\n export is-curse: func(s: string) -> bool;\n }" ;
599+ let dep_wasm = generate_dummy_component ( dep_wit, "crimes" ) ;
600+ tokio:: fs:: write ( & dep_file, & dep_wasm) . await ?;
601+
602+ let dep_name = DependencyName :: Plain ( "is-curse" . to_string ( ) . try_into ( ) . unwrap ( ) ) ;
603+ let dep_src = ComponentDependency :: Local {
604+ path : dep_file,
605+ export : None ,
606+ } ;
607+ let deps = std:: iter:: once ( ( & dep_name, & dep_src) ) ;
608+
609+ let wit = extract_wits ( deps, "." ) . await ?;
610+
611+ let resolve = parse_wit ( & wit) . expect ( "should have emitted valid WIT" ) ;
612+
613+ assert_eq ! ( 1 , resolve. packages. len( ) ) ; // root:component - world-level funcs don't retain their package when importised
614+ let ( _rc_pkg_id, rc_pkg) = resolve
615+ . packages
616+ . iter ( )
617+ . find ( |( _, p) | p. name . to_string ( ) == "root:component" )
618+ . expect ( "should have had `root:component`" ) ;
619+
620+ let root_world_id = rc_pkg
621+ . worlds
622+ . get ( "root" )
623+ . expect ( "should have had root world" ) ;
624+ let root_world = resolve
625+ . worlds
626+ . get ( * root_world_id)
627+ . expect ( "should have had root world at that id" ) ;
628+
629+ let func = root_world
630+ . imports
631+ . iter ( )
632+ . filter_map ( |( _, wi) | as_func ( wi) )
633+ . find ( |f| f. name == "is-curse" )
634+ . expect ( "is-curse function does not appear in root imports" ) ;
635+
636+ assert_eq ! ( 1 , func. params. len( ) ) ;
637+ assert_eq ! ( wit_parser:: Type :: String , func. params. first( ) . unwrap( ) . ty) ;
638+ assert_eq ! ( wit_parser:: Type :: Bool , func. result. unwrap( ) ) ;
639+
640+ Ok ( ( ) )
641+ }
642+
643+ #[ tokio:: test]
644+ async fn world_level_func_with_record_param ( ) -> anyhow:: Result < ( ) > {
645+ let tempdir = tempfile:: TempDir :: new ( ) ?;
646+ let dep_file = tempdir. path ( ) . join ( "greeter.wasm" ) ;
647+
648+ let dep_wit = r#"package my:greeter@1.0.0;
649+
650+ world greeter {
651+ record person {
652+ name: string,
653+ age: u32,
654+ }
655+ export greet: func(who: person) -> string;
656+ }"# ;
657+ let dep_wasm = generate_dummy_component ( dep_wit, "greeter" ) ;
658+ tokio:: fs:: write ( & dep_file, & dep_wasm) . await ?;
659+
660+ let dep_name = DependencyName :: Plain ( "greet" . to_string ( ) . try_into ( ) . unwrap ( ) ) ;
661+ let dep_src = ComponentDependency :: Local {
662+ path : dep_file,
663+ export : None ,
664+ } ;
665+ let deps = std:: iter:: once ( ( & dep_name, & dep_src) ) ;
666+
667+ let wit = extract_wits ( deps, "." ) . await ?;
668+
669+ let resolve = parse_wit ( & wit) . expect ( "should have emitted valid WIT" ) ;
670+
671+ let ( _rc_pkg_id, rc_pkg) = resolve
672+ . packages
673+ . iter ( )
674+ . find ( |( _, p) | p. name . to_string ( ) == "root:component" )
675+ . expect ( "should have had `root:component`" ) ;
676+
677+ let root_world_id = rc_pkg
678+ . worlds
679+ . get ( "root" )
680+ . expect ( "should have had root world" ) ;
681+ let root_world = resolve
682+ . worlds
683+ . get ( * root_world_id)
684+ . expect ( "should have had root world at that id" ) ;
685+
686+ let func = root_world
687+ . imports
688+ . iter ( )
689+ . filter_map ( |( _, wi) | as_func ( wi) )
690+ . find ( |f| f. name == "greet" )
691+ . expect ( "greet function does not appear in root imports" ) ;
692+
693+ assert_eq ! ( 1 , func. params. len( ) ) ;
694+ // The param should be a user-defined type (record)
695+ assert ! (
696+ matches!( func. params. first( ) . unwrap( ) . ty, wit_parser:: Type :: Id ( _) ) ,
697+ "expected record param to be Type::Id"
698+ ) ;
699+ assert_eq ! ( wit_parser:: Type :: String , func. result. unwrap( ) ) ;
700+
701+ Ok ( ( ) )
702+ }
703+
704+ #[ tokio:: test]
705+ async fn world_level_func_with_record_result ( ) -> anyhow:: Result < ( ) > {
706+ let tempdir = tempfile:: TempDir :: new ( ) ?;
707+ let dep_file = tempdir. path ( ) . join ( "lookup.wasm" ) ;
708+
709+ let dep_wit = r#"package my:lookup@1.0.0;
710+
711+ world lookup {
712+ record info {
713+ value: string,
714+ found: bool,
715+ }
716+ export lookup: func(key: string) -> info;
717+ }"# ;
718+ let dep_wasm = generate_dummy_component ( dep_wit, "lookup" ) ;
719+ tokio:: fs:: write ( & dep_file, & dep_wasm) . await ?;
720+
721+ let dep_name = DependencyName :: Plain ( "lookup" . to_string ( ) . try_into ( ) . unwrap ( ) ) ;
722+ let dep_src = ComponentDependency :: Local {
723+ path : dep_file,
724+ export : None ,
725+ } ;
726+ let deps = std:: iter:: once ( ( & dep_name, & dep_src) ) ;
727+
728+ let wit = extract_wits ( deps, "." ) . await ?;
729+
730+ let resolve = parse_wit ( & wit) . expect ( "should have emitted valid WIT" ) ;
731+
732+ let ( _rc_pkg_id, rc_pkg) = resolve
733+ . packages
734+ . iter ( )
735+ . find ( |( _, p) | p. name . to_string ( ) == "root:component" )
736+ . expect ( "should have had `root:component`" ) ;
737+
738+ let root_world_id = rc_pkg
739+ . worlds
740+ . get ( "root" )
741+ . expect ( "should have had root world" ) ;
742+ let root_world = resolve
743+ . worlds
744+ . get ( * root_world_id)
745+ . expect ( "should have had root world at that id" ) ;
746+
747+ let func = root_world
748+ . imports
749+ . iter ( )
750+ . filter_map ( |( _, wi) | as_func ( wi) )
751+ . find ( |f| f. name == "lookup" )
752+ . expect ( "lookup function does not appear in root imports" ) ;
753+
754+ assert_eq ! ( 1 , func. params. len( ) ) ;
755+ assert_eq ! ( wit_parser:: Type :: String , func. params. first( ) . unwrap( ) . ty) ;
756+ // The result should be a user-defined type (record)
757+ assert ! (
758+ matches!( func. result, Some ( wit_parser:: Type :: Id ( _) ) ) ,
759+ "expected record result to be Type::Id"
760+ ) ;
761+
762+ Ok ( ( ) )
763+ }
764+
765+ #[ tokio:: test]
766+ async fn world_level_func_with_enum_param ( ) -> anyhow:: Result < ( ) > {
767+ let tempdir = tempfile:: TempDir :: new ( ) ?;
768+ let dep_file = tempdir. path ( ) . join ( "color.wasm" ) ;
769+
770+ let dep_wit = r#"package my:colors@1.0.0;
771+
772+ world colors {
773+ enum color {
774+ red,
775+ green,
776+ blue,
777+ }
778+ export color-name: func(c: color) -> string;
779+ }"# ;
780+ let dep_wasm = generate_dummy_component ( dep_wit, "colors" ) ;
781+ tokio:: fs:: write ( & dep_file, & dep_wasm) . await ?;
782+
783+ let dep_name = DependencyName :: Plain ( "color-name" . to_string ( ) . try_into ( ) . unwrap ( ) ) ;
784+ let dep_src = ComponentDependency :: Local {
785+ path : dep_file,
786+ export : None ,
787+ } ;
788+ let deps = std:: iter:: once ( ( & dep_name, & dep_src) ) ;
789+
790+ let wit = extract_wits ( deps, "." ) . await ?;
791+
792+ let resolve = parse_wit ( & wit) . expect ( "should have emitted valid WIT" ) ;
793+
794+ let ( _rc_pkg_id, rc_pkg) = resolve
795+ . packages
796+ . iter ( )
797+ . find ( |( _, p) | p. name . to_string ( ) == "root:component" )
798+ . expect ( "should have had `root:component`" ) ;
799+
800+ let root_world_id = rc_pkg
801+ . worlds
802+ . get ( "root" )
803+ . expect ( "should have had root world" ) ;
804+ let root_world = resolve
805+ . worlds
806+ . get ( * root_world_id)
807+ . expect ( "should have had root world at that id" ) ;
808+
809+ let func = root_world
810+ . imports
811+ . iter ( )
812+ . filter_map ( |( _, wi) | as_func ( wi) )
813+ . find ( |f| f. name == "color-name" )
814+ . expect ( "color-name function does not appear in root imports" ) ;
815+
816+ assert_eq ! ( 1 , func. params. len( ) ) ;
817+ assert ! (
818+ matches!( func. params. first( ) . unwrap( ) . ty, wit_parser:: Type :: Id ( _) ) ,
819+ "expected enum param to be Type::Id"
820+ ) ;
821+ assert_eq ! ( wit_parser:: Type :: String , func. result. unwrap( ) ) ;
822+
823+ Ok ( ( ) )
824+ }
496825}
0 commit comments