diff options
Diffstat (limited to 'mingling_cli')
| -rw-r--r-- | mingling_cli/Cargo.lock | 592 | ||||
| -rw-r--r-- | mingling_cli/Cargo.toml | 8 | ||||
| -rw-r--r-- | mingling_cli/help/help.txt | 4 | ||||
| -rw-r--r-- | mingling_cli/src/bin/cli.rs | 3 | ||||
| -rw-r--r-- | mingling_cli/src/proj_mgr.rs | 3 | ||||
| -rw-r--r-- | mingling_cli/src/proj_mgr/cmd_proj_init.rs | 356 | ||||
| -rw-r--r-- | mingling_cli/src/proj_mgr/rule_solver.rs | 481 |
7 files changed, 1439 insertions, 8 deletions
diff --git a/mingling_cli/Cargo.lock b/mingling_cli/Cargo.lock index ddfe01c..e5993b4 100644 --- a/mingling_cli/Cargo.lock +++ b/mingling_cli/Cargo.lock @@ -3,6 +3,23 @@ version = 4 [[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] name = "annotate-snippets" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -20,6 +37,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] name = "arg-picker" version = "0.1.1" dependencies = [ @@ -43,12 +69,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] name = "bytes" version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] name = "camino" version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -83,12 +149,34 @@ dependencies = [ ] [[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] name = "colored" version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -98,6 +186,61 @@ dependencies = [ ] [[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[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-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] name = "darling" version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -133,6 +276,29 @@ dependencies = [ ] [[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] name = "derive_builder" version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -164,6 +330,17 @@ dependencies = [ ] [[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -185,6 +362,17 @@ dependencies = [ ] [[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.2", +] + +[[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -201,12 +389,38 @@ dependencies = [ ] [[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[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 = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -218,12 +432,46 @@ dependencies = [ ] [[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -240,12 +488,41 @@ dependencies = [ ] [[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] name = "just_fmt" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -259,9 +536,9 @@ checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96" [[package]] name = "just_template" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb99a3c1dee7299c57b26ef927f15535da0fbc93d6deb1d1114cae1337be4fb" +checksum = "f2a56a31287e7397340dd8d9997c8fd936ee68b27cb3f090ece2984e8a84d5bc" dependencies = [ "just_fmt 0.1.2", "just_template_macros", @@ -303,6 +580,33 @@ dependencies = [ ] [[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] name = "memchr" version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -347,6 +651,7 @@ dependencies = [ "syn 3.0.2", "tokio", "toml_edit 0.25.13+spec-1.1.0", + "zip", ] [[package]] @@ -379,6 +684,16 @@ dependencies = [ ] [[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.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -390,6 +705,18 @@ dependencies = [ ] [[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -419,12 +746,34 @@ dependencies = [ ] [[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] name = "proc-macro2" version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -443,6 +792,18 @@ dependencies = [ ] [[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -457,12 +818,18 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom", + "getrandom 0.2.17", "libredox", "thiserror", ] [[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -531,6 +898,23 @@ dependencies = [ ] [[package]] +name = "sha1" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] name = "signal-hook-registry" version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -541,6 +925,12 @@ dependencies = [ ] [[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -563,6 +953,12 @@ 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.119" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -605,6 +1001,25 @@ dependencies = [ ] [[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] name = "tokio" version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -711,6 +1126,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" [[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -723,12 +1144,72 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -762,7 +1243,112 @@ dependencies = [ ] [[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] name = "zmij" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/mingling_cli/Cargo.toml b/mingling_cli/Cargo.toml index 2bb3fd3..a9b484a 100644 --- a/mingling_cli/Cargo.toml +++ b/mingling_cli/Cargo.toml @@ -61,13 +61,19 @@ colored = "3.1.1" dirs = "6.0.0" semver = "1.0.28" +# Code gen +just_template = "0.2.1" + +# Compress +zip = "2" + [build-dependencies] # Configure & Serialization serde = { version = "1.0.229", features = ["derive"] } serde_json = "1.0.151" # Code gen -just_template = "0.2.0" +just_template = "0.2.1" [profile.release] opt-level = "z" diff --git a/mingling_cli/help/help.txt b/mingling_cli/help/help.txt index 0fff769..9e35194 100644 --- a/mingling_cli/help/help.txt +++ b/mingling_cli/help/help.txt @@ -21,8 +21,8 @@ __ ra-lint-clippy *Run* `mling lint` *and* `cargo clippy`*, an __ explain <LINT> *Explain the meaning of the specified Lint* __ **PROJECT MANAGE:** -__ proj-new <NAME> **[NOT_IMPL]** *Create a project* -__ proj-init **[NOT_IMPL]** *Initialize a project* +__ proj-init <TEMPLATE> *Initialize a project* +__ class-add <CLASS> <PARAMS...> **[NOT_IMPL]** *Create a Class of the specified type in the project* __ **PACKAGE MANAGE:** __ install *Install the project to the Mingling package list* diff --git a/mingling_cli/src/bin/cli.rs b/mingling_cli/src/bin/cli.rs index 6f7556e..2e6c972 100644 --- a/mingling_cli/src/bin/cli.rs +++ b/mingling_cli/src/bin/cli.rs @@ -1,4 +1,4 @@ -use mingling::setup::{ExitCodeSetup, picker::HelpFlagSetup}; +use mingling::setup::{DirectoryEnvironmentSetup, ExitCodeSetup, picker::HelpFlagSetup}; use mingling_cli::{ ThisProgram, linter::registry::LintRegistrySetup, metadata::MinglingMetadataSetup, pkg_mgr::PackageManagerSetup, @@ -11,6 +11,7 @@ async fn main() { // Setups program.with_setup(HelpFlagSetup::default()); program.with_setup(ExitCodeSetup::default()); + program.with_setup(DirectoryEnvironmentSetup::default()); program.with_setup(MinglingMetadataSetup); program.with_setup(LintRegistrySetup); diff --git a/mingling_cli/src/proj_mgr.rs b/mingling_cli/src/proj_mgr.rs index 8b13789..509f37d 100644 --- a/mingling_cli/src/proj_mgr.rs +++ b/mingling_cli/src/proj_mgr.rs @@ -1 +1,2 @@ - +pub mod cmd_proj_init; +pub mod rule_solver; diff --git a/mingling_cli/src/proj_mgr/cmd_proj_init.rs b/mingling_cli/src/proj_mgr/cmd_proj_init.rs new file mode 100644 index 0000000..3dfad9c --- /dev/null +++ b/mingling_cli/src/proj_mgr/cmd_proj_init.rs @@ -0,0 +1,356 @@ +use std::{ + collections::HashMap, + fs::{self, File}, + io, + path::{Path, PathBuf}, +}; + +use just_template::Template; +use mingling::{ + Grouped, RenderResult, Routable, + macros::{arg, chain, command, metadata, pack, pack_err, r_println, renderer, routeify}, + metadata::Description, + picker::{EntryPicker, value::FilePath}, + res::ResCurrentDir, +}; + +use crate::{Entry, Next, eprintln_cargo, hprintln_cargo, println_cargo}; + +use super::rule_solver::{eval_rule, parse_checklist, parse_rules, resolve_answers}; + +/// The name of the checklist file the user edits and we re-read on generate. +const CHECKLIST_FILENAME: &str = "checklist.toml"; +/// The name of the rules file declaring default params, display blocks and hides. +const RULE_FILENAME: &str = "rule.toml"; +/// The directory under the project root where the template cache lives. +const CACHE_DIR_NAME: &str = "tmpl-cache"; +/// The internal `.mling` directory name inside the template archive (with `tmpl_` prefix). +const TEMPLATE_MLING_DIR: &str = "tmpl_.mling"; +/// Prefix marking directories/files that participate in expansion. +const TMPL_PREFIX: &str = "tmpl_"; + +pack!(StateProjectGenerate = ()); +pack!(StateProjectChecklistReady = Vec<String>); + +/// Result of the checklist phase: the extracted checklist handed to the user. +#[derive(Debug, Default, Grouped)] +pub struct ResultProjectChecklistReady { + pub checklist: PathBuf, +} + +/// Result of the generate phase: files produced (and hidden) by expansion. +#[derive(Debug, Default, Grouped)] +pub struct ResultProjectGenerate { + pub generated: Vec<PathBuf>, + pub hidden: Vec<PathBuf>, +} + +pack_err!(ErrorTemplateZipNotProvided = ()); +pack_err!(ErrorUnzipFailed = String); +pack_err!(ErrorChecklistMissing = String); +pack_err!(ErrorRuleParseFailed = String); +pack_err!(ErrorTemplateExpandFailed = String); + +#[command(node = "proj-init", routeify)] +pub fn proj_init(args: Entry, cwd: &ResCurrentDir) -> Next { + // Check if the checklist.toml file exists in the current directory + if cwd.join(CHECKLIST_FILENAME).exists() { + StateProjectGenerate::new(()).into() + } else { + StateProjectChecklistReady::new(args.inner).into() + } +} + +/// Phase 1: extract the user-provided template archive into +/// `./.mling/tmpl-cache/` and hand the checklist over for editing. +#[chain(routeify)] +pub fn handle_state_proj_checklist_ready( + args: StateProjectChecklistReady, + cwd: &ResCurrentDir, +) -> Next { + let tmpl_file: Option<FilePath> = args.pick(&arg![Option<FilePath>]).to_result()?; + + // Validate + let Some(tmpl_file) = tmpl_file else { + return ErrorTemplateZipNotProvided::new(()).to_chain(); + }; + + // Extract the file into the .mling directory under the current directory; create it if it doesn't exist + let tmpl_cache = cwd.join(".mling").join(CACHE_DIR_NAME); + fs::create_dir_all(&tmpl_cache).map_err(|e| { + ErrorUnzipFailed::new(format!("failed to create {}: {e}", tmpl_cache.display())) + })?; + unzip_to(&tmpl_file, &tmpl_cache).map_err(ErrorUnzipFailed::new)?; + + // Move the internal checklist.toml to ./ for the user to fill in + let checklist_src = tmpl_cache.join(CHECKLIST_FILENAME); + if !checklist_src.is_file() { + return ErrorChecklistMissing::new(format!( + "no checklist.toml found inside {}", + tmpl_file.display() + )) + .to_chain(); + } + let checklist_dst = cwd.join(CHECKLIST_FILENAME); + fs::rename(&checklist_src, &checklist_dst).map_err(|e| { + ErrorUnzipFailed::new(format!( + "failed to move checklist.toml to {}: {e}", + checklist_dst.display() + )) + })?; + + ResultProjectChecklistReady { + checklist: checklist_dst, + } + .to_chain() +} + +/// Phase 2: expand the cached template with the checklist answers, driven by +/// `rule.toml` display/hide rules, then clean up the cache. +#[chain(routeify)] +pub fn handle_state_project_generate(_: StateProjectGenerate, cwd: &ResCurrentDir) -> Next { + let tmpl_cache = cwd.join(".mling").join(CACHE_DIR_NAME); + if !tmpl_cache.is_dir() { + return ErrorChecklistMissing::new(format!( + "template cache not found at {}; run `mling proj-init` with a template archive first", + tmpl_cache.display() + )) + .to_chain(); + } + + // Read the user-filled checklist.toml + let checklist_path = cwd.join(CHECKLIST_FILENAME); + let checklist_content = fs::read_to_string(&checklist_path).map_err(|e| { + ErrorChecklistMissing::new(format!("failed to read {}: {e}", checklist_path.display())) + })?; + let answers = parse_checklist(&checklist_content) + .map_err(|e| ErrorRuleParseFailed::new(format!("invalid checklist.toml: {e}")))?; + + // Read rule.toml (template rules) + let rule_content = fs::read_to_string(tmpl_cache.join(RULE_FILENAME)) + .map_err(|e| ErrorRuleParseFailed::new(format!("failed to read rule.toml: {e}")))?; + let rules = parse_rules(&rule_content) + .map_err(|e| ErrorRuleParseFailed::new(format!("invalid rule.toml: {e}")))?; + + // Compute final answers from checklist values + defaults declared in rule.toml + let answers = resolve_answers(&answers, &rules); + + // Compute display block toggles based on rules: checklist values + rules whose display condition is true + + let mut params: HashMap<String, String> = answers.clone(); + for display in &rules.display { + if eval_rule(&display.rule, &answers) { + params.insert(display.name.clone(), String::new()); + } + } + + // Expand all tmpl_* entries to the project root + let mut generated = Vec::new(); + expand_tree(&tmpl_cache, cwd, ¶ms, &mut generated) + .map_err(ErrorTemplateExpandFailed::new)?; + + // hide-file: delete the corresponding generated file when the rule is true + + let mut hidden = Vec::new(); + for hide in &rules.hide_files { + if !eval_rule(&hide.rule, &answers) { + continue; + } + let target = cwd.join(hide.file.trim_start_matches("./")); + remove_path(&target).map_err(|e| { + ErrorTemplateExpandFailed::new(format!("failed to hide {}: {e}", target.display())) + })?; + hidden.push(target); + } + + // Clean up the cache + fs::remove_dir_all(&tmpl_cache).map_err(|e| { + ErrorTemplateExpandFailed::new(format!("failed to remove {}: {e}", tmpl_cache.display())) + })?; + + // Project generated; remove the temporary checklist file + remove_path(&checklist_path).map_err(|e| { + ErrorTemplateExpandFailed::new(format!( + "failed to remove {}: {e}", + checklist_path.display() + )) + })?; + + ResultProjectGenerate { generated, hidden }.to_chain() +} + +/// Extract a ZIP archive into `dest`, guarding against path traversal. +fn unzip_to(zip_path: &Path, dest: &Path) -> Result<(), String> { + let file = File::open(zip_path).map_err(|e| e.to_string())?; + let mut archive = zip::ZipArchive::new(file).map_err(|e| e.to_string())?; + + for i in 0..archive.len() { + let mut entry = archive.by_index(i).map_err(|e| e.to_string())?; + let Some(rel) = entry.enclosed_name() else { + return Err(format!("illegal path in archive: {}", entry.name())); + }; + let out_path = dest.join(rel); + if entry.is_dir() { + fs::create_dir_all(&out_path).map_err(|e| e.to_string())?; + } else { + if let Some(parent) = out_path.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + let mut out = File::create(&out_path).map_err(|e| e.to_string())?; + io::copy(&mut entry, &mut out).map_err(|e| e.to_string())?; + } + } + Ok(()) +} + +/// Recursively expand the template cache into the project root. +/// +/// Every path component with a `tmpl_` prefix is stripped for the target path +/// (`tmpl_src/tmpl_main.rs` -> `src/main.rs`). `tmpl_`-prefixed files are +/// rendered through `just_template` with the resolved params; other files are +/// skipped. `tmpl_.mling` is copied into `./.mling` without rendering. +fn expand_tree( + src_root: &Path, + dst_root: &Path, + params: &HashMap<String, String>, + generated: &mut Vec<PathBuf>, +) -> Result<(), String> { + for entry in fs::read_dir(src_root).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let name = entry.file_name().to_string_lossy().into_owned(); + let src = entry.path(); + + // The template's own .mling tree is copied verbatim (minus `tmpl_` prefixes). + if name == TEMPLATE_MLING_DIR && src.is_dir() { + copy_tree(&src, &dst_root.join(".mling")).map_err(|e| e.to_string())?; + continue; + } + + let target_name = name + .strip_prefix(TMPL_PREFIX) + .map(str::to_owned) + .unwrap_or(name.clone()); + let dst = dst_root.join(&target_name); + + if src.is_dir() { + fs::create_dir_all(&dst).map_err(|e| e.to_string())?; + expand_tree(&src, &dst, params, generated)?; + } else if src.is_file() && name.starts_with(TMPL_PREFIX) { + let content = fs::read_to_string(&src).map_err(|e| e.to_string())?; + let mut tmpl = Template::from(content); + for (key, value) in params { + tmpl.insert_param(key.clone(), value.clone()); + } + let expanded = tmpl + .expand() + .ok_or_else(|| format!("failed to expand template: {}", src.display()))?; + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent).map_err(|e| e.to_string())?; + } + fs::write(&dst, expanded).map_err(|e| e.to_string())?; + generated.push(dst); + } + } + Ok(()) +} + +/// Copy a directory tree into `dst`, stripping the `tmpl_` prefix from every +/// component name (e.g. `tmpl_command` -> `command`). +fn copy_tree(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let name = entry.file_name().to_string_lossy().into_owned(); + let target_name = name + .strip_prefix(TMPL_PREFIX) + .map(str::to_owned) + .unwrap_or(name); + let from = entry.path(); + let to = dst.join(&target_name); + if from.is_dir() { + copy_tree(&from, &to)?; + } else if from.is_file() { + fs::copy(&from, &to)?; + } + } + Ok(()) +} + +/// Remove a file or directory, ignoring "not found". +fn remove_path(path: &Path) -> io::Result<()> { + if path.is_dir() { + fs::remove_dir_all(path) + } else if path.is_file() { + fs::remove_file(path) + } else { + Ok(()) + } +} + +#[renderer] +pub fn render_result_project_checklist_ready(result: ResultProjectChecklistReady) -> RenderResult { + let mut r = RenderResult::new(); + r_println!(r, "Template extracted."); + r_println!(r, ""); + hprintln_cargo!( + r, + "Fill out {} and run `mling proj-init` again to generate the project.", + result.checklist.display() + ); + r +} + +#[renderer] +pub fn render_result_project_generate(result: ResultProjectGenerate) -> RenderResult { + let mut r = RenderResult::new(); + for file in &result.generated { + println_cargo!(r, "Generated: {}", file.display()); + } + for file in &result.hidden { + println_cargo!(r, "Hidden: {}", file.display()); + } + r +} + +#[renderer] +pub fn render_error_template_zip_not_provided(_err: ErrorTemplateZipNotProvided) -> RenderResult { + let mut r = RenderResult::new(); + eprintln_cargo!( + r, + "no template archive provided; pass the path to a mingling template zip file" + ); + r +} + +#[renderer] +pub fn render_error_unzip_failed(err: ErrorUnzipFailed) -> RenderResult { + let mut r = RenderResult::new(); + eprintln_cargo!(r, "failed to extract template: {}", err.info); + r +} + +#[renderer] +pub fn render_error_checklist_missing(err: ErrorChecklistMissing) -> RenderResult { + let mut r = RenderResult::new(); + eprintln_cargo!(r, "{}", err.info); + r +} + +#[renderer] +pub fn render_error_rule_parse_failed(err: ErrorRuleParseFailed) -> RenderResult { + let mut r = RenderResult::new(); + eprintln_cargo!(r, "{}", err.info); + r +} + +#[renderer] +pub fn render_error_template_expand_failed(err: ErrorTemplateExpandFailed) -> RenderResult { + let mut r = RenderResult::new(); + eprintln_cargo!(r, "{}", err.info); + r +} + +#[metadata(EntryProjInit)] +pub fn desc_proj_init() -> Description { + "Guided creation of a Mingling project".into() +} diff --git a/mingling_cli/src/proj_mgr/rule_solver.rs b/mingling_cli/src/proj_mgr/rule_solver.rs new file mode 100644 index 0000000..0e00458 --- /dev/null +++ b/mingling_cli/src/proj_mgr/rule_solver.rs @@ -0,0 +1,481 @@ +//! Parsing and evaluation of `rule.toml` and `checklist.toml`. +//! +//! `rule.toml` drives template generation: +//! - `[[display]]` entries map a display-block name to a boolean rule; when the +//! rule evaluates to true, the corresponding `??? >>> NAME` block is enabled. +//! - `[[hide-file]]` entries map a generated file path to a boolean rule; when +//! the rule evaluates to true, the generated file is removed. +//! +//! `checklist.toml` holds the user's answers as key/value pairs and provides +//! the variables the rules are evaluated against. + +use std::collections::HashMap; + +use toml_edit::DocumentMut; + +/// A `[[display]]` rule: enable display block `name` when `rule` is true. +#[derive(Debug, Clone)] +pub struct DisplayRule { + /// Display block name, e.g. `ASYNC`, `PARSER_PICKER`. + pub name: String, + /// Boolean expression, e.g. `tokio || async_std`. + pub rule: String, +} + +/// A `[[hide-file]]` rule: hide `file` when `rule` is true. +#[derive(Debug, Clone)] +pub struct HideFileRule { + /// File path relative to the project root, e.g. `./build.rs`. + pub file: String, + /// Boolean expression, e.g. `!completion && !pathf`. + pub rule: String, +} + +/// A `[[user.*]]` entry describing a checklist answer and its default. +/// +/// Checklist keys are declared under `[[user.input]]`, `[[user.toggle]]` or +/// `[[user.selection]]`; toggles and selections may carry a `default` value +/// that applies when the user leaves the key unset in `checklist.toml`. +#[derive(Debug, Clone)] +pub struct UserRule { + /// Checklist key name, e.g. `program_name`, `tokio`, `parser`. + pub name: String, + /// Default value (stringified), if declared in `rule.toml`. + pub default: Option<String>, +} + +/// All rules parsed from `rule.toml`. +#[derive(Debug, Default, Clone)] +pub struct TemplateRules { + pub users: Vec<UserRule>, + pub display: Vec<DisplayRule>, + pub hide_files: Vec<HideFileRule>, +} + +/// Parse `checklist.toml` into a flat map of answers. +/// +/// Values are stringified: strings keep their content, booleans become +/// `"true"` / `"false"`, numbers keep their decimal representation. +/// Commented-out (disabled) keys are ignored by the TOML parser. +pub fn parse_checklist(content: &str) -> Result<HashMap<String, String>, String> { + let doc = content.parse::<DocumentMut>().map_err(|e| e.to_string())?; + let mut answers = HashMap::new(); + for (key, item) in doc.iter() { + let toml_edit::Item::Value(value) = item else { + continue; + }; + let stringified = match value { + toml_edit::Value::String(s) => s.value().clone(), + toml_edit::Value::Boolean(b) => b.value().to_string(), + toml_edit::Value::Integer(i) => i.value().to_string(), + toml_edit::Value::Float(f) => f.value().to_string(), + // Arrays, inline tables and datetimes are not valid checklist answers. + _ => continue, + }; + answers.insert(key.to_string(), stringified); + } + Ok(answers) +} + +/// Parse `rule.toml` into display and hide-file rules. +pub fn parse_rules(content: &str) -> Result<TemplateRules, String> { + let doc = content.parse::<DocumentMut>().map_err(|e| e.to_string())?; + let mut rules = TemplateRules::default(); + + // `[[user.input]]`, `[[user.toggle]]`, `[[user.selection]]` parse into a + // `user` table whose keys (`input`/`toggle`/`selection`) hold the arrays. + if let Some(user_table) = doc.get("user").and_then(|item| item.as_table()) { + for kind in ["input", "toggle", "selection"] { + let Some(tables) = user_table + .get(kind) + .and_then(|item| item.as_array_of_tables()) + else { + continue; + }; + for table in tables { + let name = table + .get("name") + .and_then(|v| v.as_str()) + .ok_or("[[user.*]] entry is missing `name`")? + .to_string(); + let default = + table + .get("default") + .and_then(|v| v.as_value()) + .and_then(|v| match v { + toml_edit::Value::Boolean(b) => Some(b.value().to_string()), + toml_edit::Value::String(s) => Some(s.value().clone()), + _ => None, + }); + rules.users.push(UserRule { name, default }); + } + } + } + + if let Some(tables) = doc + .get("display") + .and_then(|item| item.as_array_of_tables()) + { + for table in tables { + let name = table + .get("name") + .and_then(|v| v.as_str()) + .ok_or("[[display]] entry is missing `name`")? + .to_string(); + let rule = table + .get("rule") + .and_then(|v| v.as_str()) + .ok_or("[[display]] entry is missing `rule`")? + .to_string(); + rules.display.push(DisplayRule { name, rule }); + } + } + + if let Some(tables) = doc + .get("hide-file") + .and_then(|item| item.as_array_of_tables()) + { + for table in tables { + let file = table + .get("file") + .and_then(|v| v.as_str()) + .ok_or("[[hide-file]] entry is missing `file`")? + .to_string(); + let rule = table + .get("rule") + .and_then(|v| v.as_str()) + .ok_or("[[hide-file]] entry is missing `rule`")? + .to_string(); + rules.hide_files.push(HideFileRule { file, rule }); + } + } + + Ok(rules) +} + +/// Merge the checklist answers with the defaults declared in `rule.toml`. +/// +/// A key the user left unset in `checklist.toml` falls back to its declared +/// default (e.g. `pathf` defaults to `true`); keys the user filled in always +/// win. The merged map is what rules are evaluated against. +pub fn resolve_answers( + answers: &HashMap<String, String>, + rules: &TemplateRules, +) -> HashMap<String, String> { + let mut merged = answers.clone(); + for user in &rules.users { + if let Some(default) = &user.default + && !merged.contains_key(&user.name) + { + merged.insert(user.name.clone(), default.clone()); + } + } + merged +} + +/// Evaluate a boolean rule expression against the checklist answers. +/// +/// Supported syntax: +/// - bare identifier: `tokio` — true when the key exists with a truthy value +/// - comparison: `parser == clap` +/// - operators: `!`, `&&`, `||` +/// - parentheses for grouping +pub fn eval_rule(rule: &str, answers: &HashMap<String, String>) -> bool { + let mut parser = RuleParser::new(rule, answers); + parser.parse_or().unwrap_or(false) +} + +/// A key is truthy when it is present with a non-empty, non-`"false"` value. +fn is_truthy(key: &str, answers: &HashMap<String, String>) -> bool { + matches!(answers.get(key), Some(value) if !value.is_empty() && value != "false") +} + +/// Recursive-descent parser for `rule.toml` boolean expressions. +struct RuleParser<'a> { + chars: Vec<char>, + pos: usize, + answers: &'a HashMap<String, String>, +} + +impl<'a> RuleParser<'a> { + fn new(rule: &str, answers: &'a HashMap<String, String>) -> Self { + Self { + chars: rule.chars().collect(), + pos: 0, + answers, + } + } + + /// Skips whitespace, then consumes `c` if it matches. + fn eat(&mut self, c: char) -> bool { + self.skip_ws(); + if self.chars.get(self.pos) == Some(&c) { + self.pos += 1; + true + } else { + false + } + } + + fn skip_ws(&mut self) { + while self.chars.get(self.pos).is_some_and(|c| c.is_whitespace()) { + self.pos += 1; + } + } + + /// `or := and ('||' and)*` + fn parse_or(&mut self) -> Option<bool> { + let mut value = self.parse_and()?; + while self.eat('|') && self.eat('|') { + let rhs = self.parse_and()?; + value |= rhs; + } + Some(value) + } + + /// `and := unary ('&&' unary)*` + fn parse_and(&mut self) -> Option<bool> { + let mut value = self.parse_unary()?; + while self.eat('&') && self.eat('&') { + let rhs = self.parse_unary()?; + value &= rhs; + } + Some(value) + } + + /// `unary := '!' unary | primary` + fn parse_unary(&mut self) -> Option<bool> { + if self.eat('!') { + return Some(!self.parse_unary()?); + } + self.parse_primary() + } + + /// `primary := '(' or ')' | ident ('==' ident)?` + fn parse_primary(&mut self) -> Option<bool> { + if self.eat('(') { + let value = self.parse_or()?; + self.eat(')'); + return Some(value); + } + let ident = self.parse_ident()?; + if self.eat('=') && self.eat('=') { + let other = self.parse_ident()?; + return Some(self.answers.get(&ident).map(String::as_str) == Some(other.as_str())); + } + Some(is_truthy(&ident, self.answers)) + } + + fn parse_ident(&mut self) -> Option<String> { + self.skip_ws(); + let start = self.pos; + while self + .chars + .get(self.pos) + .is_some_and(|c| c.is_alphanumeric() || *c == '_' || *c == '-') + { + self.pos += 1; + } + if self.pos > start { + Some(self.chars[start..self.pos].iter().collect()) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn checklist_parses_flat_values() { + let content = r#" +program_name = "my-cli" +parser = "picker" +exit_code = true +completion = true +"#; + let answers = parse_checklist(content).unwrap(); + assert_eq!(answers.get("program_name").unwrap(), "my-cli"); + assert_eq!(answers.get("parser").unwrap(), "picker"); + assert_eq!(answers.get("exit_code").unwrap(), "true"); + assert_eq!(answers.get("completion").unwrap(), "true"); + } + + #[test] + fn checklist_ignores_commented_keys() { + let content = r#" +# tokio = true +completion = true +"#; + let answers = parse_checklist(content).unwrap(); + assert!(!answers.contains_key("tokio")); + assert!(answers.contains_key("completion")); + } + + #[test] + fn rules_parse_display_and_hide_file() { + let content = r#" +[[display]] +name = "TOKIO" +rule = "tokio" + +[[display]] +name = "ASYNC" +rule = "tokio || async_std" + +[[display]] +name = "PARSER_PICKER" +rule = "use_parser && parser == picker" + +[[hide-file]] +file = "./build.rs" +rule = "!completion && !pathf" +"#; + let rules = parse_rules(content).unwrap(); + assert_eq!(rules.display.len(), 3); + assert_eq!(rules.display[0].name, "TOKIO"); + assert_eq!(rules.display[0].rule, "tokio"); + assert_eq!(rules.display[1].rule, "tokio || async_std"); + assert_eq!(rules.display[2].rule, "use_parser && parser == picker"); + assert_eq!(rules.hide_files.len(), 1); + assert_eq!(rules.hide_files[0].file, "./build.rs"); + assert_eq!(rules.hide_files[0].rule, "!completion && !pathf"); + } + + #[test] + fn rules_parse_user_defaults() { + let content = r#" +[[user.toggle]] +name = "pathf" +default = true + +[[user.toggle]] +name = "tokio" + +[[user.selection]] +name = "parser" +option = [ "clap", "builtin", "picker" ] +"#; + let rules = parse_rules(content).unwrap(); + assert_eq!(rules.users.len(), 3); + assert_eq!(rules.users[0].name, "pathf"); + assert_eq!(rules.users[0].default.as_deref(), Some("true")); + assert_eq!(rules.users[1].name, "tokio"); + assert_eq!(rules.users[1].default, None); + assert_eq!(rules.users[2].name, "parser"); + assert_eq!(rules.users[2].default, None); + } + + #[test] + fn resolve_answers_applies_declared_defaults() { + let content = r#" +[[user.toggle]] +name = "pathf" +default = true + +[[user.toggle]] +name = "completion" +"#; + let rules = parse_rules(content).unwrap(); + + // completion answered, pathf left unset -> pathf falls back to true + let mut answers = HashMap::new(); + answers.insert("completion".into(), "true".into()); + let merged = resolve_answers(&answers, &rules); + assert_eq!(merged.get("completion").unwrap(), "true"); + assert_eq!(merged.get("pathf").unwrap(), "true"); + + // user-provided value always wins over the default + let mut answers = HashMap::new(); + answers.insert("pathf".into(), "false".into()); + let merged = resolve_answers(&answers, &rules); + assert_eq!(merged.get("pathf").unwrap(), "false"); + } + + #[test] + fn toggle_semantics_match_checklist_states() { + // rule.toml: `pathf` defaults to true, `tokio` has no default. + let content = r#" +[[user.toggle]] +name = "pathf" +default = true + +[[user.toggle]] +name = "tokio" +"#; + let rules = parse_rules(content).unwrap(); + + // 1. Commented out (`# key = true`): key absent -> default applies; + // keys without a default stay absent (falsy). + let merged = resolve_answers(&HashMap::new(), &rules); + assert_eq!(merged.get("pathf").unwrap(), "true"); + assert!(!merged.contains_key("tokio")); + assert!(eval_rule("pathf", &merged)); + assert!(!eval_rule("tokio", &merged)); + + // 2. Explicit `key = true`. + let mut answers = HashMap::new(); + answers.insert("pathf".into(), "true".into()); + answers.insert("tokio".into(), "true".into()); + let merged = resolve_answers(&answers, &rules); + assert!(eval_rule("pathf", &merged)); + assert!(eval_rule("tokio", &merged)); + + // 3. Explicit `key = false` overrides the declared default. + let mut answers = HashMap::new(); + answers.insert("pathf".into(), "false".into()); + let merged = resolve_answers(&answers, &rules); + assert_eq!(merged.get("pathf").unwrap(), "false"); + assert!(!eval_rule("pathf", &merged)); + } + + #[test] + fn eval_bare_identifier() { + let mut answers = HashMap::new(); + answers.insert("tokio".into(), "true".into()); + assert!(eval_rule("tokio", &answers)); + assert!(!eval_rule("async_std", &answers)); + } + + #[test] + fn eval_false_value_is_falsy() { + let mut answers = HashMap::new(); + answers.insert("pathf".into(), "false".into()); + assert!(!eval_rule("pathf", &answers)); + + // Empty string is also falsy. + answers.insert("empty".into(), "".into()); + assert!(!eval_rule("empty", &answers)); + } + + #[test] + fn eval_boolean_operators() { + let mut answers = HashMap::new(); + answers.insert("tokio".into(), "true".into()); + answers.insert("completion".into(), "true".into()); + answers.insert("pathf".into(), "false".into()); + + assert!(eval_rule("tokio || async_std", &answers)); + assert!(!eval_rule("async_std && tokio", &answers)); + assert!(eval_rule("!async_std", &answers)); + // completion is true, so `!completion && !pathf` is false + assert!(!eval_rule("!completion && !pathf", &answers)); + // with neither key present the same rule becomes true + assert!(eval_rule("!completion && !pathf", &HashMap::new())); + assert!(eval_rule("(tokio || async_std) && completion", &answers)); + assert!(!eval_rule("(async_std || tokio) && !completion", &answers)); + } + + #[test] + fn eval_equality_comparison() { + let mut answers = HashMap::new(); + answers.insert("parser".into(), "picker".into()); + answers.insert("use_parser".into(), "true".into()); + + assert!(eval_rule("parser == picker", &answers)); + assert!(!eval_rule("parser == clap", &answers)); + assert!(eval_rule("use_parser && parser == picker", &answers)); + } +} |
