diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..793c7fc --- /dev/null +++ b/.clippy.toml @@ -0,0 +1,10 @@ +# Modkit allowed to have more complex logic than modules +cognitive-complexity-threshold = 55 +type-complexity-threshold = 220 + +# Function length threshold (default is 100) +too-many-lines-threshold = 200 + +# Allow unwrap/expect in tests and doctests (they should panic on failure) +allow-unwrap-in-tests = true +allow-expect-in-tests = true diff --git a/Cargo.lock b/Cargo.lock index 91da068..84e6132 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "ahash" version = "0.8.12" @@ -122,6 +128,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "auth-git2" version = "0.5.8" @@ -139,6 +151,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -192,6 +226,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "camino" version = "1.2.2" @@ -237,7 +277,7 @@ dependencies = [ "semver", "serde", "tempfile", - "thiserror", + "thiserror 2.0.18", "time", "toml", "walkdir", @@ -263,7 +303,7 @@ dependencies = [ "serde", "serde-untagged", "serde-value", - "thiserror", + "thiserror 2.0.18", "toml", "unicode-xid", "url", @@ -280,7 +320,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -295,6 +335,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -355,20 +401,45 @@ dependencies = [ "anyhow", "cargo-generate", "clap", + "flate2", "liquid", "module-parser", "notify", + "reqwest", + "serde", "serde-saphyr", + "serde_json", + "tar", + "tokio", "toml", "toml_edit", ] +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "console" version = "0.16.2" @@ -402,6 +473,32 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -411,6 +508,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -524,6 +630,12 @@ dependencies = [ "syn", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -620,12 +732,39 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -664,6 +803,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -673,6 +818,45 @@ dependencies = [ "libc", ] +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -690,8 +874,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -731,7 +917,7 @@ dependencies = [ "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -746,7 +932,7 @@ dependencies = [ "gix-date", "gix-utils", "itoa", - "thiserror", + "thiserror 2.0.18", "winnow", ] @@ -766,7 +952,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 2.0.18", "unicode-bom", "winnow", ] @@ -781,7 +967,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -794,7 +980,7 @@ dependencies = [ "itoa", "jiff", "smallvec", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -822,7 +1008,7 @@ dependencies = [ "gix-features", "gix-path", "gix-utils", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -846,7 +1032,7 @@ dependencies = [ "faster-hex", "gix-features", "sha1-checked", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -868,7 +1054,7 @@ checksum = "b9fa71da90365668a621e184eb5b979904471af1b3b09b943a84bc50e8ad42ed" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -888,7 +1074,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror", + "thiserror 2.0.18", "winnow", ] @@ -901,7 +1087,7 @@ dependencies = [ "bstr", "gix-trace", "gix-validate", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -921,7 +1107,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror", + "thiserror 2.0.18", "winnow", ] @@ -973,7 +1159,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b1e63a5b516e970a594f870ed4571a8fdcb8a344e7bd407a20db8bd61dbfde4" dependencies = [ "bstr", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -989,6 +1175,25 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hash32" version = "0.3.1" @@ -1038,6 +1243,108 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -1216,6 +1523,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -1278,6 +1601,28 @@ dependencies = [ "jiff-tzdb", ] +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.34" @@ -1362,6 +1707,7 @@ checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ "bitflags 2.11.0", "libc", + "redox_syscall 0.7.3", ] [[package]] @@ -1471,6 +1817,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "memchr" version = "2.8.0" @@ -1486,6 +1838,22 @@ dependencies = [ "libc", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -1514,7 +1882,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7bddcd3bf5144b6392de80e04c347cd7fab2508f6df16a85fc496ecd5cec39bc" dependencies = [ - "rand", + "rand 0.8.5", ] [[package]] @@ -1608,6 +1976,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -1653,7 +2027,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link", ] @@ -1713,6 +2087,18 @@ dependencies = [ "sha2", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.32" @@ -1786,6 +2172,62 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -1808,8 +2250,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1819,7 +2271,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1831,6 +2293,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1841,14 +2312,23 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.5.2" +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror", + "thiserror 2.0.18", ] [[package]] @@ -1894,6 +2374,46 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rhai" version = "1.23.6" @@ -1922,6 +2442,26 @@ dependencies = [ "syn", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "1.1.4" @@ -1935,6 +2475,81 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1967,7 +2582,16 @@ checksum = "d55ae5ea09894b6d5382621db78f586df37ef18ab581bf32c754e75076b124b1" dependencies = [ "arraydeque", "smallvec", - "thiserror", + "thiserror 2.0.18", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", ] [[package]] @@ -1976,6 +2600,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.27" @@ -1998,9 +2645,9 @@ dependencies = [ [[package]] name = "serde-saphyr" -version = "0.0.20" +version = "0.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcaa44cda9e21eaf5fefc86175d544a359d4de9bcd1f3a90be7bbf77dfc3492" +checksum = "4a6fc4aa0da972ba0f51cf5c1bb16e9dba35334adc6831b09b3ffb0ec20bb264" dependencies = [ "ahash", "annotate-snippets", @@ -2124,6 +2771,18 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -2141,6 +2800,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "socket2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2159,6 +2828,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -2170,6 +2845,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -2181,6 +2865,38 @@ dependencies = [ "syn", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.25.0" @@ -2220,13 +2936,33 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -2305,6 +3041,43 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.9.12+spec-1.1.0" @@ -2366,6 +3139,76 @@ version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typeid" version = "1.0.3" @@ -2429,6 +3272,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -2475,6 +3324,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2512,6 +3370,20 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe88540d1c934c4ec8e6db0afa536876c5441289d7f9f9123d4f065ac1250a6b" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.111" @@ -2578,6 +3450,16 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6bb20ed2d9572df8584f6dc81d68a41a625cadc6f15999d649a70ce7e3597a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -2588,6 +3470,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2625,6 +3516,44 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -2661,6 +3590,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2694,6 +3638,12 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2706,6 +3656,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -2718,6 +3674,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -2742,6 +3704,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -2754,6 +3722,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -2766,6 +3740,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -2778,6 +3758,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -2893,6 +3879,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index 27595f4..018dcfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,12 +23,17 @@ cargo_metadata = { version = "0.23.1" } liquid = { version = "~0.26" } # align with cargo-generate serde = { version = "1.0", features = ["derive"] } -serde-saphyr = { version = "0.0.20" } +serde-saphyr = { version = "0.0.21" } +serde_json = { version = "1.0" } toml = { version = "0.9.12", features = ["serde"] } toml_edit = "0.25.3" notify = { version = "8.2.0", features = ["serde"] } +reqwest = { version = "0.13", features = ["json"] } +tokio = { version = "1", features = ["rt-multi-thread"] } +flate2 = { version = "1.1" } +tar = { version = "0.4" } syn = { version = "2.0.117", features = ["full"] } @@ -39,3 +44,5 @@ unsafe_code = "forbid" pedantic = { level = "warn", priority = -1 } nursery = { level = "warn", priority = -1 } struct_excessive_bools = "allow" +unwrap_used = "deny" +expect_used = "deny" diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 04aa739..f87c483 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -19,7 +19,13 @@ liquid = { workspace = true } module-parser = { workspace = true } notify = { workspace = true } +serde = { workspace = true } serde-saphyr = { workspace = true } +serde_json = { workspace = true } +reqwest = { workspace = true } +tokio = { workspace = true } +flate2 = { workspace = true } +tar = { workspace = true } toml = { workspace = true } toml_edit = { workspace = true } diff --git a/crates/cli/src/build/mod.rs b/crates/cli/src/build/mod.rs index df5bee1..9888209 100644 --- a/crates/cli/src/build/mod.rs +++ b/crates/cli/src/build/mod.rs @@ -12,13 +12,15 @@ impl BuildArgs { pub fn run(&self) -> anyhow::Result<()> { let path = self .build_run_args + .path_config .path .canonicalize() .context("can't canonicalize workspace")?; let config_path = self .build_run_args - .config + .path_config + .resolve_config_with_default(std::path::Path::new("./cyberfabric.yaml")) .canonicalize() .context("can't canonicalize config")?; diff --git a/crates/cli/src/common.rs b/crates/cli/src/common.rs index 58324be..c50cf4f 100644 --- a/crates/cli/src/common.rs +++ b/crates/cli/src/common.rs @@ -1,19 +1,49 @@ -use anyhow::Context; +use anyhow::{Context, bail}; use clap::Args; -use module_parser::{CargoToml, Config, ConfigModuleMetadata, get_module_name_from_crate}; +use module_parser::{ + CargoToml, CargoTomlDependencies, CargoTomlDependency, Config, get_module_name_from_crate, +}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; #[derive(Args)] -pub struct BuildRunArgs { - /// Path to the config file - #[arg(short = 'c', long, default_value = "./cyberfabric.yaml")] - pub config: PathBuf, +pub struct PathConfigArgs { /// Path to the module #[arg(short = 'p', long, default_value = ".")] pub path: PathBuf, + /// Path to the config file + #[arg(short = 'c', long)] + pub config: Option, +} + +impl PathConfigArgs { + pub fn resolve_config_required(&self) -> anyhow::Result { + let Some(config) = &self.config else { + bail!("missing required argument '--config '"); + }; + Ok(self.resolve_config_from(config)) + } + + pub fn resolve_config_with_default(&self, default_config: &Path) -> PathBuf { + let config = self.config.as_deref().unwrap_or(default_config); + self.resolve_config_from(config) + } + + fn resolve_config_from(&self, config: &Path) -> PathBuf { + if config.is_absolute() { + config.to_path_buf() + } else { + self.path.join(config) + } + } +} + +#[derive(Args)] +pub struct BuildRunArgs { + #[command(flatten)] + pub path_config: PathConfigArgs, /// Use OpenTelemetry tracing #[arg(long)] pub otel: bool, @@ -114,12 +144,10 @@ fn create_features() -> HashMap> { res } -fn insert_required_deps( - mut dependencies: HashMap, -) -> HashMap { +fn insert_required_deps(mut dependencies: CargoTomlDependencies) -> CargoTomlDependencies { dependencies.insert( "modkit".to_owned(), - ConfigModuleMetadata { + CargoTomlDependency { package: Some("cf-modkit".to_owned()), features: vec!["bootstrap".to_owned()], ..Default::default() @@ -127,7 +155,7 @@ fn insert_required_deps( ); dependencies.insert( "anyhow".to_owned(), - ConfigModuleMetadata { + CargoTomlDependency { package: Some("anyhow".to_owned()), version: Some("1".to_owned()), ..Default::default() @@ -135,7 +163,7 @@ fn insert_required_deps( ); dependencies.insert( "tokio".to_owned(), - ConfigModuleMetadata { + CargoTomlDependency { package: Some("tokio".to_owned()), features: vec!["full".to_owned()], version: Some("1".to_owned()), @@ -144,7 +172,7 @@ fn insert_required_deps( ); dependencies.insert( "tracing".to_owned(), - ConfigModuleMetadata { + CargoTomlDependency { package: Some("tracing".to_owned()), version: Some("0.1".to_owned()), ..Default::default() @@ -156,7 +184,7 @@ fn insert_required_deps( pub fn generate_server_structure( path: &Path, config_path: &Path, - dependencies: &HashMap, + dependencies: &CargoTomlDependencies, ) -> anyhow::Result<()> { let features = create_features(); @@ -203,7 +231,7 @@ fn create_file_structure(path: &Path, relative_path: &str, contents: &str) -> an /// UNC paths are not supported like `\\server\share`, as we replace backslashes with forward slashes. fn prepare_cargo_server_main( config_path: &Path, - dependencies: &HashMap, + dependencies: &CargoTomlDependencies, ) -> liquid::Object { use std::fmt::Write; let dependencies = dependencies.keys().fold(String::new(), |mut acc, name| { diff --git a/crates/cli/src/config/app_config.rs b/crates/cli/src/config/app_config.rs new file mode 100644 index 0000000..6de2187 --- /dev/null +++ b/crates/cli/src/config/app_config.rs @@ -0,0 +1,143 @@ +use module_parser::ConfigModuleMetadata; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value, json}; +use std::collections::BTreeMap; +use std::path::PathBuf; + +/// Main application configuration with strongly-typed global sections +/// and a flexible per-module configuration bag. +#[derive(Clone, Deserialize, Serialize)] +pub struct AppConfig { + /// Core server configuration. + pub server: ServerConfig, + /// Typed database configuration (optional). + #[serde(default)] + pub database: Option, + /// Logging configuration. + #[serde(default = "default_logging_config")] + pub logging: LoggingConfig, + /// Tracing configuration. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tracing: Option, + /// Directory containing per-module YAML files (optional). + #[serde(default)] + pub modules_dir: Option, + /// Per-module configuration bag: `module_name` -> module config. + #[serde(default)] + pub modules: BTreeMap, +} + +impl Default for AppConfig { + fn default() -> Self { + Self { + server: ServerConfig::default(), + database: None, + logging: default_logging_config(), + tracing: None, + modules_dir: None, + modules: BTreeMap::new(), + } + } +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct ServerConfig { + #[serde(default = "default_home_dir")] + pub home_dir: PathBuf, +} + +impl Default for ServerConfig { + fn default() -> Self { + Self { + home_dir: default_home_dir(), + } + } +} + +/// Logging configuration - maps subsystem names to their logging settings. +pub type LoggingConfig = BTreeMap; + +/// Create a default logging configuration. +#[must_use] +pub fn default_logging_config() -> LoggingConfig { + let mut logging = BTreeMap::new(); + logging.insert( + "default".to_owned(), + json!({ + "console_level": "info", + "file": "logs/cyberfabric.log", + "file_level": "debug", + "max_age_days": 7, + "max_backups": 3, + "max_size_mb": 100 + }), + ); + logging +} + +/// Small typed view to parse each module entry. +#[derive(Clone, Deserialize, Serialize)] +pub struct ModuleConfig { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub database: Option, + #[serde(default = "default_module_config")] + pub config: Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub metadata: Option, +} + +impl Default for ModuleConfig { + fn default() -> Self { + Self { + database: None, + config: default_module_config(), + runtime: None, + metadata: None, + } + } +} + +/// Runtime configuration for a module (local vs out-of-process). +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct ModuleRuntime { + #[serde(default, rename = "type")] + pub mod_type: RuntimeKind, + /// Execution configuration for `OoP` modules. + #[serde(default)] + pub execution: Option, +} + +/// Execution configuration for out-of-process modules. +#[derive(Clone, Deserialize, Serialize, Default)] +pub struct ExecutionConfig { + /// Path to the executable. Supports absolute paths or `~` expansion. + pub executable_path: String, + /// Command-line arguments to pass to the executable. + #[serde(default)] + pub args: Vec, + /// Working directory for the process (optional, defaults to current dir). + #[serde(default)] + pub working_directory: Option, + /// Environment variables to set for the process. + #[serde(default)] + pub environment: BTreeMap, +} + +/// Module runtime kind. +#[derive(Clone, Default, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum RuntimeKind { + #[default] + Local, + Oop, +} + +fn default_home_dir() -> PathBuf { + PathBuf::from(".cyberfabric") +} + +fn default_module_config() -> Value { + Value::Object(Map::new()) +} diff --git a/crates/cli/src/config/mod.rs b/crates/cli/src/config/mod.rs new file mode 100644 index 0000000..8e4bf86 --- /dev/null +++ b/crates/cli/src/config/mod.rs @@ -0,0 +1,29 @@ +use clap::{Args, Subcommand}; + +mod app_config; +mod modules; + +#[derive(Args)] +pub struct ConfigArgs { + #[command(subcommand)] + command: ConfigCommand, +} + +impl ConfigArgs { + pub fn run(&self) -> anyhow::Result<()> { + self.command.run() + } +} + +#[derive(Subcommand)] +pub enum ConfigCommand { + Mod(modules::ModulesArgs), +} + +impl ConfigCommand { + pub fn run(&self) -> anyhow::Result<()> { + match self { + Self::Mod(args) => args.run(), + } + } +} diff --git a/crates/cli/src/config/modules/add.rs b/crates/cli/src/config/modules/add.rs new file mode 100644 index 0000000..65f5a16 --- /dev/null +++ b/crates/cli/src/config/modules/add.rs @@ -0,0 +1,243 @@ +use super::{ + ModulesContext, load_config, resolve_modules_context, save_config, validate_module_name, +}; +use crate::common::PathConfigArgs; +use crate::config::app_config::ModuleConfig; +use anyhow::{Context, bail}; +use clap::Args; +use module_parser::{ConfigModule, ConfigModuleMetadata, get_module_name_from_crate}; +use std::collections::HashMap; + +#[derive(Args)] +pub struct AddArgs { + #[command(flatten)] + path_config: PathConfigArgs, + /// Module name + module: String, + /// Module package name for metadata + #[arg(long)] + package: Option, + /// Module package version for metadata + #[arg(long = "module-version")] + module_version: Option, + /// Whether Cargo default features should be enabled + #[arg(long)] + default_features: Option, + /// Feature to include in metadata (repeatable) + #[arg(long = "feature")] + features: Vec, + /// Dependency name to include in metadata.deps (repeatable) + #[arg(long = "dep")] + deps: Vec, +} + +impl AddArgs { + pub(super) fn run(&self) -> anyhow::Result<()> { + validate_module_name(&self.module)?; + let context = resolve_modules_context(&self.path_config)?; + + let mut config = load_config(&context.config_path)?; + if config.modules.contains_key(&self.module) { + let module = &self.module; + bail!("module '{module}' already exists in modules section"); + } + + let local_modules = discover_local_modules(&context, self)?; + let metadata = build_required_metadata(self, local_modules.get(&self.module))?; + + config.modules.insert( + self.module.clone(), + ModuleConfig { + metadata: Some(metadata), + ..ModuleConfig::default() + }, + ); + + save_config(&context.config_path, &config) + } +} + +fn discover_local_modules( + context: &ModulesContext, + args: &AddArgs, +) -> anyhow::Result> { + match get_module_name_from_crate(&context.workspace_path) { + Ok(modules) => Ok(modules), + Err(_) if args.package.is_some() && args.module_version.is_some() => { + // Allow remote module additions even if the provided -p path is not a Cargo workspace. + Ok(HashMap::new()) + } + Err(err) => Err(err).with_context(|| { + format!( + "failed to discover local modules at {}. \ + if this is a remote module, provide both --package and --module-version", + context.workspace_path.display() + ) + }), + } +} + +fn build_required_metadata( + args: &AddArgs, + local_module: Option<&ConfigModule>, +) -> anyhow::Result { + let mut metadata = local_module.map_or_else(ConfigModuleMetadata::default, |module| { + module.metadata.clone() + }); + + if let Some(package) = &args.package { + metadata.package = Some(package.clone()); + } + if let Some(version) = &args.module_version { + metadata.version = Some(version.clone()); + } + if let Some(default_features) = args.default_features { + metadata.default_features = Some(default_features); + } + // Keep config portable: do not persist local filesystem paths in metadata. + metadata.path = None; + if !args.features.is_empty() { + metadata.features.clone_from(&args.features); + } + if !args.deps.is_empty() { + metadata.deps.clone_from(&args.deps); + } + + validate_required_metadata(args, local_module.is_some(), &metadata)?; + Ok(metadata) +} + +fn validate_required_metadata( + args: &AddArgs, + is_local: bool, + metadata: &ConfigModuleMetadata, +) -> anyhow::Result<()> { + let package_missing = metadata + .package + .as_deref() + .is_none_or(|package| package.trim().is_empty()); + let version_missing = metadata + .version + .as_deref() + .is_none_or(|version| version.trim().is_empty()); + + if !package_missing && !version_missing { + return Ok(()); + } + + let module = &args.module; + if is_local { + bail!("module '{module}' is local, but metadata.package and metadata.version are required"); + } + bail!("module '{module}' is remote, provide both --package and --module-version"); +} + +#[cfg(test)] +mod tests { + use super::{AddArgs, build_required_metadata}; + use crate::common::PathConfigArgs; + use module_parser::{ConfigModule, ConfigModuleMetadata}; + use std::path::PathBuf; + + #[test] + fn build_required_metadata_uses_local_package_and_version() { + let args = AddArgs { + path_config: PathConfigArgs { + path: PathBuf::from("."), + config: None, + }, + module: "demo".to_owned(), + package: None, + module_version: None, + default_features: Some(false), + features: vec!["foo".to_owned(), "bar".to_owned()], + deps: vec!["authz".to_owned()], + }; + let local_module = ConfigModule { + metadata: ConfigModuleMetadata { + package: Some("cf-demo-local".to_owned()), + version: Some("0.3.0".to_owned()), + deps: vec!["tenant-resolver".to_owned()], + ..ConfigModuleMetadata::default() + }, + }; + + let metadata = build_required_metadata(&args, Some(&local_module)).expect("metadata"); + assert_eq!(metadata.package.as_deref(), Some("cf-demo-local")); + assert_eq!(metadata.version.as_deref(), Some("0.3.0")); + assert_eq!(metadata.default_features, Some(false)); + assert_eq!(metadata.path, None); + assert_eq!(metadata.features, vec!["foo", "bar"]); + assert_eq!(metadata.deps, vec!["authz"]); + } + + #[test] + fn build_required_metadata_requires_remote_package() { + let args = AddArgs { + path_config: PathConfigArgs { + path: PathBuf::from("."), + config: None, + }, + module: "demo".to_owned(), + package: None, + module_version: Some("1.2.3".to_owned()), + default_features: None, + features: vec![], + deps: vec![], + }; + + let err = match build_required_metadata(&args, None) { + Ok(_) => panic!("should fail"), + Err(err) => err, + }; + assert!( + err.to_string() + .contains("remote, provide both --package and --module-version") + ); + } + + #[test] + fn build_required_metadata_requires_remote_version() { + let args = AddArgs { + path_config: PathConfigArgs { + path: PathBuf::from("."), + config: None, + }, + module: "demo".to_owned(), + package: Some("cf-demo".to_owned()), + module_version: None, + default_features: None, + features: vec![], + deps: vec![], + }; + + let err = match build_required_metadata(&args, None) { + Ok(_) => panic!("should fail"), + Err(err) => err, + }; + assert!( + err.to_string() + .contains("remote, provide both --package and --module-version") + ); + } + + #[test] + fn build_required_metadata_accepts_remote_with_package_and_version() { + let args = AddArgs { + path_config: PathConfigArgs { + path: PathBuf::from("."), + config: None, + }, + module: "demo".to_owned(), + package: Some("cf-demo".to_owned()), + module_version: Some("1.2.3".to_owned()), + default_features: None, + features: vec![], + deps: vec![], + }; + + let metadata = build_required_metadata(&args, None).expect("metadata"); + assert_eq!(metadata.package.as_deref(), Some("cf-demo")); + assert_eq!(metadata.version.as_deref(), Some("1.2.3")); + } +} diff --git a/crates/cli/src/config/modules/list.rs b/crates/cli/src/config/modules/list.rs new file mode 100644 index 0000000..9d56cda --- /dev/null +++ b/crates/cli/src/config/modules/list.rs @@ -0,0 +1,330 @@ +use super::{SYSTEM_REGISTRY_MODULES, SystemRegistryModule, load_config, resolve_modules_context}; +use crate::common::PathConfigArgs; +use crate::config::app_config::ModuleConfig; +use anyhow::{Context, bail}; +use clap::Args; +use flate2::read::GzDecoder; +use module_parser::{ + Capability, ConfigModule, ConfigModuleMetadata, get_module_name_from_crate, + parse_module_rs_source, +}; +use reqwest::Client; +use serde::Deserialize; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::fmt::Display; +use std::io::{Cursor, Read}; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +#[derive(Args)] +pub struct ListArgs { + #[command(flatten)] + path_config: PathConfigArgs, + /// Show system crates also. If verbose is enabled, + /// fetches registry metadata for system crates. (makes requests to the registry) + #[arg(short = 's', long)] + system: bool, + /// Show all information related to the module. + #[arg(short = 'v', long)] + verbose: bool, + /// Registry to query when verbose mode is enabled. + #[arg(long, default_value = "crates.io")] + registry: String, +} + +impl ListArgs { + pub(super) fn run(&self) -> anyhow::Result<()> { + let context = resolve_modules_context(&self.path_config)?; + let local_modules = discover_workspace_modules(&context.workspace_path)?; + let config = load_config(&context.config_path)?; + let enabled_modules: BTreeSet<_> = config.modules.keys().map(String::as_str).collect(); + + if self.system { + println!("System crates:"); + if self.verbose { + if self.registry != "crates.io" { + let registry = &self.registry; + bail!( + "unsupported registry '{registry}'. Only 'crates.io' is currently supported" + ); + } + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .context("failed to build tokio runtime for registry queries")?; + + let metadata_by_crate = runtime.block_on(fetch_all_crates_io_metadata())?; + + for module in SYSTEM_REGISTRY_MODULES { + let Some(metadata) = metadata_by_crate.get(module.crate_name) else { + bail!("missing fetched metadata for '{}'", module.crate_name); + }; + + print_system_registry_metadata(module, metadata); + } + } else { + for module in SYSTEM_REGISTRY_MODULES { + println!(" - {}", module.module_name); + } + } + } + + println!(); + println!("Workspace modules ({}):", context.workspace_path.display()); + if local_modules.is_empty() { + println!(" (none)"); + } else { + let mut local_entries: Vec<_> = local_modules.iter().collect(); + local_entries.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + for (module_name, module) in local_entries { + let enabled_label = if enabled_modules.contains(module_name.as_str()) { + " (enabled in config)" + } else { + "" + }; + println!(" - {module_name}{enabled_label}"); + + if self.verbose { + print_local_metadata(module); + } + } + } + + println!(); + println!( + "Modules enabled in config ({}):", + context.config_path.display() + ); + if config.modules.is_empty() { + println!(" (none)"); + } else { + let mut configured_entries: Vec<_> = config.modules.iter().collect(); + configured_entries.sort_by(|(left_name, _), (right_name, _)| left_name.cmp(right_name)); + for (module_name, module) in configured_entries { + let location_label = if local_modules.contains_key(module_name.as_str()) { + " (local workspace)" + } else { + " (not found in workspace)" + }; + println!(" - {module_name}{location_label}"); + + if self.verbose { + print_config_metadata(module); + } + } + } + + Ok(()) + } +} + +fn discover_workspace_modules( + workspace_path: &Path, +) -> anyhow::Result> { + let workspace_buf = PathBuf::from(workspace_path); + get_module_name_from_crate(&workspace_buf).with_context(|| { + format!( + "failed to discover workspace modules at {}", + workspace_path.display() + ) + }) +} + +fn print_local_metadata(module: &ConfigModule) { + print_metadata(&module.metadata); +} + +fn print_config_metadata(module: &ModuleConfig) { + let Some(metadata) = &module.metadata else { + println!(" metadata: (none)"); + return; + }; + + print_metadata(metadata); +} + +fn print_system_registry_metadata(module: &SystemRegistryModule, metadata: &RegistryMetadata) { + println!(" - {}", module.module_name); + println!(" crate: {}", module.crate_name); + println!(" latest_version: {}", metadata.latest_version); + print_value_list("features", &metadata.features); + print_value_list("deps", &metadata.deps); + print_value_list("capabilities", &metadata.capabilities); +} + +fn print_metadata(metadata: &ConfigModuleMetadata) { + print_optional_field("package", metadata.package.as_deref()); + print_optional_field("version", metadata.version.as_deref()); + print_optional_field("path", metadata.path.as_deref()); + print_optional_field("default_features", metadata.default_features.as_ref()); + + print_value_list("features", &metadata.features); + print_value_list("deps", &metadata.deps); + print_value_list("capabilities", &metadata.capabilities); +} + +fn print_optional_field(label: &str, value: Option) { + if let Some(value) = value { + println!(" {label}: {value}"); + } +} + +fn print_value_list(label: &str, values: &[T]) { + if values.is_empty() { + println!(" {label}: (none)"); + } else { + println!(" {label}:"); + for value in values { + println!(" - {value}"); + } + } +} + +#[derive(Default)] +struct RegistryMetadata { + latest_version: String, + features: Vec, + deps: Vec, + capabilities: Vec, +} + +#[derive(Deserialize)] +struct CrateResponse { + #[serde(rename = "crate")] + crate_info: CrateInfo, + versions: Vec, +} + +#[derive(Deserialize)] +struct CrateInfo { + max_version: String, +} + +#[derive(Deserialize)] +struct CrateVersion { + num: String, + #[serde(default)] + features: BTreeMap>, +} + +async fn fetch_all_crates_io_metadata() -> anyhow::Result> { + let semaphore = std::sync::Arc::new(tokio::sync::Semaphore::new(4)); + let client = Client::builder() + .user_agent("cyberfabric-cli") + .timeout(Duration::from_secs(10)) + .build() + .context("failed to create registry HTTP client")?; + + let mut join_set = tokio::task::JoinSet::new(); + for module in SYSTEM_REGISTRY_MODULES.iter().copied() { + let cloned_client = client.clone(); + let permit_pool = semaphore.clone(); + join_set.spawn(async move { + let _permit = permit_pool + .acquire_owned() + .await + .context("failed to acquire registry fetch permit")?; + let metadata = fetch_crates_io_metadata(&cloned_client, module) + .await + .with_context(|| format!("failed to fetch metadata for '{}'", module.crate_name))?; + Ok::<_, anyhow::Error>((module.crate_name, metadata)) + }); + } + + let mut metadata_by_crate = HashMap::with_capacity(join_set.len()); + while let Some(task_result) = join_set.join_next().await { + let (crate_name, metadata) = task_result.context("registry task panicked")??; + metadata_by_crate.insert(crate_name, metadata); + } + + Ok(metadata_by_crate) +} + +async fn fetch_crates_io_metadata( + client: &Client, + module: SystemRegistryModule, +) -> anyhow::Result { + let crate_url = format!("https://crates.io/api/v1/crates/{}", module.crate_name); + let crate_response = client + .get(&crate_url) + .send() + .await + .with_context(|| format!("request failed for {}", module.crate_name))? + .error_for_status() + .with_context(|| format!("registry returned an error for {}", module.crate_name))? + .json::() + .await + .with_context(|| format!("invalid crate metadata for {}", module.crate_name))?; + + let latest_version = crate_response.crate_info.max_version; + let features = crate_response + .versions + .into_iter() + .find(|version| version.num == latest_version) + .map_or_else(Vec::new, |version| version.features.into_keys().collect()); + + let module_rs_content = fetch_module_rs_content(client, module, &latest_version).await?; + let module_metadata = parse_module_rs_source(&module_rs_content) + .with_context(|| format!("invalid src/module.rs for {}", module.crate_name))?; + + Ok(RegistryMetadata { + latest_version, + features, + deps: module_metadata.deps, + capabilities: module_metadata.capabilities, + }) +} + +async fn fetch_module_rs_content( + client: &Client, + module: SystemRegistryModule, + latest_version: &str, +) -> anyhow::Result { + let download_url = format!( + "https://crates.io/api/v1/crates/{}/{}/download", + module.crate_name, latest_version + ); + let crate_archive = client + .get(&download_url) + .send() + .await + .with_context(|| format!("download request failed for {}", module.crate_name))? + .error_for_status() + .with_context(|| { + format!( + "download endpoint returned an error for {}", + module.crate_name + ) + })? + .bytes() + .await + .with_context(|| format!("failed to read downloaded source for {}", module.crate_name))?; + + extract_module_rs(crate_archive.as_ref()) + .with_context(|| format!("failed to extract src/module.rs for {}", module.crate_name)) +} + +fn extract_module_rs(crate_archive: &[u8]) -> anyhow::Result { + let decoder = GzDecoder::new(Cursor::new(crate_archive)); + let mut archive = tar::Archive::new(decoder); + let entries = archive + .entries() + .context("failed to list crate archive entries")?; + + for entry in entries { + let mut entry = entry.context("failed to read crate archive entry")?; + let path = entry + .path() + .context("failed to read crate archive entry path")?; + if path.ends_with(Path::new("src/module.rs")) { + let mut module_rs = String::new(); + entry + .read_to_string(&mut module_rs) + .context("failed to read src/module.rs from crate archive")?; + return Ok(module_rs); + } + } + + bail!("crate archive does not contain src/module.rs") +} diff --git a/crates/cli/src/config/modules/mod.rs b/crates/cli/src/config/modules/mod.rs new file mode 100644 index 0000000..a272789 --- /dev/null +++ b/crates/cli/src/config/modules/mod.rs @@ -0,0 +1,148 @@ +use super::app_config::AppConfig; +use crate::common::PathConfigArgs; +use anyhow::Context; +use clap::{Args, Subcommand}; +use std::fs; +use std::path::{Path, PathBuf}; + +mod add; +mod list; +mod remove; + +#[derive(Clone, Copy)] +pub(super) struct SystemRegistryModule { + pub module_name: &'static str, + pub crate_name: &'static str, +} + +pub(super) const SYSTEM_REGISTRY_MODULES: &[SystemRegistryModule] = &[ + SystemRegistryModule { + module_name: "credstore", + crate_name: "cf-credstore", + }, + SystemRegistryModule { + module_name: "file-parser", + crate_name: "cf-file-parser", + }, + SystemRegistryModule { + module_name: "api-gateway", + crate_name: "cf-api-gateway", + }, + SystemRegistryModule { + module_name: "authn-resolver", + crate_name: "cf-authn-resolver", + }, + SystemRegistryModule { + module_name: "static-authn-plugin", + crate_name: "cf-static-authn-plugin", + }, + SystemRegistryModule { + module_name: "authz-resolver", + crate_name: "cf-authz-resolver", + }, + SystemRegistryModule { + module_name: "static-authz-plugin", + crate_name: "cf-static-authz-plugin", + }, + SystemRegistryModule { + module_name: "grpc-hub", + crate_name: "cf-grpc-hub", + }, + SystemRegistryModule { + module_name: "module-orchestrator", + crate_name: "cf-module-orchestrator", + }, + SystemRegistryModule { + module_name: "nodes-registry", + crate_name: "cf-nodes-registry", + }, + SystemRegistryModule { + module_name: "oagw", + crate_name: "cf-oagw", + }, + SystemRegistryModule { + module_name: "single-tenant-tr-plugin", + crate_name: "cf-single-tenant-tr-plugin", + }, + SystemRegistryModule { + module_name: "static-tr-plugin", + crate_name: "cf-static-tr-plugin", + }, + SystemRegistryModule { + module_name: "tenant-resolver", + crate_name: "cf-tenant-resolver", + }, + SystemRegistryModule { + module_name: "types-registry", + crate_name: "cf-types-registry", + }, +]; + +#[derive(Args)] +pub struct ModulesArgs { + #[command(subcommand)] + command: ModulesCommand, +} + +#[derive(Subcommand)] +pub enum ModulesCommand { + /// List available system crates + List(list::ListArgs), + /// Add a module to the modules section + Add(add::AddArgs), + /// Remove a module from the modules section + Rm(remove::RemoveArgs), +} + +pub(super) struct ModulesContext { + workspace_path: PathBuf, + config_path: PathBuf, +} + +impl ModulesArgs { + pub fn run(&self) -> anyhow::Result<()> { + match &self.command { + ModulesCommand::List(args) => args.run(), + ModulesCommand::Add(args) => args.run(), + ModulesCommand::Rm(args) => args.run(), + } + } +} + +pub(super) fn resolve_modules_context( + path_config: &PathConfigArgs, +) -> anyhow::Result { + Ok(ModulesContext { + workspace_path: path_config.path.clone(), + config_path: path_config.resolve_config_required()?, + }) +} + +pub(super) fn load_config(path: &Path) -> anyhow::Result { + let raw = fs::read_to_string(path) + .with_context(|| format!("can't read config file {}", path.display()))?; + serde_saphyr::from_str(&raw).with_context(|| format!("config not valid at {}", path.display())) +} + +pub(super) fn validate_module_name(module: &str) -> anyhow::Result<()> { + if module.is_empty() + || !module + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_') + { + anyhow::bail!("invalid module name '{module}'. Use only letters, numbers, '-' and '_'"); + } + Ok(()) +} + +pub(super) fn save_config(path: &Path, config: &AppConfig) -> anyhow::Result<()> { + let mut serialized = serde_saphyr::to_string(config).context("failed to serialize config")?; + if !serialized.ends_with('\n') { + serialized.push('\n'); + } + let tmp_path = path.with_extension("tmp"); + fs::write(&tmp_path, serialized) + .with_context(|| format!("can't write temp config file {}", tmp_path.display()))?; + fs::rename(&tmp_path, path) + .with_context(|| format!("can't replace config file {}", path.display())) +} diff --git a/crates/cli/src/config/modules/remove.rs b/crates/cli/src/config/modules/remove.rs new file mode 100644 index 0000000..420326c --- /dev/null +++ b/crates/cli/src/config/modules/remove.rs @@ -0,0 +1,27 @@ +use super::{load_config, resolve_modules_context, save_config, validate_module_name}; +use crate::common::PathConfigArgs; +use anyhow::bail; +use clap::Args; + +#[derive(Args)] +pub struct RemoveArgs { + #[command(flatten)] + path_config: PathConfigArgs, + /// Module name + module: String, +} + +impl RemoveArgs { + pub(super) fn run(&self) -> anyhow::Result<()> { + validate_module_name(&self.module)?; + let context = resolve_modules_context(&self.path_config)?; + + let mut config = load_config(&context.config_path)?; + if config.modules.remove(&self.module).is_none() { + let module = &self.module; + bail!("module '{module}' not found in modules section"); + } + + save_config(&context.config_path, &config) + } +} diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 1ce3d28..fba18e3 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -2,6 +2,7 @@ use clap::{Parser, Subcommand}; mod build; mod common; +mod config; mod lint; mod r#mod; mod run; @@ -20,6 +21,7 @@ struct Cli { #[derive(Subcommand)] enum Commands { Mod(r#mod::ModArgs), + Config(config::ConfigArgs), Lint(lint::LintArgs), Test(test::TestArgs), Tools(tools::ToolsArgs), @@ -32,6 +34,7 @@ fn main() -> anyhow::Result<()> { match cli.command { Commands::Mod(r#mod) => r#mod.run(), + Commands::Config(config) => config.run(), Commands::Lint(lint) => lint.run(), Commands::Test(test) => test.run(), Commands::Tools(tools) => tools.run(), diff --git a/crates/cli/src/mod/add.rs b/crates/cli/src/mod/add.rs index 29383de..fd15719 100644 --- a/crates/cli/src/mod/add.rs +++ b/crates/cli/src/mod/add.rs @@ -1,14 +1,15 @@ use anyhow::{Context, bail}; use cargo_generate::{GenerateArgs, TemplatePath, generate}; -use clap::Args; -use module_parser::CargoTomlDependencies; +use clap::{Args, ValueEnum}; +use module_parser::{CargoTomlDependencies, CargoTomlDependency}; use std::fs; use std::path::{Path, PathBuf}; #[derive(Args)] pub struct AddArgs { - /// Kebab-case name of the new module to create (e.g., "my-new-module") - name: String, + /// Module template and module name to generate + #[arg(value_enum)] + name: ModuleTemplateName, /// Path to the workspace root (defaults to current directory) #[arg(short = 'p', long, default_value = ".")] path: PathBuf, @@ -32,16 +33,28 @@ pub struct AddArgs { branch: Option, } -impl AddArgs { - pub fn run(&self) -> anyhow::Result<()> { - if !is_kebab_case(&self.name) { - bail!( - "module name '{}' is not valid kebab-case. \ - Use lowercase letters, numbers, and hyphens (e.g., 'my-module-name').", - self.name - ); +#[derive(Clone, Debug, ValueEnum)] +enum ModuleTemplateName { + #[value(name = "background-worker")] + BackgroundWorker, + #[value(name = "api-db-handler")] + ApiDbHandler, + #[value(name = "rest-gateway")] + RestGateway, +} + +impl ModuleTemplateName { + const fn as_str(&self) -> &'static str { + match self { + Self::BackgroundWorker => "background-worker", + Self::ApiDbHandler => "api-db-handler", + Self::RestGateway => "rest-gateway", } + } +} +impl AddArgs { + pub fn run(&self) -> anyhow::Result<()> { let modules_dir = self.path.join("modules"); if !modules_dir.exists() { @@ -67,10 +80,11 @@ impl AddArgs { } fn generate_module(&self) -> anyhow::Result<(Vec, CargoTomlDependencies)> { + let module_name = self.name.as_str(); let modules_path = self.path.join("modules"); - let module_path = modules_path.join(&self.name); + let module_path = modules_path.join(module_name); if module_path.exists() { - bail!("module {} already exists", self.name); + bail!("module {module_name} already exists"); } let (git, branch) = if self.local_path.is_some() { @@ -79,30 +93,32 @@ impl AddArgs { (self.git.clone(), self.branch.clone()) }; + let auto_path = format!("{}/{}", self.subfolder, module_name); + generate(GenerateArgs { template_path: TemplatePath { - auto_path: Some(self.subfolder.clone()), + auto_path: Some(auto_path), git, path: self.local_path.clone(), branch, ..TemplatePath::default() }, destination: Some(modules_path), - name: Some(self.name.clone()), + name: Some(module_name.to_string()), quiet: !self.verbose, verbose: self.verbose, no_workspace: true, ..GenerateArgs::default() }) - .with_context(|| format!("can't generate module '{}'", self.name))?; + .with_context(|| format!("can't generate module '{module_name}'"))?; let mut dependencies = get_cargo_toml(&module_path).map(|x| get_dependencies(&x))?; - let mut generated = vec![format!("modules/{}", self.name)]; + let mut generated = vec![format!("modules/{}", module_name)]; let sdk_template = module_path.join("sdk"); if sdk_template.exists() { - generated.push(format!("modules/{}/sdk", self.name)); + generated.push(format!("modules/{module_name}/sdk")); dependencies.extend(get_cargo_toml(&sdk_template).map(|x| get_dependencies(&x))?); } @@ -110,16 +126,6 @@ impl AddArgs { } } -fn is_kebab_case(s: &str) -> bool { - if s.is_empty() || s.starts_with('-') || s.ends_with('-') || s.contains("--") { - return false; - } - - // Only lowercase letters, numbers, and hyphens allowed - s.chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') -} - fn get_cargo_toml(path: &Path) -> anyhow::Result { let cargo_toml_path = path.join("Cargo.toml"); fs::read_to_string(&cargo_toml_path) @@ -137,34 +143,68 @@ fn get_dependencies(doc: &toml_edit::DocumentMut) -> CargoTomlDependencies { for (name, value) in dependencies { let metadata = if let Some(dep) = value.as_str() { // Simple string version: `package = "1.0"` - module_parser::ConfigModuleMetadata { + CargoTomlDependency { package: Some(name.to_string()), version: Some(dep.to_string()), ..Default::default() } } else { // Table or inline table: `package = { version = "1.0", ... }` - let (package, version, pkg) = if let Some(table) = value.as_table() { - ( - table.get("package").and_then(|p| p.as_str()), - table.get("version").and_then(|v| v.as_str()), - table.get("path").and_then(|p| p.as_str()), - ) - } else if let Some(inline) = value.as_inline_table() { - ( - inline.get("package").and_then(|p| p.as_str()), - inline.get("version").and_then(|v| v.as_str()), - inline.get("path").and_then(|p| p.as_str()), - ) - } else { - continue; - }; - - module_parser::ConfigModuleMetadata { + let (package, version, pkg, features, default_features) = + if let Some(table) = value.as_table() { + let features = table + .get("features") + .and_then(|f| f.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(ToOwned::to_owned)) + .collect::>() + }) + .unwrap_or_default(); + let default_features = table + .get("default-features") + .or_else(|| table.get("default_features")) + .and_then(toml_edit::Item::as_bool); + + ( + table.get("package").and_then(|p| p.as_str()), + table.get("version").and_then(|v| v.as_str()), + table.get("path").and_then(|p| p.as_str()), + features, + default_features, + ) + } else if let Some(inline) = value.as_inline_table() { + let features = inline + .get("features") + .and_then(|f| f.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(ToOwned::to_owned)) + .collect::>() + }) + .unwrap_or_default(); + let default_features = inline + .get("default-features") + .or_else(|| inline.get("default_features")) + .and_then(toml_edit::Value::as_bool); + + ( + inline.get("package").and_then(|p| p.as_str()), + inline.get("version").and_then(|v| v.as_str()), + inline.get("path").and_then(|p| p.as_str()), + features, + default_features, + ) + } else { + continue; + }; + + CargoTomlDependency { package: package.map(String::from), version: version.map(String::from), path: pkg.map(String::from), - ..Default::default() + features, + default_features, } }; result.insert(name.to_string(), metadata); @@ -217,6 +257,10 @@ fn add_dependencies_to_workspace( dep_table.insert("version", "*".into()); } + if let Some(default_features) = metadata.default_features { + dep_table.insert("default-features", default_features.into()); + } + if !metadata.features.is_empty() { let features_array: toml_edit::Array = metadata .features @@ -226,6 +270,10 @@ fn add_dependencies_to_workspace( dep_table.insert("features", toml_edit::Value::Array(features_array)); } + if let Some(path) = metadata.path { + dep_table.insert("path", path.into()); + } + workspace_deps.insert(&name, toml_edit::Item::Value(dep_table.into())); } diff --git a/crates/cli/src/run/mod.rs b/crates/cli/src/run/mod.rs index 4ced5e2..cea0eb3 100644 --- a/crates/cli/src/run/mod.rs +++ b/crates/cli/src/run/mod.rs @@ -18,13 +18,15 @@ impl RunArgs { pub fn run(&self) -> anyhow::Result<()> { let path = self .br_args + .path_config .path .canonicalize() .context("can't canonicalize workspace")?; let config_path = self .br_args - .config + .path_config + .resolve_config_with_default(std::path::Path::new("./cyberfabric.yaml")) .canonicalize() .context("can't canonicalize config")?; diff --git a/crates/cli/src/run/run_loop.rs b/crates/cli/src/run/run_loop.rs index c460876..5184dc3 100644 --- a/crates/cli/src/run/run_loop.rs +++ b/crates/cli/src/run/run_loop.rs @@ -1,8 +1,7 @@ use crate::common; use anyhow::{Context, bail}; -use module_parser::ConfigModuleMetadata; use notify::{RecursiveMode, Watcher}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::atomic::AtomicBool; @@ -111,7 +110,9 @@ impl RunLoop { if let Err(err) = watcher.unwatch(old) { eprintln!("failed to unwatch {}: {err}", old.display()); _ = signal_tx.send(RunSignal::Stop); - runner_handle.join().expect("runner thread panicked"); + runner_handle.join().map_err(|e| { + anyhow::anyhow!("runner thread panicked: {e:?}") + })?; return Ok(RunSignal::Rerun); } } @@ -120,7 +121,9 @@ impl RunLoop { { eprintln!("failed to watch {}: {err}", new_p.display()); _ = signal_tx.send(RunSignal::Stop); - runner_handle.join().expect("runner thread panicked"); + runner_handle.join().map_err(|e| { + anyhow::anyhow!("runner thread panicked: {e:?}") + })?; return Ok(RunSignal::Rerun); } } @@ -140,7 +143,9 @@ impl RunLoop { // Watcher channel closed - shut down the runner _ = signal_tx.send(RunSignal::Stop); - runner_handle.join().expect("runner thread panicked"); + runner_handle + .join() + .map_err(|e| anyhow::anyhow!("runner thread panicked: {e:?}"))?; Ok(RunSignal::Stop) } @@ -220,7 +225,7 @@ fn cargo_run_loop(cargo_dir: &Path, signal_rx: &mpsc::Receiver) { } fn collect_dep_paths( - deps: &HashMap, + deps: &module_parser::CargoTomlDependencies, base_path: &Path, ) -> HashSet { deps.values() @@ -230,7 +235,7 @@ fn collect_dep_paths( } fn watch_dependency_paths( - deps: &HashMap, + deps: &module_parser::CargoTomlDependencies, watcher: &mut impl Watcher, base_path: &Path, ) -> HashSet { diff --git a/crates/module-parser/src/config.rs b/crates/module-parser/src/config.rs index b054391..abc1698 100644 --- a/crates/module-parser/src/config.rs +++ b/crates/module-parser/src/config.rs @@ -1,6 +1,7 @@ use anyhow::bail; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::fmt; #[derive(Deserialize)] pub struct Config { @@ -8,7 +9,7 @@ pub struct Config { } impl Config { - pub fn create_dependencies(self) -> anyhow::Result> { + pub fn create_dependencies(self) -> anyhow::Result { let mut dependencies = HashMap::with_capacity(self.modules.len()); for (name, module) in self.modules.into_iter() { let Some(package) = module.metadata.package.clone() else { @@ -18,7 +19,17 @@ impl Config { if dependencies.contains_key(&package) { bail!("module '{name}' has duplicate package name '{package}'"); } - dependencies.insert(package, module.metadata); + + dependencies.insert( + package, + CargoTomlDependency { + package: module.metadata.package, + version: module.metadata.version, + features: module.metadata.features, + default_features: module.metadata.default_features, + path: module.metadata.path, + }, + ); } Ok(dependencies) @@ -30,6 +41,33 @@ pub struct ConfigModule { pub metadata: ConfigModuleMetadata, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum Capability { + Db, + Rest, + RestHost, + Stateful, + System, + GrpcHub, + Grpc, +} + +impl fmt::Display for Capability { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let name = match self { + Self::Db => "db", + Self::Rest => "rest", + Self::RestHost => "rest_host", + Self::Stateful => "stateful", + Self::System => "system", + Self::GrpcHub => "grpc_hub", + Self::Grpc => "grpc", + }; + f.write_str(name) + } +} + #[derive(Clone, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct ConfigModuleMetadata { #[serde(skip_serializing_if = "Option::is_none")] @@ -42,10 +80,14 @@ pub struct ConfigModuleMetadata { pub version: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub features: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_features: Option, #[serde(skip_serializing_if = "Option::is_none")] pub path: Option, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub deps: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub capabilities: Vec, } #[derive(Default, Serialize)] @@ -58,7 +100,25 @@ pub struct CargoToml { pub workspace: HashMap>, } -pub type CargoTomlDependencies = HashMap; +pub type CargoTomlDependencies = HashMap; + +#[derive(Clone, Default, PartialEq, Eq, Deserialize, Serialize)] +pub struct CargoTomlDependency { + #[serde(skip_serializing_if = "Option::is_none")] + pub package: Option, + #[serde( + default, + serialize_with = "opt_string_none_as_star::serialize", + deserialize_with = "opt_string_none_as_star::deserialize" + )] + pub version: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub features: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub default_features: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option, +} #[derive(Serialize)] pub struct Package { diff --git a/crates/module-parser/src/lib.rs b/crates/module-parser/src/lib.rs index d387d5b..a6c3240 100644 --- a/crates/module-parser/src/lib.rs +++ b/crates/module-parser/src/lib.rs @@ -4,3 +4,4 @@ mod module_rs; pub use config::*; pub use metadata::*; +pub use module_rs::{ParsedModule, parse_module_rs_source}; diff --git a/crates/module-parser/src/module_rs.rs b/crates/module-parser/src/module_rs.rs index 455af85..77b0f78 100644 --- a/crates/module-parser/src/module_rs.rs +++ b/crates/module-parser/src/module_rs.rs @@ -1,9 +1,17 @@ -use super::config::{ConfigModule, ConfigModuleMetadata}; +use super::config::{Capability, ConfigModule, ConfigModuleMetadata}; use anyhow::Context; use cargo_metadata::{Package, Target}; use std::fs; use std::path::PathBuf; -use syn::{Attribute, Item, Lit}; +use syn::parse::{Parse, ParseStream}; +use syn::{Attribute, Item, Lit, Meta}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParsedModule { + pub name: String, + pub deps: Vec, + pub capabilities: Vec, +} pub(crate) fn retrieve_module_rs( package: &Package, @@ -16,33 +24,47 @@ pub(crate) fn retrieve_module_rs( let module_rs = src.join("module.rs"); let content = fs::read_to_string(&module_rs) .with_context(|| format!("can't read module from {}", module_rs.display()))?; - let ast = syn::parse_file(&content)?; + let parsed_module = parse_module_rs_source(&content) + .with_context(|| format!("invalid {}", module_rs.display()))?; let crate_root = PathBuf::from(&package.manifest_path) .parent() .map(|p| p.display().to_string()); + let config_module = ConfigModule { + metadata: ConfigModuleMetadata { + package: Some(package.name.to_string()), + version: Some(package.version.to_string()), + features: vec![], + default_features: None, + path: crate_root, + deps: parsed_module.deps, + capabilities: parsed_module.capabilities, + }, + }; + Ok((parsed_module.name, config_module)) +} + +pub fn parse_module_rs_source(content: &str) -> anyhow::Result { + let ast = syn::parse_file(content)?; for item in ast.items { if let Item::Struct(struct_item) = item && let Some(module_info) = parse_modkit_module_attribute(&struct_item.attrs)? { - let config_module = ConfigModule { - metadata: ConfigModuleMetadata { - package: Some(package.name.to_string()), - version: Some(package.version.to_string()), - features: vec![], - path: crate_root, - deps: module_info.deps, - }, - }; - return Ok((module_info.name, config_module)); + return Ok(ParsedModule { + name: module_info.name, + deps: module_info.deps, + capabilities: module_info.capabilities, + }); } } + Err(anyhow::anyhow!("no module found")) } struct ModuleInfo { name: String, deps: Vec, + capabilities: Vec, } fn parse_modkit_module_attribute(attrs: &[Attribute]) -> anyhow::Result> { @@ -65,6 +87,7 @@ fn is_modkit_module_path(attr: &Attribute) -> bool { fn parse_module_args(attr: &Attribute) -> anyhow::Result { let mut name = None; let mut deps = Vec::new(); + let mut capabilities = Vec::new(); attr.parse_nested_meta(|meta| { if meta.path.is_ident("name") { @@ -75,31 +98,156 @@ fn parse_module_args(attr: &Attribute) -> anyhow::Result { } } else if meta.path.is_ident("deps") { let value = meta.value()?; - let content; - syn::bracketed!(content in value); - while !content.is_empty() { - let lit: Lit = content.parse()?; - if let Lit::Str(lit_str) = lit { - deps.push(lit_str.value()); - } - if !content.is_empty() { - let _: syn::token::Comma = content.parse()?; + let expr: syn::Expr = value.parse()?; + if let syn::Expr::Array(array) = expr { + for element in array.elems { + if let syn::Expr::Lit(syn::ExprLit { + lit: Lit::Str(lit_str), + .. + }) = element + { + deps.push(lit_str.value()); + } } } } else if meta.path.is_ident("capabilities") { let value = meta.value()?; - let content; - syn::bracketed!(content in value); - while !content.is_empty() { - let _ident: syn::Ident = content.parse()?; - if !content.is_empty() { - let _: syn::token::Comma = content.parse()?; + let expr: syn::Expr = value.parse()?; + if let syn::Expr::Array(array) = expr { + for element in array.elems { + let capability = match element { + syn::Expr::Path(path_expr) => { + let Some(ident) = path_expr.path.get_ident() else { + return Err(syn::Error::new_spanned( + path_expr.path, + "capability must be a simple identifier", + )); + }; + parse_capability_name(&ident.to_string()).ok_or_else(|| { + syn::Error::new_spanned( + ident, + "unknown capability, expected one of: db, rest, rest_host, stateful, system, grpc_hub, grpc", + ) + })? + } + syn::Expr::Lit(syn::ExprLit { + lit: Lit::Str(lit_str), + .. + }) => parse_capability_name(&lit_str.value()).ok_or_else(|| { + syn::Error::new_spanned( + lit_str, + "unknown capability, expected one of: db, rest, rest_host, stateful, system, grpc_hub, grpc", + ) + })?, + other => { + return Err(syn::Error::new_spanned( + other, + "capability must be an identifier or string literal", + )); + } + }; + capabilities.push(capability); } + } else { + return Err(syn::Error::new_spanned( + expr, + "capabilities must be an array, e.g. capabilities = [db, rest]", + )); } + } else { + consume_unknown_meta(meta.input)?; } Ok(()) })?; let name = name.context("module attribute must have a name")?; - Ok(ModuleInfo { name, deps }) + Ok(ModuleInfo { + name, + deps, + capabilities, + }) +} + +fn parse_capability_name(name: &str) -> Option { + match name { + "db" => Some(Capability::Db), + "rest" => Some(Capability::Rest), + "rest_host" => Some(Capability::RestHost), + "stateful" => Some(Capability::Stateful), + "system" => Some(Capability::System), + "grpc_hub" => Some(Capability::GrpcHub), + "grpc" => Some(Capability::Grpc), + _ => None, + } +} + +fn consume_unknown_meta(input: ParseStream<'_>) -> syn::Result<()> { + if input.peek(syn::Token![=]) { + let _: syn::Token![=] = input.parse()?; + let _expr: syn::Expr = input.parse()?; + } else if input.peek(syn::token::Paren) { + let content; + syn::parenthesized!(content in input); + let _nested: syn::punctuated::Punctuated = + content.parse_terminated(Meta::parse, syn::Token![,])?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::parse_module_rs_source; + use crate::Capability; + + #[test] + fn parses_module_with_lifecycle_meta() { + let content = r#" + #[modkit::module( + name = "grpc-hub", + capabilities = [stateful, system, grpc_hub], + lifecycle(entry = "serve", await_ready) + )] + pub struct GrpcHub; + "#; + + let parsed = parse_module_rs_source(content).expect("module should parse"); + assert_eq!(parsed.name, "grpc-hub"); + assert!(parsed.deps.is_empty()); + assert_eq!( + parsed.capabilities, + vec![ + Capability::Stateful, + Capability::System, + Capability::GrpcHub + ] + ); + } + + #[test] + fn parses_deps_list() { + let content = r#" + #[module(name = "demo", deps = ["authz", "tenant-resolver"])] + pub struct Demo; + "#; + + let parsed = parse_module_rs_source(content).expect("module should parse"); + assert_eq!(parsed.name, "demo"); + assert_eq!(parsed.deps, vec!["authz", "tenant-resolver"]); + assert!(parsed.capabilities.is_empty()); + } + + #[test] + fn parses_capabilities_from_strings() { + let content = r#" + #[module(name = "demo", capabilities = ["db", "rest_host"])] + pub struct Demo; + "#; + + let parsed = parse_module_rs_source(content).expect("module should parse"); + assert_eq!(parsed.name, "demo"); + assert_eq!( + parsed.capabilities, + vec![Capability::Db, Capability::RestHost] + ); + } }