diff --git a/Cargo.lock b/Cargo.lock index ceaa916..257d3fa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -406,9 +406,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cap-fs-ext" @@ -547,9 +547,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75ca66430e33a14957acc24c5077b503e7d374151b2b4b3a10c83b4ceb4be0e" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -557,9 +557,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.56" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793207c7fa6300a0608d1080b858e5fdbe713cdc1c8db9fb17777d8a13e63df0" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -611,7 +611,7 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "composable-runtime" -version = "0.2.1" +version = "0.3.0" dependencies = [ "anyhow", "clap", @@ -779,36 +779,36 @@ dependencies = [ [[package]] name = "cranelift-assembler-x64" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32b9105ce689b3e79ae288f62e9c2d0de66e4869176a11829e5c696da0f018f" +checksum = "5394862aa254f2cc52f0e566fe4e3392c8cd39c56595314b4156f5f2d7ce4b22" dependencies = [ "cranelift-assembler-x64-meta", ] [[package]] name = "cranelift-assembler-x64-meta" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e950e8dd96c1760f1c3a2b06d3d35584a3617239d034e73593ec096a1f3ea69" +checksum = "64ddeadbaba1230fc2323203a64fa86924b0ca4cf4c09f1b8205c1e9d44c1988" dependencies = [ "cranelift-srcgen", ] [[package]] name = "cranelift-bforest" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d769576bc48246fccf7f07173739e5f7a7fb3270eb9ac363c0792cad8963c034" +checksum = "9a50f2336905397e4fb21cd5a23ce789f8a1b5c2ec1bc998b8f2a670d8188b51" dependencies = [ "cranelift-entity", ] [[package]] name = "cranelift-bitset" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d37c4589e52def48bd745c3b28b523d66ade8b074644ed3a366144c225f212" +checksum = "8265efe579ca6d058c16d30b5cb479bbc3f645e532fb7adb55118cd5ace93b99" dependencies = [ "serde", "serde_derive", @@ -816,9 +816,9 @@ dependencies = [ [[package]] name = "cranelift-codegen" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c23b5ab93367eba82bddf49b63d841d8a0b8b39fb89d82829de6647b3a747108" +checksum = "c220af4f081034f2c99bbe418553aab5962c94bafb405f9b49544f3deda931c8" dependencies = [ "bumpalo", "cranelift-assembler-x64", @@ -843,9 +843,9 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6118d26dd046455d31374b9432947ea2ba445c21fd8724370edd072f51f3bd" +checksum = "17779d84afd287551b1f1b302a7f20ee30c7629c90c0d62048c911e57221382f" dependencies = [ "cranelift-assembler-x64-meta", "cranelift-codegen-shared", @@ -856,24 +856,24 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a068c67f04f37de835fda87a10491e266eea9f9283d0887d8bd0a2c0726588a9" +checksum = "7ae5bc63af60b57ff330e00b8447d6af28e5b1b7830b2d631138d449de3c5f27" [[package]] name = "cranelift-control" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ceb830549fcd7f05493a3b6d3d2bcfa4d43588b099e8c2393d2d140d6f7951" +checksum = "715ff4081b8d25e449f6fcdf476fc6d93c7c19db2adfbf9c9e490e81c35357aa" dependencies = [ "arbitrary", ] [[package]] name = "cranelift-entity" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b130f0edd119e7665f1875b8d686bd3fccefd9d74d10e9005cbcd76392e1831" +checksum = "b0054dde6df0598d1ffcf8ff053b21d4d943514b319b6fff89fe2408c887f345" dependencies = [ "cranelift-bitset", "serde", @@ -882,9 +882,9 @@ dependencies = [ [[package]] name = "cranelift-frontend" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626a46aa207183bae011de3411a40951c494cea3fb2ef223d3118f75e13b23ca" +checksum = "0972cfb8c88a15a70aa642a0c00eebb7606ffb804b1f4be7c6879518940878ac" dependencies = [ "cranelift-codegen", "log", @@ -894,15 +894,15 @@ dependencies = [ [[package]] name = "cranelift-isle" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09dab08a5129cf59919fdd4567e599ea955de62191a852982150ac42ce4ab21" +checksum = "e3f88428d378420e0e7873f28c8bc66ad77b04cd90ca3427a28fa84dc84336c9" [[package]] name = "cranelift-native" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "847b8eaef0f7095b401d3ce80587036495b94e7a051904df9e28d6cd14e69b94" +checksum = "ed9accb524d7e4136a9682700c49e32cf819f8842c3c86b934b14e1be3f40622" dependencies = [ "cranelift-codegen", "libc", @@ -911,9 +911,9 @@ dependencies = [ [[package]] name = "cranelift-srcgen" -version = "0.128.1" +version = "0.128.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a4849e90e778f2fcc9fd1b93bd074dbf6b8b6f420951f9617c4774fe71e7fc" +checksum = "6d2cb5ac82ae9cb24c0b028d79af7d2717284d66ea63d5a0606a293831e0ef15" [[package]] name = "crc32fast" @@ -1462,9 +1462,9 @@ checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1906,7 +1906,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.4", "tower-service", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -3292,9 +3292,9 @@ dependencies = [ [[package]] name = "pulley-interpreter" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b733bc861727077314d961c926e41f4a2f366c9bf1c2b29caf8182b979e9fd" +checksum = "7eaf996754e8ac54980166a9e230bd70dc8d5a52c13b4ae245c90c05950e5f00" dependencies = [ "cranelift-bitset", "log", @@ -3304,9 +3304,9 @@ dependencies = [ [[package]] name = "pulley-macros" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "591c2768539dc694548d3aec1460b5afeb6bdeccb3ca1fbeac4d81a381fedc05" +checksum = "b7217f9311a6ec94adb6cc311044423a488b29a20c76d9d841b793db75a34e0b" dependencies = [ "proc-macro2", "quote", @@ -3537,9 +3537,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3549,9 +3549,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3560,9 +3560,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "reqwest" @@ -3608,7 +3608,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] @@ -4883,9 +4883,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wac-graph" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d511e0c9462a5f6369e7e17e9f0f3b566eab2a235076a23f2db19ca7bf36d32c" +checksum = "7c22d99cf996435bda507f323cca418cd513c3c604ca3157f5e4e79990b47378" dependencies = [ "anyhow", "id-arena", @@ -4895,23 +4895,23 @@ dependencies = [ "semver", "thiserror 1.0.69", "wac-types", - "wasm-encoder 0.239.0", - "wasm-metadata 0.239.0", - "wasmparser 0.239.0", + "wasm-encoder 0.244.0", + "wasm-metadata 0.244.0", + "wasmparser 0.244.0", ] [[package]] name = "wac-types" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fdef742a5198856c7c754944b329ed684f703dca477d0a77b474b37d990121" +checksum = "c86d6f994ea751789cd416144648039ee9bdb385dffb6d890bd51a90e2f50778" dependencies = [ "anyhow", "id-arena", "indexmap 2.13.0", "semver", - "wasm-encoder 0.239.0", - "wasmparser 0.239.0", + "wasm-encoder 0.244.0", + "wasmparser 0.244.0", ] [[package]] @@ -5256,16 +5256,6 @@ dependencies = [ "wasmparser 0.230.0", ] -[[package]] -name = "wasm-encoder" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be00faa2b4950c76fe618c409d2c3ea5a3c9422013e079482d78544bb2d184c" -dependencies = [ - "leb128fmt", - "wasmparser 0.239.0", -] - [[package]] name = "wasm-encoder" version = "0.240.0" @@ -5310,9 +5300,9 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.239.0" +version = "0.240.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20b3ec880a9ac69ccd92fbdbcf46ee833071cf09f82bb005b2327c7ae6025ae2" +checksum = "ee093e1e1ccffa005b9b778f7a10ccfd58e25a20eccad294a1a93168d076befb" dependencies = [ "anyhow", "auditable-serde", @@ -5323,15 +5313,15 @@ dependencies = [ "serde_json", "spdx", "url", - "wasm-encoder 0.239.0", - "wasmparser 0.239.0", + "wasm-encoder 0.240.0", + "wasmparser 0.240.0", ] [[package]] name = "wasm-metadata" -version = "0.240.0" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee093e1e1ccffa005b9b778f7a10ccfd58e25a20eccad294a1a93168d076befb" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "auditable-serde", @@ -5342,27 +5332,15 @@ dependencies = [ "serde_json", "spdx", "url", - "wasm-encoder 0.240.0", - "wasmparser 0.240.0", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.13.0", "wasm-encoder 0.244.0", "wasmparser 0.244.0", ] [[package]] name = "wasm-pkg-client" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1665d5eb54b96a2a648ea7ac5e0063a9add862c1a53f074228483856b908cb" +checksum = "84808ebb7c58a39968cfe86213b73662eba9e62962cc2bc8d690c5e660597861" dependencies = [ "anyhow", "async-trait", @@ -5388,16 +5366,16 @@ dependencies = [ "warg-client", "warg-crypto", "warg-protocol", - "wasm-metadata 0.240.0", + "wasm-metadata 0.244.0", "wasm-pkg-common", - "wit-component 0.240.0", + "wit-component 0.244.0", ] [[package]] name = "wasm-pkg-common" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58e453663b87d2fb87a8db0f7ed856208da5ff41e9f640d386404a5a45f9fa68" +checksum = "f559b51f405ebed23dded43888e5f286034b1df008272626dd09e02ce0267968" dependencies = [ "anyhow", "bytes", @@ -5449,19 +5427,6 @@ dependencies = [ "semver", ] -[[package]] -name = "wasmparser" -version = "0.239.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9d90bb93e764f6beabf1d02028c70a2156a6583e63ac4218dd07ef733368b0" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver", - "serde", -] - [[package]] name = "wasmparser" version = "0.240.0" @@ -5498,6 +5463,7 @@ dependencies = [ "hashbrown 0.15.5", "indexmap 2.13.0", "semver", + "serde", ] [[package]] @@ -5523,9 +5489,9 @@ dependencies = [ [[package]] name = "wasmtime" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a1198409bd281650c097b95ac1d20a82e5b403a5ca7223ea607fe1272125d5a" +checksum = "2071e49a50eaef6bd12eec02355b2a709236a2735e65b6d23472c32e7c020c17" dependencies = [ "addr2line", "anyhow", @@ -5580,9 +5546,9 @@ dependencies = [ [[package]] name = "wasmtime-environ" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b9af430b11ff3cd63fbef54cf38e26154089c179316b8a5e400b8ba2d0ebf1" +checksum = "1eb20b557b1a9df3a2e8a37defbcd3313054bc02d5eb9f2b15ae59631a3f1184" dependencies = [ "anyhow", "cpp_demangle", @@ -5607,9 +5573,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-cache" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f09527993e5d3ab68857fa8b4cddfb300ec89d8bbe6ba33e279f0234367e73" +checksum = "1bbad778d22bd1aa9d4ffed34ee375a242379beeb8d0e0e7a667234e6796b482" dependencies = [ "base64 0.22.1", "directories-next", @@ -5627,9 +5593,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-macro" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5c69a6d1514ee5bcae494f69f3fee7a20528a38048fc9e847e0833af71071b" +checksum = "dcf66b77a1291286a0024d42ef5d9d0334f38227977b58af5ec15b965567afec" dependencies = [ "anyhow", "proc-macro2", @@ -5642,15 +5608,15 @@ dependencies = [ [[package]] name = "wasmtime-internal-component-util" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa29030e4457259121400fa9043e9af3bb29e004e2f56b5e26caf1a2728fc5f" +checksum = "ad1f3fcec132942d77acb522b169a18f7a6c9236059545b75170ef55e57e37ff" [[package]] name = "wasmtime-internal-cranelift" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "452397e623732c58fd9ce0545c62210965c0446155667fbd59c380642ce6df1b" +checksum = "8f443a74164cd518bdcfc590b4fc041e54c980d338c1810995e4160d12e67600" dependencies = [ "cfg-if", "cranelift-codegen", @@ -5675,9 +5641,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-fiber" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa94737a693a38227edca24aaa995d3a3a80b2fe88a7de029345bd35c0d19b13" +checksum = "a8723e44dc785c549573a638bf8b6694e14464c81067dc2229bd31366660a768" dependencies = [ "cc", "cfg-if", @@ -5690,9 +5656,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-debug" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2d760a8909786674007cc1a65fd999d280502437c73b2eb4fab2fe6b714effe" +checksum = "4e09439c739fda8e0e1726a8c9ead01b554923932cdc1c385aa8ddf91cf1f2e8" dependencies = [ "cc", "object", @@ -5702,36 +5668,36 @@ dependencies = [ [[package]] name = "wasmtime-internal-jit-icache-coherence" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b46da671c07242b5f5eab491b12d6c25dd26929f1693c055fcca94489ef8f5" +checksum = "4b03d32d6f314c2fb6e63812ea4c52f21492d5a61c71335bb47ef304bc93f5c6" dependencies = [ + "anyhow", "cfg-if", "libc", - "wasmtime-environ", "windows-sys 0.61.2", ] [[package]] name = "wasmtime-internal-math" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d1f0763c6f6f78e410f964db9f53d9b84ab4cc336945e81f0b78717b0a9934e" +checksum = "e52892b22815800b4df55cb8d4439e2c565b7b93fd5b8a7c8db7df2f1ff3dd36" dependencies = [ "libm", ] [[package]] name = "wasmtime-internal-slab" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f641abc8d6c6d5464615222b0617c85317f391c14aaa60b13183e4e2a63462" +checksum = "66beaaa7299518878636fcd0de8977ce7199e45c8856a5a8e81d3a86579ac924" [[package]] name = "wasmtime-internal-unwinder" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6916a23c8369d3caf04630f55598b5c326782817faa318c5e9355ed7dea8f172" +checksum = "a5453b0c89e9d2d7d055acda31883945ce5a3a55c2fdd7527f4599ce8fe3ce04" dependencies = [ "cfg-if", "cranelift-codegen", @@ -5742,9 +5708,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-versioned-export-macros" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a724908757d1b5c174984f4215e377183de1d4fe789f3755f6b4fd7928274fb" +checksum = "16ad635e44d818517658643a20a2d820a51e2ae7f0ecebdf1c62b9103548684d" dependencies = [ "proc-macro2", "quote", @@ -5753,9 +5719,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-winch" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa86f52a53d2bfcb60673b039a0e07bbcc2dd3e5a6459df1dcc195e563045479" +checksum = "783cced1001cc5e65f60fd7e4d02d34e836c0d996dc596b2979945f1b9a9a274" dependencies = [ "cranelift-codegen", "gimli 0.32.3", @@ -5770,9 +5736,9 @@ dependencies = [ [[package]] name = "wasmtime-internal-wit-bindgen" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5c0d0892239910953c6f3e9ff5cf418c29eb964470ea855b64b2c0af67f2b8a" +checksum = "b7d7097d2d45cba0d54a02313edf2f29ae7ac25136b276b266c5d3f4cebc377d" dependencies = [ "anyhow", "bitflags", @@ -5783,9 +5749,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b48028f5a86dc62c4d23b4769f5a59dcafb572c172b7b94a53619820a2727f3d" +checksum = "9e9a5f0cb1a3f431126ce3492eb4377cbee4ccbff4bd665c16cd820fde98374f" dependencies = [ "anyhow", "async-trait", @@ -5814,9 +5780,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-config" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6827ea256b4e63e16bc2c80609abec95602dde1ba042ed6635cfccbdf86a609" +checksum = "02cdeb0f426ddb9521999ee5de6c99fef4b1ff7d6e2ad90d5d8fea5485157932" dependencies = [ "anyhow", "wasmtime", @@ -5824,9 +5790,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-http" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81820fe01f1946d204e119f383d8f25c48bf2b5cd9e308b156098cc42799b3a4" +checksum = "d472ce54835f5dc651e6f7a822a7fd4a796b850a564ebf2e446411f97d3e737c" dependencies = [ "anyhow", "async-trait", @@ -5848,9 +5814,9 @@ dependencies = [ [[package]] name = "wasmtime-wasi-io" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb53401d473beef46b530a5d6394f3ea9ccdbabc1b66456c72b8ad6015060697" +checksum = "3b02fff2b36bc4890bf8572c9ca04aadd43a5bb2b106acca33354d3570d63236" dependencies = [ "anyhow", "async-trait", @@ -5916,23 +5882,23 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.5", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.5" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bed680863276c63889429bfd6cab3b99943659923822de1c8a39c49e4d722c" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] [[package]] name = "wiggle" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6ae01b30b9f18d138161960031656929f85d747b3dd4bcfad7ee34fe097a65" +checksum = "16b42475e1648312bcbca35cb01d2cb7d6ff78cc08ebde2c55d6e57b72d908d4" dependencies = [ "anyhow", "bitflags", @@ -5944,9 +5910,9 @@ dependencies = [ [[package]] name = "wiggle-generate" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9938a7719a726027b28bfc435bd162004a73a31410615894a920a80ee8119216" +checksum = "f1cc82757160f7b3e746a07f30fe7eb03bfc31c55b8a26db9e07cf24da61ee62" dependencies = [ "anyhow", "heck 0.5.0", @@ -5958,9 +5924,9 @@ dependencies = [ [[package]] name = "wiggle-macro" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b933908b084f69998d6a3d1072a32e534d1f9888d04b4ffd0fe179c7759af239" +checksum = "e6b1e7fc31371904777f39ac534c4615dc330a67744ebe5a5231694dcce15a34" dependencies = [ "proc-macro2", "quote", @@ -6001,9 +5967,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" -version = "41.0.1" +version = "41.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37f6bea231cd5a9b4e70f30172556c6793dedf4308dcb45902e6be3e1cb0448d" +checksum = "fadddf48b9dff460e640146eabae4df1f204fa97fff6d7570b7603403bee38a9" dependencies = [ "anyhow", "cranelift-assembler-x64", @@ -6600,18 +6566,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7456cf00f0685ad319c5b1693f291a650eaf345e941d082fc4e03df8a03996ac" +checksum = "57cf3aa6855b23711ee9852dfc97dfaa51c45feaba5b645d0c777414d494a961" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.37" +version = "0.8.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1328722bbf2115db7e19d69ebcc15e795719e2d66b60827c6a69a117365e37a0" +checksum = "8a616990af1a287837c4fe6596ad77ef57948f787e46ce28e166facc0cc1cb75" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 227df14..466e6ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "composable-runtime" -version = "0.2.1" +version = "0.3.0" edition = "2024" description = "Modulewise Composable Runtime" @@ -15,9 +15,9 @@ serde_json = "1" static-config = "0.2" tokio = { version = "1", features = ["macros", "rt-multi-thread"], optional = true } toml = "0.9" -wac-graph = "0.8" -wac-types = "0.8" -wasm-pkg-client = "0.13" +wac-graph = "0.9" +wac-types = "0.9" +wasm-pkg-client = "0.14" wasmtime = "41" wasmtime-wasi = "41" wasmtime-wasi-config = "41" diff --git a/src/graph.rs b/src/graph.rs index 9539ec3..91cde9f 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,170 +1,27 @@ use anyhow::Result; use petgraph::graph::{DiGraph, NodeIndex}; use petgraph::visit::EdgeRef; -use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ops::{Index, IndexMut}; +use std::path::PathBuf; -// Type definitions for component and runtime feature definitions -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct DefinitionBase { - pub uri: String, - #[serde(default = "default_enables")] - pub enables: String, // "none"|"package"|"namespace"|"unexposed"|"exposed"|"any" -} - -pub fn default_enables() -> String { - "none".to_string() -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ComponentDefinitionBase { - #[serde(flatten)] - pub base: DefinitionBase, - #[serde(default)] - pub expects: Vec, // Named components this expects to be available - #[serde(default)] - pub intercepts: Vec, // Components this intercepts - #[serde(default)] - pub precedence: i32, // Lower values have higher precedence - #[serde(default)] - pub exposed: bool, - pub config: Option>, -} - -impl std::ops::Deref for ComponentDefinitionBase { - type Target = DefinitionBase; - fn deref(&self) -> &Self::Target { - &self.base - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct RuntimeFeatureDefinition { - pub name: String, - #[serde(flatten)] - pub base: DefinitionBase, -} - -impl std::ops::Deref for RuntimeFeatureDefinition { - type Target = DefinitionBase; - fn deref(&self) -> &Self::Target { - &self.base - } -} - -impl std::fmt::Debug for RuntimeFeatureDefinition { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RuntimeFeatureDefinition") - .field("name", &self.name) - .field("uri", &self.uri) - .field("enables", &self.enables) - .finish() - } -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct ComponentDefinition { - pub name: String, - #[serde(flatten)] - pub base: ComponentDefinitionBase, -} - -impl std::ops::Deref for ComponentDefinition { - type Target = ComponentDefinitionBase; - fn deref(&self) -> &Self::Target { - &self.base - } -} - -impl std::fmt::Debug for ComponentDefinition { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ComponentDefinition") - .field("name", &self.name) - .field("uri", &self.uri) - .field("enables", &self.enables) - .field("expects", &self.expects) - .field("intercepts", &self.intercepts) - .field("precedence", &self.precedence) - .field("exposed", &self.exposed) - .field("config", &self.config) - .finish() - } -} - -impl AsRef for ComponentDefinition { - fn as_ref(&self) -> &DefinitionBase { - &self.base.base - } -} +use crate::loader; +use crate::types::{ComponentDefinition, RuntimeFeatureDefinition}; pub struct ComponentGraph { graph: DiGraph, node_map: HashMap, } -impl std::fmt::Debug for ComponentGraph { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - struct FlatNode<'a>(&'a Node); - impl<'a> std::fmt::Debug for FlatNode<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self.0 { - Node::Component(def) => std::fmt::Debug::fmt(def, f), - Node::RuntimeFeature(def) => std::fmt::Debug::fmt(def, f), - } - } - } - - let mut debug_struct = f.debug_struct("ComponentGraph"); - - let nodes: Vec<_> = self - .graph - .raw_nodes() - .iter() - .map(|n| FlatNode(&n.weight)) - .collect(); - debug_struct.field("nodes", &nodes); - - let edges: Vec = self - .graph - .edge_references() - .map(|edge| { - let source_node = &self.graph[edge.source()]; - let target_node = &self.graph[edge.target()]; - let source_name = match source_node { - Node::Component(def) => &def.name, - Node::RuntimeFeature(def) => &def.name, - }; - let target_name = match target_node { - Node::Component(def) => &def.name, - Node::RuntimeFeature(def) => &def.name, - }; - format!("{} -> {} ({:?})", source_name, target_name, edge.weight()) - }) - .collect(); - debug_struct.field("edges", &edges); - debug_struct.finish() - } -} - -impl Index for ComponentGraph { - type Output = Node; - - fn index(&self, index: NodeIndex) -> &Self::Output { - &self.graph[index] - } -} - -impl IndexMut for ComponentGraph { - fn index_mut(&mut self, index: NodeIndex) -> &mut Self::Output { - &mut self.graph[index] +impl ComponentGraph { + /// Create a new GraphBuilder + pub fn builder() -> GraphBuilder { + GraphBuilder::new() } -} -impl ComponentGraph { /// Create a graph where each component and runtime feature is a node /// and each dependency or interceptor relationship is an edge. - pub fn build( + pub(crate) fn build( component_definitions: &[ComponentDefinition], runtime_feature_definitions: &[RuntimeFeatureDefinition], ) -> Result { @@ -375,6 +232,64 @@ impl ComponentGraph { } } +impl std::fmt::Debug for ComponentGraph { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + struct FlatNode<'a>(&'a Node); + impl<'a> std::fmt::Debug for FlatNode<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.0 { + Node::Component(def) => std::fmt::Debug::fmt(def, f), + Node::RuntimeFeature(def) => std::fmt::Debug::fmt(def, f), + } + } + } + + let mut debug_struct = f.debug_struct("ComponentGraph"); + + let nodes: Vec<_> = self + .graph + .raw_nodes() + .iter() + .map(|n| FlatNode(&n.weight)) + .collect(); + debug_struct.field("nodes", &nodes); + + let edges: Vec = self + .graph + .edge_references() + .map(|edge| { + let source_node = &self.graph[edge.source()]; + let target_node = &self.graph[edge.target()]; + let source_name = match source_node { + Node::Component(def) => &def.name, + Node::RuntimeFeature(def) => &def.name, + }; + let target_name = match target_node { + Node::Component(def) => &def.name, + Node::RuntimeFeature(def) => &def.name, + }; + format!("{} -> {} ({:?})", source_name, target_name, edge.weight()) + }) + .collect(); + debug_struct.field("edges", &edges); + debug_struct.finish() + } +} + +impl Index for ComponentGraph { + type Output = Node; + + fn index(&self, index: NodeIndex) -> &Self::Output { + &self.graph[index] + } +} + +impl IndexMut for ComponentGraph { + fn index_mut(&mut self, index: NodeIndex) -> &mut Self::Output { + &mut self.graph[index] + } +} + fn is_interceptor_enabled( interceptor: &ComponentDefinition, consumer: &ComponentDefinition, @@ -406,3 +321,27 @@ pub enum Edge { Dependency, Interceptor(i32), // Precedence } + +/// Builder for constructing a ComponentGraph +pub struct GraphBuilder { + paths: Vec, +} + +impl GraphBuilder { + fn new() -> Self { + Self { paths: Vec::new() } + } + + /// Load definitions from a file (.toml or .wasm) + pub fn load_file(mut self, path: impl Into) -> Self { + self.paths.push(path.into()); + self + } + + /// Build the ComponentGraph from all loaded definitions + pub fn build(self) -> Result { + let (component_definitions, runtime_feature_definitions) = + loader::parse_definition_files(&self.paths)?; + ComponentGraph::build(&component_definitions, &runtime_feature_definitions) + } +} diff --git a/src/lib.rs b/src/lib.rs index e60e21c..37ed243 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,10 +3,10 @@ //! A runtime for Wasm Components that supports //! composition, config, and capability management. -pub use graph::ComponentGraph; -pub use loader::load_definitions; +pub use graph::{ComponentGraph, GraphBuilder}; pub use registry::HostExtension; -pub use runtime::{Component, ComponentState, Runtime}; +pub use runtime::{Component, Runtime, RuntimeBuilder}; +pub use types::ComponentState; pub use wit::{Function, FunctionParam}; // exposed for testing, hidden from docs @@ -14,6 +14,8 @@ pub use wit::{Function, FunctionParam}; pub mod graph; #[doc(hidden)] pub mod registry; +#[doc(hidden)] +pub mod types; mod composer; mod loader; diff --git a/src/loader.rs b/src/loader.rs index 9a19a78..8d9c695 100644 --- a/src/loader.rs +++ b/src/loader.rs @@ -3,24 +3,14 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::PathBuf; -use crate::graph::{ - ComponentDefinition, ComponentDefinitionBase, ComponentGraph, DefinitionBase, - RuntimeFeatureDefinition, default_enables, +use crate::types::{ + ComponentDefinition, ComponentDefinitionBase, DefinitionBase, RuntimeFeatureDefinition, + default_enables, }; -/// Load component definitions and runtime feature definitions from configuration files -/// and build a component graph -pub fn load_definitions( +pub(crate) fn parse_definition_files( definition_files: &[PathBuf], // .toml and .wasm files -) -> Result { - let (runtime_feature_definitions, component_definitions) = - parse_definition_files(definition_files)?; - ComponentGraph::build(&component_definitions, &runtime_feature_definitions) -} - -fn parse_definition_files( - definition_files: &[PathBuf], // .toml and .wasm files -) -> Result<(Vec, Vec)> { +) -> Result<(Vec, Vec)> { let mut toml_files = Vec::new(); let mut wasm_files = Vec::new(); @@ -49,15 +39,15 @@ fn parse_definition_files( fn build_definitions( toml_files: &[PathBuf], wasm_files: &[PathBuf], -) -> Result<(Vec, Vec)> { - let mut runtime_feature_definitions = Vec::new(); +) -> Result<(Vec, Vec)> { let mut component_definitions = Vec::new(); + let mut runtime_feature_definitions = Vec::new(); - // Parse TOML files to extract both runtime features and components + // Parse TOML files to extract both components and runtime features for file in toml_files { - let (runtime_features, components) = parse_toml_file(file)?; - runtime_feature_definitions.extend(runtime_features); + let (components, runtime_features) = parse_toml_file(file)?; component_definitions.extend(components); + runtime_feature_definitions.extend(runtime_features); } // Add implicit component definitions from standalone .wasm files @@ -100,7 +90,7 @@ fn build_definitions( } } - Ok((runtime_feature_definitions, component_definitions)) + Ok((component_definitions, runtime_feature_definitions)) } fn validate_runtime_feature_enables_scope(enables: &str, name: &str) -> Result<()> { @@ -126,37 +116,39 @@ fn validate_component_enables_scope(enables: &str) -> Result<()> { fn parse_toml_file( path: &PathBuf, -) -> Result<(Vec, Vec)> { +) -> Result<(Vec, Vec)> { let content = fs::read_to_string(path)?; let toml_doc: toml::Value = toml::from_str(&content)?; - let mut runtime_features = Vec::new(); let mut components = Vec::new(); + let mut runtime_features = Vec::new(); if let toml::Value::Table(table) = toml_doc { for (name, value) in table { if let toml::Value::Table(def_table) = value { // Check if this is a runtime feature (wasmtime:* or host:*) or component if let Some(uri) = def_table.get("uri").and_then(|v| v.as_str()) { + let mut definition_value = def_table.clone(); + let config = if let Some(toml::Value::Table(config_table)) = + definition_value.remove("config") + { + Some(convert_toml_table_to_json_map(&config_table)?) + } else { + None + }; + if uri.starts_with("wasmtime:") || uri.starts_with("host:") { - let definition_base: DefinitionBase = - toml::Value::Table(def_table).try_into().map_err(|e| { + let definition_base: DefinitionBase = toml::Value::Table(definition_value) + .try_into() + .map_err(|e| { anyhow::anyhow!("Failed to parse runtime feature '{name}': {e}") })?; runtime_features.push(RuntimeFeatureDefinition { name: name.clone(), base: definition_base, + config: config.unwrap_or_default(), }); } else { - let mut definition_value = def_table.clone(); - let config = if let Some(toml::Value::Table(config_table)) = - definition_value.remove("config") - { - Some(convert_toml_table_to_json_map(&config_table)?) - } else { - None - }; - let mut component_base: ComponentDefinitionBase = toml::Value::Table(definition_value) .try_into() @@ -184,7 +176,7 @@ fn parse_toml_file( "TOML file must contain a table at root level" )); } - Ok((runtime_features, components)) + Ok((components, runtime_features)) } fn create_implicit_component_definitions( diff --git a/src/main.rs b/src/main.rs index 29519fc..ec32606 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use anyhow::Result; use clap::{Args, Parser, Subcommand}; -use composable_runtime::{ComponentGraph, Runtime, load_definitions}; +use composable_runtime::{ComponentGraph, Runtime}; use rustyline::Editor; use rustyline::error::ReadlineError; use rustyline::history::DefaultHistory; @@ -58,7 +58,11 @@ async fn main() -> Result<()> { let cli = Cli::parse(); println!("Loading definitions from: {:?}...", cli.definitions); - let graph = load_definitions(&cli.definitions)?; + let mut builder = ComponentGraph::builder(); + for path in &cli.definitions { + builder = builder.load_file(path); + } + let graph = builder.build()?; if cli.mode.dry_run { println!("--- Component Dependency Graph (Dry Run) ---"); @@ -77,7 +81,7 @@ async fn main() -> Result<()> { async fn run_interactive_session(graph: &ComponentGraph) -> Result<()> { println!("Building runtime..."); - let runtime = Runtime::from_graph(graph).await?; + let runtime = Runtime::builder(graph).build().await?; let components = runtime.list_components(); println!( "Successfully built runtime with {} exposed components.", diff --git a/src/registry.rs b/src/registry.rs index 601716a..b443b7b 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -1,47 +1,66 @@ use anyhow::Result; use serde::{Deserialize, Serialize}; +use std::any::{Any, TypeId}; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; use std::sync::Arc; +use wasmtime::component::Linker; use crate::composer::Composer; -use crate::graph::{ComponentDefinition, ComponentGraph, Node, RuntimeFeatureDefinition}; +use crate::graph::{ComponentGraph, Node}; +use crate::types::{ComponentDefinition, ComponentState, RuntimeFeatureDefinition}; use crate::wit::{ComponentMetadata, Parser}; -type InitializerFn = Box< - dyn Fn(&mut wasmtime::component::Linker) -> Result<()> - + Send - + Sync, ->; - -/// A host-provided extension that can be passed to Runtime::from_graph_with_host_extensions(). +/// Trait implemented by host extension instances. /// -/// Host extensions provide custom runtime features implemented by the embedding application. -/// They are matched to `host:` URI entries in the component graph's TOML configuration. -pub struct HostExtension { - /// The extension name (matches the name after "host:" in the URI) - pub name: String, - /// List of WIT interfaces this extension provides (e.g. "example:foo/bar") - pub interfaces: Vec, - /// Function to add implementations to the linker - pub initializer: InitializerFn, +/// An instance represents a configured feature (from one TOML block). +/// Multiple TOML blocks with the same `uri = "host:X"` create multiple instances. +pub trait HostExtension: Send + Sync { + /// Fully qualified interfaces this extension provides (namespace:package/interface@version) + fn interfaces(&self) -> Vec; + + /// Add bindings to the linker. Called once per component instantiation. + fn link(&self, linker: &mut Linker) -> Result<()>; + + /// Create per-component-instance state. Called once per component instantiation. + /// Returns None if extension needs no per-instance state. + fn create_state_boxed(&self) -> Result)>> { + Ok(None) + } } -impl HostExtension { - /// Create a new host extension - pub fn new(name: impl Into, interfaces: Vec, initializer: F) -> Self - where - F: Fn(&mut wasmtime::component::Linker) -> Result<()> - + Send - + Sync - + 'static, - { - Self { - name: name.into(), - interfaces, - initializer: Box::new(initializer), +/// Factory function that creates a HostExtension instance from TOML config. +pub(crate) type HostExtensionFactory = + Box Result> + Send + Sync>; + +/// Macro for implementing `create_state_boxed()` with automatic TypeId inference. +/// +/// The `$body` expression has access to `self` (the extension instance) and can use `?` +/// for fallible operations. +/// +/// # Example +/// +/// ```ignore +/// impl HostExtension for MyFeature { +/// // ... +/// create_state!(MyState, { +/// MyState { +/// shared_resource: self.get_resource(), +/// counter: 0, +/// } +/// }); +/// } +/// ``` +#[macro_export] +macro_rules! create_state { + ($type:ty, $body:expr) => { + fn create_state_boxed( + &self, + ) -> anyhow::Result)>> { + let state: $type = $body; + Ok(Some((std::any::TypeId::of::<$type>(), Box::new(state)))) } - } + }; } #[derive(Serialize, Deserialize)] @@ -49,14 +68,9 @@ pub struct RuntimeFeature { pub uri: String, pub enables: String, pub interfaces: Vec, + /// The host extension instance (for `host:` URIs) #[serde(skip)] - pub initializer: Option< - Box< - dyn Fn(&mut wasmtime::component::Linker) -> Result<()> - + Send - + Sync, - >, - >, + pub extension: Option>, } impl std::fmt::Debug for RuntimeFeature { @@ -66,8 +80,8 @@ impl std::fmt::Debug for RuntimeFeature { .field("enables", &self.enables) .field("interfaces", &self.interfaces) .field( - "initializer", - &self.initializer.as_ref().map(|_| ""), + "extension", + &self.extension.as_ref().map(|_| ""), ) .finish() } @@ -233,8 +247,8 @@ impl Default for ComponentRegistry { /// Build registries from definitions pub async fn build_registries( component_graph: &ComponentGraph, - host_extensions: Vec, -) -> Result<(RuntimeFeatureRegistry, ComponentRegistry)> { + factories: HashMap<&'static str, HostExtensionFactory>, +) -> Result<(ComponentRegistry, RuntimeFeatureRegistry)> { let mut runtime_feature_definitions = Vec::new(); for node in component_graph.nodes() { if let Node::RuntimeFeature(def) = &node.weight { @@ -243,7 +257,7 @@ pub async fn build_registries( } let runtime_feature_registry = - create_runtime_feature_registry(runtime_feature_definitions, host_extensions).await?; + create_runtime_feature_registry(runtime_feature_definitions, factories)?; let sorted_indices = component_graph.get_build_order(); @@ -295,38 +309,51 @@ pub async fn build_registries( } Ok(( - runtime_feature_registry, ComponentRegistry { components: Arc::new(exposed_components), enabling_components: Arc::new(enabling_components), }, + runtime_feature_registry, )) } -async fn create_runtime_feature_registry( +fn create_runtime_feature_registry( runtime_feature_definitions: Vec, - host_extensions: Vec, + factories: HashMap<&'static str, HostExtensionFactory>, ) -> Result { let mut runtime_features = HashMap::new(); - // Convert to map for lookup - let mut host_extensions: HashMap = host_extensions - .into_iter() - .map(|ext| (ext.name.clone(), ext)) - .collect(); - for def in runtime_feature_definitions { - let (interfaces, initializer) = if let Some(feature_name) = def.uri.strip_prefix("host:") { - let host_ext = host_extensions.remove(feature_name).ok_or_else(|| { + let (interfaces, extension) = if let Some(feature_name) = def.uri.strip_prefix("host:") { + let factory = factories.get(feature_name).ok_or_else(|| { + anyhow::anyhow!( + "Host extension '{}' (URI: '{}') not registered. Use Runtime::builder().with_host_extension::(\"{}\")", + feature_name, + def.uri, + feature_name + ) + })?; + + // Deserialize config into extension instance + let config_value = serde_json::to_value(&def.config)?; + let ext = factory(config_value).map_err(|e| { anyhow::anyhow!( - "Host extension '{}' (URI: '{}') not provided. Pass it to Runtime::from_graph_with_host_extensions()", + "Failed to create host extension '{}' from TOML block '{}': {}", feature_name, - def.uri + def.name, + e ) })?; - (host_ext.interfaces, Some(host_ext.initializer)) + + (ext.interfaces(), Some(ext)) } else { // wasmtime feature + if !def.config.is_empty() { + println!( + "Warning: Config provided for runtime feature '{}' but only host extensions support config", + def.name + ); + } (get_interfaces_for_runtime_feature(&def.uri), None) }; @@ -334,9 +361,9 @@ async fn create_runtime_feature_registry( uri: def.uri.clone(), enables: def.enables.clone(), interfaces, - initializer, + extension, }; - runtime_features.insert(def.name.clone(), runtime_feature); + runtime_features.insert(def.name, runtime_feature); } Ok(RuntimeFeatureRegistry::new(runtime_features)) diff --git a/src/runtime.rs b/src/runtime.rs index 2995199..88815b1 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,16 +1,22 @@ use anyhow::Result; +use serde::de::DeserializeOwned; use std::collections::HashMap; +use std::collections::hash_map::Entry; use wasmtime::{ Cache, Config, Engine, Store, component::{Component as WasmComponent, Linker, Type, Val}, }; use wasmtime_wasi::random::{WasiRandom, WasiRandomView}; -use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView}; +use wasmtime_wasi::{ResourceTable, WasiCtxBuilder, WasiCtxView, WasiView}; use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; use wasmtime_wasi_io::IoView; use crate::graph::ComponentGraph; -use crate::registry::{ComponentRegistry, HostExtension, RuntimeFeatureRegistry, build_registries}; +use crate::registry::{ + ComponentRegistry, HostExtension, HostExtensionFactory, RuntimeFeatureRegistry, + build_registries, +}; +use crate::types::ComponentState; use crate::wit::Function; /// Wasm Component whose functions can be invoked @@ -24,29 +30,14 @@ pub struct Component { #[derive(Clone)] pub struct Runtime { invoker: Invoker, - runtime_feature_registry: RuntimeFeatureRegistry, component_registry: ComponentRegistry, + runtime_feature_registry: RuntimeFeatureRegistry, } impl Runtime { - /// Create a Runtime from a ComponentGraph - pub async fn from_graph(graph: &ComponentGraph) -> Result { - Self::from_graph_with_host_extensions(graph, vec![]).await - } - - /// Create a Runtime from a ComponentGraph with HostExtensions - pub async fn from_graph_with_host_extensions( - graph: &ComponentGraph, - host_extensions: Vec, - ) -> Result { - let (runtime_feature_registry, component_registry) = - build_registries(graph, host_extensions).await?; - let invoker = Invoker::new()?; - Ok(Self { - invoker, - runtime_feature_registry, - component_registry, - }) + /// Create a RuntimeBuilder from a ComponentGraph + pub fn builder(graph: &ComponentGraph) -> RuntimeBuilder<'_> { + RuntimeBuilder::new(graph) } /// List all exposed components @@ -146,10 +137,61 @@ impl Runtime { } } -pub struct ComponentState { - pub wasi_ctx: WasiCtx, - pub wasi_http_ctx: Option, - pub resource_table: ResourceTable, +/// Builder for configuring and creating a Runtime +pub struct RuntimeBuilder<'a> { + graph: &'a ComponentGraph, + factories: HashMap<&'static str, HostExtensionFactory>, +} + +impl<'a> RuntimeBuilder<'a> { + fn new(graph: &'a ComponentGraph) -> Self { + Self { + graph, + factories: HashMap::new(), + } + } + + /// Register a host extension type for the given name. + /// + /// The name corresponds to the suffix in `uri = "host:name"` in TOML. + /// + /// If the TOML block has an empty config and deserialization fails, + /// falls back to `Default::default()`. + pub fn with_host_extension(mut self, name: &'static str) -> Self + where + T: HostExtension + DeserializeOwned + Default + 'static, + { + self.factories.insert( + name, + Box::new( + |config: serde_json::Value| -> Result> { + match serde_json::from_value::(config.clone()) { + Ok(instance) => Ok(Box::new(instance)), + Err(e) => { + if config == serde_json::json!({}) { + Ok(Box::new(T::default())) + } else { + Err(e.into()) + } + } + } + }, + ), + ); + self + } + + /// Build the Runtime + pub async fn build(self) -> Result { + let (component_registry, runtime_feature_registry) = + build_registries(self.graph, self.factories).await?; + let invoker = Invoker::new()?; + Ok(Runtime { + invoker, + component_registry, + runtime_feature_registry, + }) + } } impl IoView for ComponentState { @@ -249,11 +291,11 @@ impl Invoker { } } } else if runtime_feature.uri.starts_with("host:") { - if let Some(initializer) = &runtime_feature.initializer { - initializer(&mut linker)?; + if let Some(ext) = &runtime_feature.extension { + ext.link(&mut linker)?; } else { return Err(anyhow::anyhow!( - "Host feature '{}' requested but no initializer registered", + "Host feature '{}' requested but no extension registered", feature_name )); } @@ -309,6 +351,28 @@ impl Invoker { == Some("http") }); + // Collect extension states before creating ComponentState + let mut extensions = HashMap::new(); + for feature_name in runtime_features { + if let Some(runtime_feature) = + runtime_feature_registry.get_runtime_feature(feature_name) + && runtime_feature.uri.starts_with("host:") + && let Some(ext) = &runtime_feature.extension + && let Some((type_id, boxed_state)) = ext.create_state_boxed()? + { + match extensions.entry(type_id) { + Entry::Vacant(e) => { + e.insert(boxed_state); + } + Entry::Occupied(_) => { + anyhow::bail!( + "Duplicate extension state type for feature '{feature_name}'" + ); + } + } + } + } + let state = ComponentState { wasi_ctx: wasi_builder.build(), wasi_http_ctx: if needs_http { @@ -317,6 +381,7 @@ impl Invoker { None }, resource_table: ResourceTable::new(), + extensions, }; let mut store = Store::new(&self.engine, state); diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..5ba0f37 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,134 @@ +//! Core type definitions shared across the crate. + +use serde::{Deserialize, Serialize}; +use std::any::{Any, TypeId}; +use std::collections::HashMap; + +/// Base definition with URI and enables scope +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct DefinitionBase { + pub uri: String, + #[serde(default = "default_enables")] + pub enables: String, // "none"|"package"|"namespace"|"unexposed"|"exposed"|"any" +} + +pub fn default_enables() -> String { + "none".to_string() +} + +/// Component definition base with additional fields +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ComponentDefinitionBase { + #[serde(flatten)] + pub base: DefinitionBase, + #[serde(default)] + pub expects: Vec, // Named components this expects to be available + #[serde(default)] + pub intercepts: Vec, // Components this intercepts + #[serde(default)] + pub precedence: i32, // Lower values have higher precedence + #[serde(default)] + pub exposed: bool, + pub config: Option>, +} + +impl std::ops::Deref for ComponentDefinitionBase { + type Target = DefinitionBase; + fn deref(&self) -> &Self::Target { + &self.base + } +} + +/// Runtime feature definition +#[derive(Deserialize, Serialize, Clone)] +pub struct RuntimeFeatureDefinition { + pub name: String, + #[serde(flatten)] + pub base: DefinitionBase, + /// Configuration from `config.[key]` entries in TOML + #[serde(default)] + pub config: HashMap, +} + +impl std::ops::Deref for RuntimeFeatureDefinition { + type Target = DefinitionBase; + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl std::fmt::Debug for RuntimeFeatureDefinition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RuntimeFeatureDefinition") + .field("name", &self.name) + .field("uri", &self.uri) + .field("enables", &self.enables) + .field("config", &self.config) + .finish() + } +} + +/// Component definition +#[derive(Deserialize, Serialize, Clone)] +pub struct ComponentDefinition { + pub name: String, + #[serde(flatten)] + pub base: ComponentDefinitionBase, +} + +impl std::ops::Deref for ComponentDefinition { + type Target = ComponentDefinitionBase; + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl std::fmt::Debug for ComponentDefinition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ComponentDefinition") + .field("name", &self.name) + .field("uri", &self.uri) + .field("enables", &self.enables) + .field("expects", &self.expects) + .field("intercepts", &self.intercepts) + .field("precedence", &self.precedence) + .field("exposed", &self.exposed) + .field("config", &self.config) + .finish() + } +} + +impl AsRef for ComponentDefinition { + fn as_ref(&self) -> &DefinitionBase { + &self.base.base + } +} + +/// State passed to Wasm components during execution. +pub struct ComponentState { + pub wasi_ctx: wasmtime_wasi::WasiCtx, + pub wasi_http_ctx: Option, + pub resource_table: wasmtime_wasi::ResourceTable, + pub(crate) extensions: HashMap>, +} + +impl ComponentState { + /// Get a reference to an extension by type. + pub fn get_extension(&self) -> Option<&T> { + self.extensions + .get(&TypeId::of::()) + .and_then(|boxed| boxed.downcast_ref()) + } + + /// Get a mutable reference to an extension by type. + pub fn get_extension_mut(&mut self) -> Option<&mut T> { + self.extensions + .get_mut(&TypeId::of::()) + .and_then(|boxed| boxed.downcast_mut()) + } + + /// Set an extension value by type. + pub fn set_extension(&mut self, value: T) { + self.extensions.insert(TypeId::of::(), Box::new(value)); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 55adfde..4fe0157 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,8 +1,10 @@ #![allow(dead_code)] -use composable_runtime::graph::{ComponentDefinition, Node, RuntimeFeatureDefinition}; +use composable_runtime::ComponentGraph; +use composable_runtime::graph::Node; use composable_runtime::registry::{ComponentRegistry, RuntimeFeatureRegistry, build_registries}; -use composable_runtime::{ComponentGraph, load_definitions}; +use composable_runtime::types::{ComponentDefinition, RuntimeFeatureDefinition}; +use std::collections::HashMap; use std::io::Write; use std::ops::Deref; use std::path::Path; @@ -132,8 +134,8 @@ pub fn get_runtime_feature_definition<'a>( pub async fn build_registries_and_assert_ok( graph: &ComponentGraph, -) -> (RuntimeFeatureRegistry, ComponentRegistry) { - let registries_result = build_registries(graph, vec![]).await; +) -> (ComponentRegistry, RuntimeFeatureRegistry) { + let registries_result = build_registries(graph, HashMap::new()).await; assert!( registries_result.is_ok(), "build_registries failed with: {:?}", @@ -143,10 +145,14 @@ pub async fn build_registries_and_assert_ok( } pub fn load_graph_and_assert_ok(paths: &[PathBuf]) -> ComponentGraph { - let graph_result = load_definitions(paths); + let mut builder = ComponentGraph::builder(); + for path in paths { + builder = builder.load_file(path); + } + let graph_result = builder.build(); assert!( graph_result.is_ok(), - "load_definitions failed with: {:?}", + "ComponentGraph::builder().build() failed with: {:?}", graph_result.err() ); graph_result.unwrap() diff --git a/tests/config-composition.rs b/tests/config-composition.rs index f1804b9..5b331c0 100644 --- a/tests/config-composition.rs +++ b/tests/config-composition.rs @@ -4,7 +4,7 @@ mod common; async fn empty_config() { let configurable_wasm = common::configurable_wasm(); let graph = common::load_graph_and_assert_ok(&[configurable_wasm.to_path_buf()]); - let (_runtime_registry, component_registry) = + let (component_registry, _runtime_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); @@ -26,7 +26,7 @@ async fn config_value() { ); let toml_file = common::create_toml_test_file(&toml_content); let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); diff --git a/tests/cycle_detection.rs b/tests/cycle_detection.rs index e1e8d57..a67058b 100644 --- a/tests/cycle_detection.rs +++ b/tests/cycle_detection.rs @@ -1,5 +1,5 @@ mod common; -use composable_runtime::load_definitions; +use composable_runtime::ComponentGraph; #[test] #[should_panic(expected = "Circular dependency detected")] @@ -23,5 +23,8 @@ fn test_circular_dependency() { ); let toml_file = common::create_toml_test_file(&toml_content); - load_definitions(&[toml_file.to_path_buf()]).unwrap(); + ComponentGraph::builder() + .load_file(toml_file.to_path_buf()) + .build() + .unwrap(); } diff --git a/tests/direct_wasm_file.rs b/tests/direct_wasm_file.rs index f39b2e2..1072c5f 100644 --- a/tests/direct_wasm_file.rs +++ b/tests/direct_wasm_file.rs @@ -19,7 +19,7 @@ async fn test_direct_wasm_file() { panic!("Node was not a component"); } - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); diff --git a/tests/expects_and_enables.rs b/tests/expects_and_enables.rs index 9b3703f..080a29f 100644 --- a/tests/expects_and_enables.rs +++ b/tests/expects_and_enables.rs @@ -47,7 +47,7 @@ async fn test_expects_and_enables() { assert_eq!(handler_def.enables, "none"); assert!(handler_def.exposed); - let (runtime_feature_registry, component_registry) = + let (component_registry, runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!( runtime_feature_registry diff --git a/tests/host_extensions.rs b/tests/host_extensions.rs index 287d616..8127673 100644 --- a/tests/host_extensions.rs +++ b/tests/host_extensions.rs @@ -1,8 +1,24 @@ mod common; use anyhow::Result; -use composable_runtime::Runtime; -use composable_runtime::registry::{HostExtension, build_registries}; +use composable_runtime::{ComponentState, HostExtension, Runtime}; +use serde::Deserialize; +use std::any::{Any, TypeId}; +use wasmtime::component::Linker; + +/// Test extension that provides a greeter interface +#[derive(Deserialize, Default)] +struct GreeterFeature; + +impl HostExtension for GreeterFeature { + fn interfaces(&self) -> Vec { + vec!["modulewise:test-host/greeter".to_string()] + } + + fn link(&self, _linker: &mut Linker) -> Result<()> { + Ok(()) + } +} fn component_importing_host_interface() -> common::TestFile { let wat = r#" @@ -42,36 +58,28 @@ async fn test_host_extension_provides_interface() { let toml_file = common::create_toml_test_file(&toml_content); let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); - let greeter_extension = HostExtension::new( - "greeter", - vec!["modulewise:test-host/greeter".to_string()], - |_linker| -> Result<()> { Ok(()) }, - ); + // Build runtime with the host extension + let runtime = Runtime::builder(&graph) + .with_host_extension::("greeter") + .build() + .await; - // Build registries with the host extension - let result = build_registries(&graph, vec![greeter_extension]).await; assert!( - result.is_ok(), - "build_registries failed: {:?}", - result.err() + runtime.is_ok(), + "Runtime::builder failed: {:?}", + runtime.err() ); - let (runtime_feature_registry, component_registry) = result.unwrap(); - - // Verify the runtime feature was registered - let feature = runtime_feature_registry.get_runtime_feature("greeter"); - assert!(feature.is_some(), "greeter feature should be registered"); - assert_eq!(feature.unwrap().uri, "host:greeter"); + let runtime = runtime.unwrap(); // Verify the component was registered - assert_eq!(component_registry.get_components().count(), 1); - let component = component_registry.get_components().next().unwrap(); - assert_eq!(component.name, "guest"); - assert!(component.runtime_features.contains(&"greeter".to_string())); + let components: Vec<_> = runtime.list_components(); + assert_eq!(components.len(), 1); + assert_eq!(components[0].name, "guest"); } #[tokio::test] -#[should_panic(expected = "Host extension 'missing' (URI: 'host:missing') not provided")] +#[should_panic(expected = "Host extension 'missing' (URI: 'host:missing') not registered")] async fn test_missing_host_extension_panics() { let component_wasm = component_importing_host_interface(); @@ -92,8 +100,26 @@ async fn test_missing_host_extension_panics() { let toml_file = common::create_toml_test_file(&toml_content); let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); - // Build registries without providing the host extension - should fail - let _ = build_registries(&graph, vec![]).await.unwrap(); + // Build runtime without providing the host extension - should fail + let _ = Runtime::builder(&graph).build().await.unwrap(); +} + +/// Test extension that provides a value-provider interface +#[derive(Deserialize, Default)] +struct ValueProviderFeature; + +impl HostExtension for ValueProviderFeature { + fn interfaces(&self) -> Vec { + vec!["modulewise:test-host/value-provider".to_string()] + } + + fn link(&self, linker: &mut Linker) -> Result<()> { + let mut inst = linker.instance("modulewise:test-host/value-provider")?; + inst.func_wrap("get-value", |_ctx, (): ()| -> Result<(u32,)> { + Ok((42u32,)) + })?; + Ok(()) + } } fn component_calling_host_get_value() -> common::TestFile { @@ -140,19 +166,9 @@ async fn test_host_extension_invoked() { let toml_file = common::create_toml_test_file(&toml_content); let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); - let value_extension = HostExtension::new( - "value-provider", - vec!["modulewise:test-host/value-provider".to_string()], - |linker| -> Result<()> { - let mut inst = linker.instance("modulewise:test-host/value-provider")?; - inst.func_wrap("get-value", |_ctx, (): ()| -> Result<(u32,)> { - Ok((42u32,)) - })?; - Ok(()) - }, - ); - - let runtime = Runtime::from_graph_with_host_extensions(&graph, vec![value_extension]) + let runtime = Runtime::builder(&graph) + .with_host_extension::("value-provider") + .build() .await .expect("Failed to create runtime"); @@ -163,3 +179,388 @@ async fn test_host_extension_invoked() { assert_eq!(result, serde_json::json!(42)); } + +// --- Tests for TOML config --- + +/// Extension that reads config from TOML and uses it in host function +#[derive(Deserialize, Clone)] +struct MultiplierFeature { + #[serde(default = "default_multiplier")] + multiplier: u32, +} + +fn default_multiplier() -> u32 { + 1 +} + +impl Default for MultiplierFeature { + fn default() -> Self { + Self { + multiplier: default_multiplier(), + } + } +} + +impl HostExtension for MultiplierFeature { + fn interfaces(&self) -> Vec { + vec!["modulewise:test-host/multiplier".to_string()] + } + + fn link(&self, linker: &mut Linker) -> Result<()> { + let multiplier = self.multiplier; + let mut inst = linker.instance("modulewise:test-host/multiplier")?; + inst.func_wrap( + "multiply", + move |_ctx, (value,): (u32,)| -> Result<(u32,)> { Ok((value * multiplier,)) }, + )?; + Ok(()) + } +} + +fn component_calling_multiply() -> common::TestFile { + let wat = r#" + (component + (import "modulewise:test-host/multiplier" (instance $mult + (export "multiply" (func (param "value" u32) (result u32))) + )) + (core func $host_multiply (canon lower (func $mult "multiply"))) + (core module $m + (import "" "multiply" (func $imported_multiply (param i32) (result i32))) + (func (export "calc") (result i32) + (call $imported_multiply (i32.const 10)) + ) + ) + (core instance $i (instantiate $m + (with "" (instance (export "multiply" (func $host_multiply)))) + )) + (func $calc (result u32) (canon lift (core func $i "calc"))) + (export "calc" (func $calc)) + ) + "#; + common::create_wasm_test_file(wat) +} + +#[tokio::test] +async fn test_host_extension_with_config() { + let component_wasm = component_calling_multiply(); + + let toml_content = format!( + r#" + [multiplier] + uri = "host:multiplier" + enables = "any" + config.multiplier = 5 + + [guest] + uri = "{}" + expects = ["multiplier"] + exposed = true + "#, + component_wasm.display() + ); + + let toml_file = common::create_toml_test_file(&toml_content); + let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); + + let runtime = Runtime::builder(&graph) + .with_host_extension::("multiplier") + .build() + .await + .expect("Failed to create runtime"); + + let result = runtime + .invoke("guest", "calc", vec![]) + .await + .expect("Failed to invoke"); + + // 10 * 5 = 50 + assert_eq!(result, serde_json::json!(50)); +} + +#[tokio::test] +async fn test_host_extension_with_default_config() { + let component_wasm = component_calling_multiply(); + + // No config.multiplier - should use default value of 1 + let toml_content = format!( + r#" + [multiplier] + uri = "host:multiplier" + enables = "any" + + [guest] + uri = "{}" + expects = ["multiplier"] + exposed = true + "#, + component_wasm.display() + ); + + let toml_file = common::create_toml_test_file(&toml_content); + let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); + + let runtime = Runtime::builder(&graph) + .with_host_extension::("multiplier") + .build() + .await + .expect("Failed to create runtime"); + + let result = runtime + .invoke("guest", "calc", vec![]) + .await + .expect("Failed to invoke"); + + // 10 * 1 = 10 (default multiplier) + assert_eq!(result, serde_json::json!(10)); +} + +// --- Tests for extension state --- + +struct CounterState { + count: u32, +} + +#[derive(Deserialize, Default)] +struct CounterFeature; + +impl HostExtension for CounterFeature { + fn interfaces(&self) -> Vec { + vec!["modulewise:test-host/counter".to_string()] + } + + fn link(&self, linker: &mut Linker) -> Result<()> { + let mut inst = linker.instance("modulewise:test-host/counter")?; + inst.func_wrap("increment", |mut ctx, (): ()| -> Result<(u32,)> { + let state = ctx + .data_mut() + .get_extension_mut::() + .ok_or_else(|| anyhow::anyhow!("CounterState not found"))?; + state.count += 1; + Ok((state.count,)) + })?; + Ok(()) + } + + fn create_state_boxed(&self) -> Result)>> { + Ok(Some(( + TypeId::of::(), + Box::new(CounterState { count: 0 }), + ))) + } +} + +fn component_calling_increment_twice() -> common::TestFile { + let wat = r#" + (component + (import "modulewise:test-host/counter" (instance $counter + (export "increment" (func (result u32))) + )) + (core func $host_increment (canon lower (func $counter "increment"))) + (core module $m + (import "" "increment" (func $imported_increment (result i32))) + (func (export "count-twice") (result i32) + (drop (call $imported_increment)) + (call $imported_increment) + ) + ) + (core instance $i (instantiate $m + (with "" (instance (export "increment" (func $host_increment)))) + )) + (func $count_twice (result u32) (canon lift (core func $i "count-twice"))) + (export "count-twice" (func $count_twice)) + ) + "#; + common::create_wasm_test_file(wat) +} + +#[tokio::test] +async fn test_host_extension_with_state() { + let component_wasm = component_calling_increment_twice(); + + let toml_content = format!( + r#" + [counter] + uri = "host:counter" + enables = "any" + + [guest] + uri = "{}" + expects = ["counter"] + exposed = true + "#, + component_wasm.display() + ); + + let toml_file = common::create_toml_test_file(&toml_content); + let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); + + let runtime = Runtime::builder(&graph) + .with_host_extension::("counter") + .build() + .await + .expect("Failed to create runtime"); + + let result = runtime + .invoke("guest", "count-twice", vec![]) + .await + .expect("Failed to invoke"); + + // increment() called twice + assert_eq!(result, serde_json::json!(2)); +} + +#[tokio::test] +async fn test_host_extension_state_isolated_per_instance() { + let component_wasm = component_calling_increment_twice(); + + let toml_content = format!( + r#" + [counter] + uri = "host:counter" + enables = "any" + + [guest] + uri = "{}" + expects = ["counter"] + exposed = true + "#, + component_wasm.display() + ); + + let toml_file = common::create_toml_test_file(&toml_content); + let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); + + let runtime = Runtime::builder(&graph) + .with_host_extension::("counter") + .build() + .await + .expect("Failed to create runtime"); + + // First invocation + let result1 = runtime + .invoke("guest", "count-twice", vec![]) + .await + .expect("Failed to invoke"); + assert_eq!(result1, serde_json::json!(2)); + + // Second invocation - should start fresh (new instance, new state) + let result2 = runtime + .invoke("guest", "count-twice", vec![]) + .await + .expect("Failed to invoke"); + assert_eq!(result2, serde_json::json!(2)); +} + +// --- Tests for duplicate state type detection --- + +// Shared state type used by two different extensions +#[allow(dead_code)] +struct SharedState { + value: u32, +} + +#[derive(Deserialize, Default)] +struct FirstFeatureWithSharedState; + +impl HostExtension for FirstFeatureWithSharedState { + fn interfaces(&self) -> Vec { + vec!["modulewise:test-host/first".to_string()] + } + + fn link(&self, _linker: &mut Linker) -> Result<()> { + Ok(()) + } + + fn create_state_boxed(&self) -> Result)>> { + Ok(Some(( + TypeId::of::(), + Box::new(SharedState { value: 1 }), + ))) + } +} + +#[derive(Deserialize, Default)] +struct SecondFeatureWithSharedState; + +impl HostExtension for SecondFeatureWithSharedState { + fn interfaces(&self) -> Vec { + vec!["modulewise:test-host/second".to_string()] + } + + fn link(&self, _linker: &mut Linker) -> Result<()> { + Ok(()) + } + + fn create_state_boxed(&self) -> Result)>> { + // Returns same TypeId as FirstFeatureWithSharedState - should cause error + Ok(Some(( + TypeId::of::(), + Box::new(SharedState { value: 2 }), + ))) + } +} + +fn component_importing_two_host_interfaces() -> common::TestFile { + let wat = r#" + (component + (import "modulewise:test-host/first" (instance)) + (import "modulewise:test-host/second" (instance)) + (core module $m + (func (export "run")) + ) + (core instance $i (instantiate $m)) + (func $run (canon lift (core func $i "run"))) + (export "run" (func $run)) + ) + "#; + common::create_wasm_test_file(wat) +} + +#[tokio::test] +async fn test_duplicate_extension_state_type_fails() { + let component_wasm = component_importing_two_host_interfaces(); + + let toml_content = format!( + r#" + [first] + uri = "host:first" + enables = "any" + + [second] + uri = "host:second" + enables = "any" + + [guest] + uri = "{}" + expects = ["first", "second"] + exposed = true + "#, + component_wasm.display() + ); + + let toml_file = common::create_toml_test_file(&toml_content); + let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); + + let runtime = Runtime::builder(&graph) + .with_host_extension::("first") + .with_host_extension::("second") + .build() + .await + .expect("Failed to create runtime"); + + // State is created during instantiation, not during build + let result = runtime.instantiate("guest").await; + + // Should fail because both extensions try to register SharedState + match result { + Ok(_) => panic!("Expected error due to duplicate state type, but instantiation succeeded"), + Err(e) => { + let err = e.to_string(); + assert!( + err.contains("Duplicate extension state type"), + "Expected 'Duplicate extension state type' error, got: {}", + err + ); + } + } +} diff --git a/tests/interceptors.rs b/tests/interceptors.rs index 438fee6..26b0a78 100644 --- a/tests/interceptors.rs +++ b/tests/interceptors.rs @@ -54,7 +54,7 @@ async fn test_simple_interceptor() { assert_eq!(handler_def.enables, "none"); assert!(handler_def.exposed); - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); @@ -126,7 +126,7 @@ async fn test_interceptor_with_enables_scope_mismatch() { provider_name ); - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); } @@ -210,7 +210,7 @@ async fn test_selective_interception_for_exposed_and_unexposed() { unexposed_provider_name ); - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); // Only exposed-handler is exposed } @@ -309,7 +309,7 @@ async fn test_multiple_interceptors() { }; assert_eq!(inner_provider_name, "client"); - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; assert_eq!(component_registry.get_components().count(), 1); } diff --git a/tests/unsatisfied_imports.rs b/tests/unsatisfied_imports.rs index 640ecea..05c0b58 100644 --- a/tests/unsatisfied_imports.rs +++ b/tests/unsatisfied_imports.rs @@ -15,7 +15,7 @@ async fn test_unsatisfied_import_for_exposed_component() { let toml_file = common::create_toml_test_file(&toml_content); let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); - let (_runtime_feature_registry, component_registry) = + let (component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; // Unsatisfied import should cause an exposed component to be skipped. assert_eq!(component_registry.get_components().count(), 0); @@ -37,6 +37,6 @@ async fn test_unsatisfied_import_for_enabling_component() { let toml_file = common::create_toml_test_file(&toml_content); let graph = common::load_graph_and_assert_ok(&[toml_file.to_path_buf()]); - let (_runtime_feature_registry, _component_registry) = + let (_component_registry, _runtime_feature_registry) = common::build_registries_and_assert_ok(&graph).await; }