diff --git a/.github/dependabot.yml b/.github/dependabot.yml index be614717..2d36099e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -48,3 +48,7 @@ updates: directory: "/crates/detect-targets" schedule: interval: "daily" + - package-ecosystem: "cargo" + directory: "/crates/leon" + schedule: + interval: "daily" diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 7e8ae121..899fb7ae 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -16,6 +16,7 @@ on: - detect-wasi - fs-lock - normalize-path + - leon version: description: Version to release required: true diff --git a/Cargo.lock b/Cargo.lock index 36ef7add..1e5b44b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "async-compression" version = "0.3.15" @@ -78,7 +84,7 @@ checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.3", ] [[package]] @@ -95,6 +101,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -262,9 +279,9 @@ checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" [[package]] name = "block-buffer" -version = "0.10.4" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] @@ -292,9 +309,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -335,7 +352,7 @@ version = "0.21.3" dependencies = [ "binstalk", "binstalk-manifests", - "clap", + "clap 4.1.11", "compact_str", "dirs", "embed-resource", @@ -365,9 +382,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f83bc2e401ed041b7057345ebc488c005efa0341d5541ce7004d30458d0090b" dependencies = [ "serde", - "toml 0.7.3", + "toml 0.7.1", ] +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.2" @@ -379,9 +402,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" dependencies = [ "jobserver", ] @@ -392,6 +415,45 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" + +[[package]] +name = "ciborium-ll" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap", + "textwrap 0.16.0", +] + [[package]] name = "clap" version = "4.1.11" @@ -400,7 +462,7 @@ checksum = "42dfd32784433290c51d92c438bb72ea5063797fc3cc9a21a8c4346bebbb2098" dependencies = [ "bitflags 2.0.2", "clap_derive", - "clap_lex", + "clap_lex 0.3.1", "is-terminal", "once_cell", "strsim", @@ -417,14 +479,23 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] name = "clap_lex" -version = "0.3.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" dependencies = [ "os_str_bytes", ] @@ -489,6 +560,40 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap 3.2.23", + "criterion-plot", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -572,9 +677,9 @@ dependencies = [ [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if", ] @@ -588,7 +693,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] @@ -614,9 +719,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -629,14 +734,14 @@ checksum = "bbe20e5f282c61432e5cf0e33185e5dde032b2c2e4281c4735c31ce5b455a869" [[package]] name = "filetime" -version = "0.2.20" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys 0.45.0", + "windows-sys 0.42.0", ] [[package]] @@ -700,24 +805,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" [[package]] name = "futures-lite" @@ -731,32 +836,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" dependencies = [ "futures-core", "futures-io", @@ -813,9 +918,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" [[package]] name = "guess_host_triple" @@ -831,9 +936,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.16" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4" dependencies = [ "bytes", "fnv", @@ -848,6 +953,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + [[package]] name = "hashbrown" version = "0.12.3" @@ -859,9 +970,18 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] [[package]] name = "hermit-abi" @@ -872,12 +992,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" - [[package]] name = "home" version = "0.5.4" @@ -900,9 +1014,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -934,9 +1048,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", @@ -1030,13 +1144,12 @@ checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074" [[package]] name = "io-lifetimes" -version = "1.0.9" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" dependencies = [ - "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.42.0", ] [[package]] @@ -1059,14 +1172,14 @@ checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" [[package]] name = "is-terminal" -version = "0.4.5" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes 1.0.9", - "rustix 0.36.11", - "windows-sys 0.45.0", + "hermit-abi 0.2.6", + "io-lifetimes 1.0.4", + "rustix 0.36.6", + "windows-sys 0.42.0", ] [[package]] @@ -1086,15 +1199,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] @@ -1115,9 +1228,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -1128,11 +1241,23 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "leon" +version = "0.0.1" +dependencies = [ + "clap 4.1.11", + "criterion", + "miette", + "serde", + "thiserror", + "tinytemplate", +] + [[package]] name = "libc" -version = "0.2.140" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libmimalloc-sys" @@ -1219,9 +1344,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" -version = "0.1.10" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "maybe-owned" @@ -1253,7 +1378,7 @@ dependencies = [ "supports-hyperlinks", "supports-unicode", "terminal_size", - "textwrap", + "textwrap 0.15.2", "thiserror", "unicode-width", ] @@ -1266,7 +1391,7 @@ checksum = "2a07ad93a80d1b92bb44cb42d7c49b49c9aab1778befefad49cceb5e4c5bf460" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] @@ -1280,9 +1405,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.17" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" @@ -1295,14 +1420,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.42.0", ] [[package]] @@ -1325,9 +1450,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a" +checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1349,6 +1474,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -1361,9 +1495,9 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.30.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" dependencies = [ "memchr", ] @@ -1375,10 +1509,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] -name = "openssl" -version = "0.10.47" +name = "oorandom" +version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b277f87dacc05a6b709965d1cbafac4649d6ce9f3ce9ceb88508b5666dfec9" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "openssl" +version = "0.10.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" dependencies = [ "bitflags 1.3.2", "cfg-if", @@ -1397,7 +1537,7 @@ checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] @@ -1408,9 +1548,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.82" +version = "0.9.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a95792af3c4e0153c3914df2261bedd30a98476f94dc892b67dfe1d89d433a04" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" dependencies = [ "autocfg", "cc", @@ -1421,9 +1561,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] name = "overload" @@ -1449,15 +1589,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "ba1ef8814b5c993410bb3adfad7a5ed269563e4a2f90c41f5d85be7fb47133bf" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys 0.45.0", + "windows-sys 0.42.0", ] [[package]] @@ -1483,7 +1623,7 @@ checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] @@ -1519,7 +1659,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", "version_check", ] @@ -1780,16 +1920,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.36.11" +version = "0.36.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" +checksum = "4feacf7db682c6c329c4ede12649cd36ecab0f3be5b7d74e6a20304725db4549" dependencies = [ "bitflags 1.3.2", "errno", - "io-lifetimes 1.0.9", + "io-lifetimes 1.0.4", "libc", "linux-raw-sys 0.1.4", - "windows-sys 0.45.0", + "windows-sys 0.42.0", ] [[package]] @@ -1836,15 +1976,24 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "5583e89e108996506031660fe09baa5011b9dd0341b89029313006d1fb508d70" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] [[package]] name = "schannel" @@ -1873,9 +2022,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.8.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -1886,9 +2035,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" dependencies = [ "core-foundation-sys", "libc", @@ -1905,9 +2054,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.157" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707de5fcf5df2b5788fca98dd7eab490bc2fd9b7ef1404defc462833b83f25ca" +checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" dependencies = [ "serde_derive", ] @@ -1923,13 +2072,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.157" +version = "1.0.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78997f4555c22a7971214540c4a661291970619afd56de19f77e0de86296e1e5" +checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.3", ] [[package]] @@ -1988,18 +2137,18 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" dependencies = [ "autocfg", ] @@ -2018,9 +2167,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -2060,7 +2209,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] @@ -2093,9 +2242,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.109" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -2104,9 +2253,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.4" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +checksum = "e8234ae35e70582bfa0f1fedffa6daa248e41dd045310b19800c4a36382c8f60" dependencies = [ "proc-macro2", "quote", @@ -2122,7 +2271,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix 0.36.11", + "rustix 0.36.6", "windows-sys 0.42.0", ] @@ -2156,6 +2305,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.40" @@ -2173,16 +2328,15 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.4", + "syn 2.0.3", ] [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ - "cfg-if", "once_cell", ] @@ -2207,9 +2361,9 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.1" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" @@ -2239,14 +2393,14 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] name = "tokio-native-tls" -version = "0.3.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" dependencies = [ "native-tls", "tokio", @@ -2265,9 +2419,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce" dependencies = [ "futures-core", "pin-project-lite", @@ -2314,9 +2468,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "772c1426ab886e7362aedf4abc9c0d1348a979517efedfc25862944d10137af0" dependencies = [ "serde", "serde_spanned", @@ -2396,7 +2550,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", ] [[package]] @@ -2520,15 +2674,15 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-linebreak" @@ -2617,6 +2771,17 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -2635,9 +2800,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2645,24 +2810,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if", "js-sys", @@ -2672,9 +2837,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2682,22 +2847,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 1.0.107", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "wasm-streams" @@ -2714,9 +2879,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -2919,7 +3084,7 @@ version = "0.12.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" dependencies = [ - "zstd-safe 6.0.4+zstd.1.5.4", + "zstd-safe 6.0.3+zstd.1.5.2", ] [[package]] @@ -2934,9 +3099,9 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "6.0.4+zstd.1.5.4" +version = "6.0.3+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7afb4b54b8910cf5447638cb54bf4e8a65cbedd783af98b98c62ffe91f185543" +checksum = "68e4a3f57d13d0ab7e478665c60f35e2a613dcd527851c2c7287ce5c787e134a" dependencies = [ "libc", "zstd-sys", diff --git a/Cargo.toml b/Cargo.toml index 4e3e1f52..c4fd9e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ members = [ "crates/fs-lock", "crates/normalize-path", "crates/detect-targets", + "crates/leon", ] [profile.release] diff --git a/crates/leon/Cargo.toml b/crates/leon/Cargo.toml new file mode 100644 index 00000000..b12324d1 --- /dev/null +++ b/crates/leon/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "leon" +description = "Dead-simple string templating" +repository = "https://github.com/cargo-bins/cargo-binstall" +documentation = "https://docs.rs/leon" +version = "0.0.1" +rust-version = "1.61.0" +authors = ["Félix Saparelli "] +edition = "2021" +license = "Apache-2.0 OR MIT" + +[dependencies] +clap = { version = "4.1.4", features = ["derive"], optional = true } +miette = { version = "5.5.0", default-features = false, optional = true } +thiserror = "1.0.38" + +[features] +default = ["miette"] +cli = ["dep:clap", "miette?/fancy-no-backtrace"] +miette = ["dep:miette"] + +[dev-dependencies] +criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] } +serde = { version = "1.0.152", features = ["derive"] } +tinytemplate = "1.2.1" + +[[bench]] +name = "values" +harness = false + +[[bench]] +name = "others" +harness = false diff --git a/crates/leon/LICENSE-APACHE b/crates/leon/LICENSE-APACHE new file mode 100644 index 00000000..1b5ec8b7 --- /dev/null +++ b/crates/leon/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/crates/leon/LICENSE-MIT b/crates/leon/LICENSE-MIT new file mode 100644 index 00000000..31aa7938 --- /dev/null +++ b/crates/leon/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/crates/leon/benches/others.rs b/crates/leon/benches/others.rs new file mode 100644 index 00000000..9b48f871 --- /dev/null +++ b/crates/leon/benches/others.rs @@ -0,0 +1,89 @@ +use std::borrow::Cow; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use leon::{vals, Template}; +use serde::Serialize; +use tinytemplate::TinyTemplate; + +fn compare_impls(c: &mut Criterion) { + const TEMPLATE: &str = "hello {name}! i am {age} years old. my goal is to {goal}. i like: {flower}, {music}, {animal}, {color}, {food}. i'm drinking {drink}"; + fn replace_fn<'s>(key: &'s str) -> Option> { + Some(Cow::Borrowed(match key { + "name" => "marcus", + "age" => "42", + "goal" => "primary", + "flower" => "lotus", + "music" => "jazz", + "animal" => "cat", + "color" => "blue", + "food" => "pizza", + "drink" => "coffee", + _ => return None, + })) + } + + #[derive(Serialize)] + struct Context<'c> { + name: &'c str, + age: u8, + goal: &'c str, + flower: &'c str, + music: &'c str, + animal: &'c str, + color: &'c str, + food: &'c str, + drink: &'c str, + } + + let tt_context = Context { + name: "marcus", + age: 42, + goal: "primary", + flower: "lotus", + music: "jazz", + animal: "cat", + color: "blue", + food: "pizza", + drink: "coffee", + }; + + c.bench_function("leon", |b| { + b.iter(|| { + let template = Template::parse(black_box(TEMPLATE)).unwrap(); + let output = template.render(&vals(replace_fn)).unwrap(); + black_box(output); + }) + }); + + c.bench_function("std, string replaces", |b| { + b.iter(|| { + let mut output = black_box(TEMPLATE).to_string(); + for (key, value) in [ + ("name", "marcus"), + ("age", "42"), + ("goal", "primary"), + ("flower", "lotus"), + ("music", "jazz"), + ("animal", "cat"), + ("color", "blue"), + ("food", "pizza"), + ("drink", "coffee"), + ] { + output = output.replace(&format!("{{{}}}", key), value); + } + black_box(output); + }) + }); + + c.bench_function("tiny template", |b| { + b.iter(|| { + let mut tt = TinyTemplate::new(); + tt.add_template("tmp", black_box(TEMPLATE)).unwrap(); + let output = tt.render("tmp", &tt_context).unwrap(); + black_box(output); + }) + }); +} + +criterion_group!(compare, compare_impls); +criterion_main!(compare); diff --git a/crates/leon/benches/values.rs b/crates/leon/benches/values.rs new file mode 100644 index 00000000..b4fd7944 --- /dev/null +++ b/crates/leon/benches/values.rs @@ -0,0 +1,298 @@ +use std::{borrow::Cow, collections::HashMap}; + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use leon::{vals, Template, Values, ValuesFn}; + +macro_rules! make_values { + ($($name:expr => $value:expr),*) => { + ( + &[$(($name, $value)),*], + { + let mut map = HashMap::new(); + $( + map.insert($name, $value); + )* + map + }, + vals(|key| match key { + $( + $name => Some(Cow::Borrowed($value)), + )* + _ => None, + }) + ) + }; +} + +fn one_replace(c: &mut Criterion) { + const TEMPLATE: &str = "Hello, {name}!"; + + let (slice, hashmap, vals) = make_values!( + "name" => "marcus" + ); + + inner_bench("one replace", c, TEMPLATE, vals, hashmap, slice); +} + +fn some_replaces(c: &mut Criterion) { + const TEMPLATE: &str = "hello {name}! i am {age} years old. my goal is to {goal}. i like: {flower}, {music}, {animal}, {color}, {food}. i'm drinking {drink}"; + + let (slice, hashmap, vals) = make_values!( + "name" => "marcus", + "age" => "42", + "goal" => "primary", + "flower" => "lotus", + "music" => "jazz", + "animal" => "cat", + "color" => "blue", + "food" => "pizza", + "drink" => "coffee" + ); + + inner_bench("some replaces", c, TEMPLATE, vals, hashmap, slice); +} + +fn many_replaces(c: &mut Criterion) { + const TEMPLATE: &str = " + {artichoke} + {aubergine} + {asparagus} + {broccoflower} + {broccoli} + {brussels sprouts} + {cabbage} + {kohlrabi} + {Savoy cabbage} + {red cabbage} + {cauliflower} + {celery} + {endive} + {fiddleheads} + {frisee} + {fennel} + {greens} + {arugula} + {bok choy} + {chard} + {collard greens} + {kale} + {lettuce} + {mustard greens} + {spinach} + {herbs} + {anise} + {basil} + {caraway} + {coriander} + {chamomile} + {daikon} + {dill} + {squash} + {lavender} + {cymbopogon} + {marjoram} + {oregano} + {parsley} + {rosemary} + {thyme} + {legumes} + {alfalfa sprouts} + {azuki beans} + {bean sprouts} + {black beans} + {black-eyed peas} + {borlotti bean} + {broad beans} + {chickpeas, garbanzos, or ceci beans} + {green beans} + {kidney beans} + {lentils} + {lima beans or butter bean} + {mung beans} + {navy beans} + {peanuts} + {pinto beans} + {runner beans} + {split peas} + {soy beans} + {peas} + {mange tout or snap peas} + {mushrooms} + {nettles} + {New Zealand spinach} + {okra} + {onions} + {chives} + {garlic} + {leek} + {onion} + {shallot} + {scallion} + {peppers} + {bell pepper} + {chili pepper} + {jalapeño} + {habanero} + {paprika} + {tabasco pepper} + {cayenne pepper} + {radicchio} + {rhubarb} + {root vegetables} + {beetroot} + {beet} + {mangelwurzel} + {carrot} + {celeriac} + {corms} + {eddoe} + {konjac} + {taro} + {water chestnut} + {ginger} + {parsnip} + {rutabaga} + {radish} + {wasabi} + "; + + let (slice, hashmap, vals) = make_values!( + "artichoke" => "Abiu", + "aubergine" => "Açaí", + "asparagus" => "Acerola", + "broccoflower" => "Akebi", + "broccoli" => "Ackee", + "brussels sprouts" => "African Cherry Orange", + "cabbage" => "American Mayapple", + "kohlrabi" => "Apple", + "Savoy cabbage" => "Apricot", + "red cabbage" => "Araza", + "cauliflower" => "Avocado", + "celery" => "Banana", + "endive" => "Bilberry", + "fiddleheads" => "Blackberry", + "frisee" => "Blackcurrant", + "fennel" => "Black sapote", + "greens" => "Blueberry", + "arugula" => "Boysenberry", + "bok choy" => "Breadfruit", + "chard" => "Buddha's hand", + "collard greens" => "Cactus pear", + "kale" => "Canistel", + "lettuce" => "Cashew", + "mustard greens" => "Cempedak", + "spinach" => "Cherimoya", + "herbs" => "Cherry", + "anise" => "Chico fruit", + "basil" => "Cloudberry", + "caraway" => "Coco de mer", + "coriander" => "Coconut", + "chamomile" => "Crab apple", + "daikon" => "Cranberry", + "dill" => "Currant", + "squash" => "Damson", + "lavender" => "Date", + "cymbopogon" => "Dragonfruit", + "marjoram" => "Pitaya", + "oregano" => "Durian", + "parsley" => "Elderberry", + "rosemary" => "Feijoa", + "thyme" => "Fig", + "legumes" => "Finger Lime", + "alfalfa sprouts" => "Caviar Lime", + "azuki beans" => "Goji berry", + "bean sprouts" => "Gooseberry", + "black beans" => "Grape", + "black-eyed peas" => "Raisin", + "borlotti bean" => "Grapefruit", + "broad beans" => "Grewia asiatica", + "chickpeas, garbanzos, or ceci beans" => "Guava", + "green beans" => "Hala Fruit", + "kidney beans" => "Honeyberry", + "lentils" => "Huckleberry", + "lima beans or butter bean" => "Jabuticaba", + "mung beans" => "Jackfruit", + "navy beans" => "Jambul", + "peanuts" => "Japanese plum", + "pinto beans" => "Jostaberry", + "runner beans" => "Jujube", + "split peas" => "Juniper berry", + "soy beans" => "Kaffir Lime", + "peas" => "Kiwano", + "mange tout or snap peas" => "Kiwifruit", + "mushrooms" => "Kumquat", + "nettles" => "Lemon", + "New Zealand spinach" => "Lime", + "okra" => "Loganberry", + "onions" => "Longan", + "chives" => "Loquat", + "garlic" => "Lulo", + "leek" => "Lychee", + "onion" => "Magellan Barberry", + "shallot" => "Mamey Apple", + "scallion" => "Mamey Sapote", + "peppers" => "Mango", + "bell pepper" => "Mangosteen", + "chili pepper" => "Marionberry", + "jalapeño" => "Melon", + "habanero" => "Cantaloupe", + "paprika" => "Galia melon", + "tabasco pepper" => "Honeydew", + "cayenne pepper" => "Mouse melon", + "radicchio" => "Musk melon", + "rhubarb" => "Watermelon", + "root vegetables" => "Miracle fruit", + "beetroot" => "Momordica fruit", + "beet" => "Monstera deliciosa", + "mangelwurzel" => "Mulberry", + "carrot" => "Nance", + "celeriac" => "Nectarine", + "corms" => "Orange", + "eddoe" => "Blood orange", + "konjac" => "Clementine", + "taro" => "Mandarine", + "water chestnut" => "Tangerine", + "ginger" => "Papaya", + "parsnip" => "Passionfruit", + "rutabaga" => "Pawpaw", + "radish" => "Peach", + "wasabi" => "Pear" + ); + + inner_bench("many replaces", c, TEMPLATE, vals, hashmap, slice); +} + +fn inner_bench( + name: &str, + c: &mut Criterion, + template_str: &str, + vals: ValuesFn, + hashmap: HashMap<&str, &str>, + slice: &[(&str, &str)], +) where + F: for<'s> Fn(&'s str) -> Option> + Send + 'static, +{ + c.bench_function(&format!("{name}, fn"), |b| { + b.iter(|| { + let template = Template::parse(black_box(template_str)).unwrap(); + black_box(template.render(&vals).unwrap()); + }) + }); + c.bench_function(&format!("{name}, hashmap"), |b| { + b.iter(|| { + let template = Template::parse(black_box(template_str)).unwrap(); + black_box(template.render(&hashmap).unwrap()); + }) + }); + c.bench_function(&format!("{name}, slice"), |b| { + b.iter(|| { + let template = Template::parse(black_box(template_str)).unwrap(); + black_box(template.render(&slice as &dyn Values).unwrap()); + }) + }); +} + +criterion_group!(one, one_replace); +criterion_group!(some, some_replaces); +criterion_group!(many, many_replaces); +criterion_main!(one, some, many); diff --git a/crates/leon/fuzz/.gitignore b/crates/leon/fuzz/.gitignore new file mode 100644 index 00000000..1a45eee7 --- /dev/null +++ b/crates/leon/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/crates/leon/fuzz/Cargo.lock b/crates/leon/fuzz/Cargo.lock new file mode 100644 index 00000000..9409a0dd --- /dev/null +++ b/crates/leon/fuzz/Cargo.lock @@ -0,0 +1,150 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e90af4de65aa7b293ef2d09daff88501eb254f58edde2e1ac02c82d873eadad" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "leon" +version = "0.0.0" +dependencies = [ + "miette", + "thiserror", +] + +[[package]] +name = "leon-fuzz" +version = "0.0.0" +dependencies = [ + "leon", + "libfuzzer-sys", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "miette" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd9b301defa984bbdbe112b4763e093ed191750a0d914a78c1106b2d0fe703" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c2401ab7ac5282ca5c8b518a87635b1a93762b0b90b9990c509888eeccba29" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" diff --git a/crates/leon/fuzz/Cargo.toml b/crates/leon/fuzz/Cargo.toml new file mode 100644 index 00000000..1801da08 --- /dev/null +++ b/crates/leon/fuzz/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "leon-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.leon] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "parser" +path = "fuzz_targets/parser.rs" +test = false +doc = false diff --git a/crates/leon/fuzz/fuzz_targets/parser.rs b/crates/leon/fuzz/fuzz_targets/parser.rs new file mode 100644 index 00000000..9cd51fe5 --- /dev/null +++ b/crates/leon/fuzz/fuzz_targets/parser.rs @@ -0,0 +1,9 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + if let Ok(s) = std::str::from_utf8(data) { + let _ = leon::Template::parse(s); + } +}); diff --git a/crates/leon/src/error.rs b/crates/leon/src/error.rs new file mode 100644 index 00000000..6e064e2f --- /dev/null +++ b/crates/leon/src/error.rs @@ -0,0 +1,85 @@ +#[derive(Debug, thiserror::Error)] +#[cfg_attr(feature = "miette", derive(miette::Diagnostic))] +pub enum RenderError { + /// A key was missing from the provided values. + #[error("missing key `{0}`")] + MissingKey(String), + + /// An I/O error passed through from [`Template::render_into`]. + #[error("write failed: {0}")] + Io(#[from] std::io::Error), +} + +/// An error that can occur when parsing a template. +/// +/// When the `miette` feature is enabled, this is a rich miette-powered error +/// which will highlight the source of the error in the template when output +/// (with miette's `fancy` feature). With `miette` disabled, this is opaque. +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] +#[cfg_attr(feature = "miette", derive(miette::Diagnostic))] +#[cfg_attr(feature = "miette", diagnostic(transparent))] +#[error(transparent)] +pub struct ParseError(Box); + +/// The inner (unboxed) type of [`ParseError`]. +#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)] +#[cfg_attr(feature = "miette", derive(miette::Diagnostic))] +#[error("template parse failed")] +struct InnerParseError { + #[cfg_attr(feature = "miette", source_code)] + src: String, + + #[cfg_attr(feature = "miette", label("This bracket is not opening or closing anything. Try removing it, or escaping it with a backslash."))] + unbalanced: Option<(usize, usize)>, + + #[cfg_attr(feature = "miette", label("This escape is malformed."))] + escape: Option<(usize, usize)>, + + #[cfg_attr(feature = "miette", label("A key cannot be empty."))] + key_empty: Option<(usize, usize)>, + + #[cfg_attr(feature = "miette", label("Escapes are not allowed in keys."))] + key_escape: Option<(usize, usize)>, +} + +impl ParseError { + pub(crate) fn unbalanced(src: &str, start: usize, end: usize) -> Self { + Self(Box::new(InnerParseError { + src: String::from(src), + unbalanced: Some((start, end.saturating_sub(start) + 1)), + escape: None, + key_empty: None, + key_escape: None, + })) + } + + pub(crate) fn escape(src: &str, start: usize, end: usize) -> Self { + Self(Box::new(InnerParseError { + src: String::from(src), + unbalanced: None, + escape: Some((start, end.saturating_sub(start) + 1)), + key_empty: None, + key_escape: None, + })) + } + + pub(crate) fn key_empty(src: &str, start: usize, end: usize) -> Self { + Self(Box::new(InnerParseError { + src: String::from(src), + unbalanced: None, + escape: None, + key_empty: Some((start, end.saturating_sub(start) + 1)), + key_escape: None, + })) + } + + pub(crate) fn key_escape(src: &str, start: usize, end: usize) -> Self { + Self(Box::new(InnerParseError { + src: String::from(src), + unbalanced: None, + escape: None, + key_empty: None, + key_escape: Some((start, end.saturating_sub(start) + 1)), + })) + } +} diff --git a/crates/leon/src/lib.rs b/crates/leon/src/lib.rs new file mode 100644 index 00000000..17eeeeca --- /dev/null +++ b/crates/leon/src/lib.rs @@ -0,0 +1,148 @@ +//! Dead-simple string templating. +//! +//! Leon parses a template string into a list of tokens, and then substitutes +//! provided values in. Unlike other templating engines, it is extremely simple: +//! it supports no logic, only replaces. It is even simpler than `format!()`, +//! albeit with a similar syntax. +//! +//! # Syntax +//! +//! ```plain +//! it is better to rule { group } +//! one can live {adverb} without power +//! ``` +//! +//! A replacement is denoted by `{` and `}`. The contents of the braces, trimmed +//! of any whitespace, are the key. Any text outside of braces is left as-is. +//! +//! To escape a brace, use `\{` or `\}`. To escape a backslash, use `\\`. Keys +//! cannot contain escapes. +//! +//! ```plain +//! \{ leon \} +//! ``` +//! +//! The above examples, given the values `group = "no one"` and +//! `adverb = "honourably"`, would render to: +//! +//! ```plain +//! it is better to rule no one +//! one can live honourably without power +//! { leon } +//! ``` +//! +//! # Usage +//! +//! A template is first parsed to a token list: +//! +//! ``` +//! use leon::Template; +//! +//! let template = Template::parse("hello {name}").unwrap(); +//! ``` +//! +//! The template can be inspected, for example to check if a key is present: +//! +//! ``` +//! # use leon::Template; +//! # +//! # let template = Template::parse("hello {name}").unwrap(); +//! assert!(template.has_key("name")); +//! ``` +//! +//! The template can be rendered to a string: +//! +//! ``` +//! # use leon::Template; +//! use leon::vals; +//! # +//! # let template = Template::parse("hello {name}").unwrap(); +//! assert_eq!( +//! template.render( +//! &vals(|_key| Some("marcus".into())) +//! ).unwrap().as_str(), +//! "hello marcus", +//! ); +//! ``` +//! +//! …or to a writer: +//! +//! ``` +//! use std::io::Write; +//! # use leon::Template; +//! use leon::vals; +//! # +//! # let template = Template::parse("hello {name}").unwrap(); +//! let mut buf: Vec = Vec::new(); +//! template.render_into( +//! &mut buf, +//! &vals(|key| if key == "name" { +//! Some("julius".into()) +//! } else { +//! None +//! }) +//! ).unwrap(); +//! assert_eq!(buf.as_slice(), b"hello julius"); +//! ``` +//! +//! …with a map: +//! +//! ``` +//! use std::collections::HashMap; +//! # use leon::Template; +//! # let template = Template::parse("hello {name}").unwrap(); +//! let mut values = HashMap::new(); +//! values.insert("name", "brutus"); +//! assert_eq!(template.render(&values).unwrap().as_str(), "hello brutus"); +//! ``` +//! +//! …or with your own type, if you implement the [`Values`] trait: +//! +//! ``` +//! # use leon::Template; +//! use std::borrow::Cow; +//! use leon::Values; +//! +//! struct MyMap { +//! name: &'static str, +//! } +//! impl Values for MyMap { +//! fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { +//! if key == "name" { +//! Some(self.name.into()) +//! } else { +//! None +//! } +//! } +//! } +//! # +//! # let template = Template::parse("hello {name}").unwrap(); +//! let values = MyMap { name: "pontifex" }; +//! assert_eq!(template.render(&values).unwrap().as_str(), "hello pontifex"); +//! ``` +//! +//! # Errors +//! +//! Leon will return a [`LeonError::InvalidTemplate`] if the template fails to +//! parse. This can happen if there are unbalanced braces, or if a key is empty. +//! +//! Leon will return a [`LeonError::MissingKey`] if a key is missing from keyed +//! values passed to [`Template::render()`], unless a default value is provided +//! with [`Template.default`]. +//! +//! It will also pass through I/O errors when using [`Template::render_into()`]. + +#[doc(inline)] +pub use error::*; + +#[doc(inline)] +pub use template::*; + +#[doc(inline)] +pub use values::*; + +mod error; +mod macros; +mod parser; +mod template; +mod values; diff --git a/crates/leon/src/macros.rs b/crates/leon/src/macros.rs new file mode 100644 index 00000000..3833a3c3 --- /dev/null +++ b/crates/leon/src/macros.rs @@ -0,0 +1,50 @@ +/// Construct a template constant without needing to make an items constant. +/// +/// This is essentially a shorthand for: +/// +/// ``` +/// use leon::{Item, Template}; +/// Template::new({ +/// const ITEMS: &'static [Item<'static>] = &[Item::Text("Hello "), Item::Key("name")]; +/// ITEMS +/// }, Some("world")); +/// ``` +/// +/// # Examples +/// +/// ``` +/// use leon::Item::*; +/// assert_eq!( +/// leon::template!(Text("Hello "), Key("name")) +/// .render(&[("name", "Магда Нахман")]) +/// .unwrap(), +/// "Hello Магда Нахман", +/// ); +/// ``` +/// +/// With a default: +/// +/// ``` +/// use leon::Item::*; +/// assert_eq!( +/// leon::template!(Text("Hello "), Key("name"); "M. P. T. Acharya") +/// .render(&[("city", "Madras")]) +/// .unwrap(), +/// "Hello M. P. T. Acharya", +/// ); +/// ``` +#[macro_export] +macro_rules! template { + ($($item:expr),* $(,)?) => { + $crate::Template::new({ + const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*]; + ITEMS + }, ::core::option::Option::None) + }; + ($($item:expr),* $(,)? ; $default:expr) => { + $crate::Template::new({ + const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*]; + ITEMS + }, ::core::option::Option::Some($default)) + }; +} diff --git a/crates/leon/src/main.rs b/crates/leon/src/main.rs new file mode 100644 index 00000000..acbdb1f5 --- /dev/null +++ b/crates/leon/src/main.rs @@ -0,0 +1,61 @@ +#[cfg(feature = "cli")] +fn main() -> miette::Result<()> { + use std::{collections::HashMap, error::Error, io::stdout}; + + use clap::Parser; + use leon::Template; + + /// Render a Leon template with the given values. + #[derive(Parser, Debug)] + #[command(author, version, about, long_about = None)] + struct Args { + /// Leon template + template: String, + + /// Default to use for missing keys + #[arg(long)] + default: Option, + + /// Use values from the environment + #[arg(long)] + env: bool, + + /// Key-value pairs to use + #[arg(short, long, value_parser = parse_key_val::)] + values: Vec<(String, String)>, + } + + /// Parse a single key-value pair + fn parse_key_val(s: &str) -> Result<(T, U), Box> + where + T: std::str::FromStr, + T::Err: Error + Send + Sync + 'static, + U: std::str::FromStr, + U::Err: Error + Send + Sync + 'static, + { + let (k, v) = s + .split_once('=') + .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?; + Ok((k.parse()?, v.parse()?)) + } + + let args = Args::parse(); + let mut values: HashMap = HashMap::from_iter(args.values); + if args.env { + for (key, value) in std::env::vars() { + values.entry(key).or_insert(value); + } + } + + let template = args.template; + let mut template = Template::parse(&template)?; + if let Some(default) = &args.default { + template.set_default(default); + } + + template.render_into(&mut stdout().lock(), &values)?; + Ok(()) +} + +#[cfg(not(feature = "cli"))] +fn main() {} diff --git a/crates/leon/src/parser.rs b/crates/leon/src/parser.rs new file mode 100644 index 00000000..5f9efb37 --- /dev/null +++ b/crates/leon/src/parser.rs @@ -0,0 +1,565 @@ +use std::mem::replace; + +use crate::{Item, ParseError, Template}; + +#[derive(Debug, Clone, Copy)] +enum Token { + Text { + start: usize, + end: usize, + }, + BracePair { + start: usize, + key_seen: bool, + end: usize, + }, + Escape { + start: usize, + end: usize, + ch: Option, + }, +} + +impl Token { + fn start_text(pos: usize, ch: char) -> Self { + Self::Text { + start: pos, + end: pos + ch.len_utf8(), + } + } + + fn start_text_single(pos: usize) -> Self { + Self::Text { + start: pos, + end: pos, + } + } + + fn start_brace_pair(pos: usize, ch: char) -> Self { + Self::BracePair { + start: pos, + key_seen: false, + end: pos + ch.len_utf8(), + } + } + + fn start_escape(pos: usize, ch: char) -> Self { + Self::Escape { + start: pos, + end: pos + ch.len_utf8(), + ch: None, + } + } + + fn is_empty(&self, source_len: usize) -> bool { + match self { + Self::Text { start, end } => { + *start >= source_len || *end >= source_len || *start > *end + } + Self::BracePair { + start, + key_seen, + end, + } => !key_seen || *start >= source_len || *end >= source_len || *start > *end, + Self::Escape { start, end, .. } => { + *start >= source_len || *end >= source_len || *start > *end + } + } + } + + fn start(&self) -> usize { + match self { + Self::Text { start, .. } + | Self::BracePair { start, .. } + | Self::Escape { start, .. } => *start, + } + } + + fn end(&self) -> usize { + match self { + Self::Text { end, .. } | Self::BracePair { end, .. } | Self::Escape { end, .. } => *end, + } + } + + fn set_end(&mut self, pos: usize) { + match self { + Self::Text { end, .. } | Self::BracePair { end, .. } | Self::Escape { end, .. } => { + *end = pos + } + } + } +} + +impl<'s> Template<'s> { + pub(crate) fn parse_items(s: &'s str) -> Result>, ParseError> { + let source_len = s.len(); + let mut tokens = Vec::new(); + + let mut current = Token::start_text(0, '\0'); + + for (pos, chara) in s.char_indices() { + match (&mut current, chara) { + (tok @ (Token::Text { .. } | Token::Escape { ch: Some(_), .. }), ch @ '{') => { + if matches!(tok, Token::Text { .. }) && tok.start() == pos { + *tok = Token::start_brace_pair(pos, ch); + } else { + if let Token::Text { end, .. } = tok { + *end = pos - 1; + } + tokens.push(replace(tok, Token::start_brace_pair(pos, ch))); + } + } + (txt @ Token::Text { .. }, ch @ '\\') => { + if txt.is_empty(source_len) || txt.start() == pos { + *txt = Token::start_escape(pos, ch); + } else { + if let Token::Text { end, .. } = txt { + *end = pos - 1; + } + tokens.push(replace(txt, Token::start_escape(pos, ch))); + } + } + (bp @ Token::BracePair { .. }, '}') => { + if let Token::BracePair { end, .. } = bp { + *end = pos; + } else { + unreachable!("bracepair isn't bracepair"); + } + + tokens.push(replace(bp, Token::start_text_single(pos + 1))); + } + (Token::BracePair { start, .. }, '\\') => { + return Err(ParseError::key_escape(s, *start, pos)); + } + (Token::BracePair { key_seen, end, .. }, ws) if ws.is_whitespace() => { + if *key_seen { + *end = pos; + } else { + // We're in a brace pair, but we're not in the key yet. + } + } + (Token::BracePair { key_seen, end, .. }, _) => { + *key_seen = true; + *end = pos + 1; + } + (Token::Text { .. }, '}') => { + return Err(ParseError::unbalanced(s, pos, pos)); + } + (Token::Text { end, .. }, _) => { + *end = pos; + } + (esc @ Token::Escape { .. }, es @ ('\\' | '{' | '}')) => { + if let Token::Escape { start, end, ch, .. } = esc { + if ch.is_none() { + *end = pos; + *ch = Some(es); + } else if es == '\\' { + // A new escape right after a completed escape. + tokens.push(replace(esc, Token::start_escape(pos, es))); + } else if es == '{' { + // A new brace pair right after a completed escape, should be handled prior to this. + unreachable!("escape followed by brace pair, unhandled"); + } else { + // } right after a completed escape, probably unreachable but just in case: + return Err(ParseError::key_escape(s, *start, pos)); + } + } else { + unreachable!("escape is not an escape"); + } + } + ( + Token::Escape { + start, ch: None, .. + }, + _, + ) => { + return Err(ParseError::escape(s, *start, pos)); + } + (Token::Escape { ch: Some(_), .. }, _) => { + tokens.push(replace(&mut current, Token::start_text_single(pos))); + } + } + } + + if !current.is_empty(source_len) { + if current.end() < source_len - 1 { + current.set_end(source_len - 1); + } + + tokens.push(current); + } + + if let Token::BracePair { start, end, .. } = current { + return Err(ParseError::unbalanced(s, start, end)); + } + + if let Token::Escape { + start, + end, + ch: None, + } = current + { + return Err(ParseError::escape(s, start, end)); + } + + let mut items = Vec::with_capacity(tokens.len()); + for token in tokens { + match token { + Token::Text { start, end } => { + items.push(Item::Text(&s[start..=end])); + } + Token::BracePair { + start, + end, + key_seen: false, + } => { + return Err(ParseError::key_empty(s, start, end)); + } + Token::BracePair { + start, + end, + key_seen: true, + } => { + let key = s[start..=end] + .trim_matches(|c: char| c.is_whitespace() || c == '{' || c == '}'); + if key.is_empty() { + return Err(ParseError::key_empty(s, start, end)); + } else { + items.push(Item::Key(key)); + } + } + Token::Escape { + ch: Some(_), end, .. + } => { + items.push(Item::Text(&s[end..=end])); + } + Token::Escape { + ch: None, + start, + end, + } => { + return Err(ParseError::escape(s, start, end)); + } + } + } + + Ok(items) + } +} + +#[cfg(test)] +mod test_valid { + use crate::{template, Item::*, Template}; + + #[test] + fn empty() { + let template = Template::parse("").unwrap(); + assert_eq!(template, Template::default()); + } + + #[test] + fn no_keys() { + let template = Template::parse("hello world").unwrap(); + assert_eq!(template, template!(Text("hello world"))); + } + + #[test] + fn leading_key() { + let template = Template::parse("{salutation} world").unwrap(); + assert_eq!(template, template!(Key("salutation"), Text(" world"))); + } + + #[test] + fn trailing_key() { + let template = Template::parse("hello {name}").unwrap(); + assert_eq!(template, template!(Text("hello "), Key("name"))); + } + + #[test] + fn middle_key() { + let template = Template::parse("hello {name}!").unwrap(); + assert_eq!(template, template!(Text("hello "), Key("name"), Text("!"))); + } + + #[test] + fn middle_text() { + let template = Template::parse("{salutation} good {title}").unwrap(); + assert_eq!( + template, + template!(Key("salutation"), Text(" good "), Key("title")) + ); + } + + #[test] + fn multiline() { + let template = Template::parse( + " + And if thy native country was { ancient civilisation }, + What need to slight thee? Came not {hero} thence, + Who gave to { country } her books and art of writing? + ", + ) + .unwrap(); + assert_eq!( + template, + template!( + Text("\n And if thy native country was "), + Key("ancient civilisation"), + Text(",\n What need to slight thee? Came not "), + Key("hero"), + Text(" thence,\n Who gave to "), + Key("country"), + Text(" her books and art of writing?\n "), + ) + ); + } + + #[test] + fn key_no_whitespace() { + let template = Template::parse("{word}").unwrap(); + assert_eq!(template, template!(Key("word"))); + } + + #[test] + fn key_leading_whitespace() { + let template = Template::parse("{ word}").unwrap(); + assert_eq!(template, template!(Key("word"))); + } + + #[test] + fn key_trailing_whitespace() { + let template = Template::parse("{word\n}").unwrap(); + assert_eq!(template, template!(Key("word"))); + } + + #[test] + fn key_both_whitespace() { + let template = Template::parse( + "{ + \tword + }", + ) + .unwrap(); + assert_eq!(template, template!(Key("word"))); + } + + #[test] + fn key_inner_whitespace() { + let template = Template::parse("{ a word }").unwrap(); + assert_eq!(template, template!(Key("a word"))); + } + + #[test] + fn escape_left() { + let template = Template::parse(r"this \{ single left brace").unwrap(); + assert_eq!( + template, + template!(Text("this "), Text("{"), Text(" single left brace")) + ); + } + + #[test] + fn escape_right() { + let template = Template::parse(r"this \} single right brace").unwrap(); + assert_eq!( + template, + template!(Text("this "), Text("}"), Text(" single right brace")) + ); + } + + #[test] + fn escape_both() { + let template = Template::parse(r"these \{ two \} braces").unwrap(); + assert_eq!( + template, + template!( + Text("these "), + Text("{"), + Text(" two "), + Text("}"), + Text(" braces") + ) + ); + } + + #[test] + fn escape_doubled() { + let template = Template::parse(r"these \{\{ four \}\} braces").unwrap(); + assert_eq!( + template, + template!( + Text("these "), + Text("{"), + Text("{"), + Text(" four "), + Text("}"), + Text("}"), + Text(" braces") + ) + ); + } + + #[test] + fn escape_escape() { + let template = Template::parse(r"these \\ backslashes \\\\").unwrap(); + assert_eq!( + template, + template!( + Text("these "), + Text(r"\"), + Text(" backslashes "), + Text(r"\"), + Text(r"\"), + ) + ); + } + + #[test] + fn escape_before_key() { + let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap(); + assert_eq!( + template, + template!( + Text(r"\"), + Key("a"), + Text(" "), + Text(r"{"), + Key("b"), + Text(" "), + Text(r"}"), + Key("c"), + ) + ); + } + + #[test] + fn escape_after_key() { + let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap(); + assert_eq!( + template, + template!( + Key("a"), + Text(r"\"), + Text(" "), + Key("b"), + Text(r"{"), + Text(" "), + Key("c"), + Text(r"}"), + ) + ); + } + + #[test] + fn multibyte_texts() { + let template = Template::parse("幸徳 {particle} 秋水").unwrap(); + assert_eq!( + template, + template!(Text("幸徳 "), Key("particle"), Text(" 秋水")) + ); + } + + #[test] + fn multibyte_key() { + let template = Template::parse("The { 連盟 }").unwrap(); + assert_eq!(template, template!(Text("The "), Key("連盟"))); + } + + #[test] + fn multibyte_both() { + let template = Template::parse("大杉 {栄}").unwrap(); + assert_eq!(template, template!(Text("大杉 "), Key("栄"))); + } + + #[test] + fn multibyte_whitespace() { + let template = Template::parse("岩佐 作{ 太 }郎").unwrap(); + assert_eq!(template, template!(Text("岩佐 作"), Key("太"), Text("郎"))); + } + + #[test] + fn multibyte_with_escapes() { + let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap(); + assert_eq!( + template, + template!( + Text("日本"), + Text(r"{"), + Text("アナキスト"), + Text(r"}"), + Text("連盟") + ) + ); + } + + #[test] + fn multibyte_rtl_text() { + let template = Template::parse("محمد صايل").unwrap(); + assert_eq!(template, template!(Text("محمد صايل"))); + } + + #[test] + fn multibyte_rtl_key() { + let template = Template::parse("محمد {ريشة}").unwrap(); + assert_eq!(template, template!(Text("محمد "), Key("ريشة"))); + } +} + +#[cfg(test)] +mod test_error { + use crate::{ParseError, Template}; + + #[test] + fn key_left_half() { + let template = Template::parse("{ open").unwrap_err(); + assert_eq!(template, ParseError::unbalanced("{ open", 0, 6)); + } + + #[test] + fn key_right_half() { + let template = Template::parse("open }").unwrap_err(); + assert_eq!(template, ParseError::unbalanced("open }", 5, 5)); + } + + #[test] + fn key_with_half_escape() { + let template = Template::parse(r"this is { not \ allowed }").unwrap_err(); + assert_eq!( + template, + ParseError::key_escape(r"this is { not \ allowed }", 8, 14) + ); + } + + #[test] + fn key_with_full_escape() { + let template = Template::parse(r"{ not \} allowed }").unwrap_err(); + assert_eq!( + template, + ParseError::key_escape(r"{ not \} allowed }", 0, 6) + ); + } + + #[test] + fn key_empty() { + let template = Template::parse(r"void: {}").unwrap_err(); + assert_eq!(template, ParseError::key_empty(r"void: {}", 6, 7)); + } + + #[test] + fn key_only_whitespace() { + let template = Template::parse(r"nothing: { }").unwrap_err(); + assert_eq!(template, ParseError::key_empty(r"nothing: { }", 9, 11)); + } + + #[test] + fn bad_escape() { + let template = Template::parse(r"not \a thing").unwrap_err(); + assert_eq!(template, ParseError::escape(r"not \a thing", 4, 5)); + } + + #[test] + fn end_escape() { + let template = Template::parse(r"forget me not \").unwrap_err(); + assert_eq!(template, ParseError::escape(r"forget me not \", 14, 15)); + } +} diff --git a/crates/leon/src/template.rs b/crates/leon/src/template.rs new file mode 100644 index 00000000..bf32e92d --- /dev/null +++ b/crates/leon/src/template.rs @@ -0,0 +1,198 @@ +use std::{borrow::Cow, fmt::Display, io::Write, ops::Add}; + +use crate::{ParseError, RenderError, Values}; + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct Template<'s> { + pub items: Cow<'s, [Item<'s>]>, + pub default: Option>, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Item<'s> { + Text(&'s str), + Key(&'s str), +} + +impl<'s> Template<'s> { + /// Construct a template with the given items and default. + /// + /// You can write a template literal without any help by constructing it directly: + /// + /// ``` + /// use std::borrow::Cow; + /// use leon::{Item, Template}; + /// const TEMPLATE: Template = Template { + /// items: Cow::Borrowed({ + /// const ITEMS: &'static [Item<'static>] = &[ + /// Item::Text("Hello"), + /// Item::Key("name"), + /// ]; + /// ITEMS + /// }), + /// default: None, + /// }; + /// assert_eq!(TEMPLATE.render(&[("name", "world")]).unwrap(), "Helloworld"); + /// ``` + /// + /// As that's a bit verbose, using this function and the enum shorthands can be helpful: + /// + /// ``` + /// use leon::{Item, Item::*, Template}; + /// const TEMPLATE: Template = Template::new({ + /// const ITEMS: &'static [Item<'static>] = &[Text("Hello "), Key("name")]; + /// ITEMS + /// }, Some("world")); + /// + /// assert_eq!(TEMPLATE.render(&[("unrelated", "value")]).unwrap(), "Hello world"); + /// ``` + /// + /// For an even more ergonomic syntax, see the [`leon::template!`] macro. + pub const fn new(items: &'s [Item<'s>], default: Option<&'s str>) -> Template<'s> { + Template { + items: Cow::Borrowed(items), + default: match default { + Some(default) => Some(Cow::Borrowed(default)), + None => None, + }, + } + } + + /// Parse a template from a string. + /// + /// # Syntax + /// + /// ```plain + /// it is better to rule { group } + /// one can live {adverb} without power + /// ``` + /// + /// A replacement is denoted by `{` and `}`. The contents of the braces, trimmed + /// of any whitespace, are the key. Any text outside of braces is left as-is. + /// + /// To escape a brace, use `\{` or `\}`. To escape a backslash, use `\\`. Keys + /// cannot contain escapes. + /// + /// ```plain + /// \{ leon \} + /// ``` + /// + /// The above examples, given the values `group = "no one"` and + /// `adverb = "honourably"`, would render to: + /// + /// ```plain + /// it is better to rule no one + /// one can live honourably without power + /// { leon } + /// ``` + /// + /// # Example + /// + /// ``` + /// use leon::Template; + /// let template = Template::parse("hello {name}").unwrap(); + /// ``` + /// + pub fn parse(s: &'s str) -> Result { + Self::parse_items(s).map(|items| Template { + items: Cow::Owned(items), + default: None, + }) + } + + pub fn render_into( + &self, + writer: &mut dyn Write, + values: &dyn Values, + ) -> Result<(), RenderError> { + for token in self.items.as_ref() { + match token { + Item::Text(text) => writer.write_all(text.as_bytes())?, + Item::Key(key) => { + if let Some(value) = values.get_value(key) { + writer.write_all(value.as_bytes())?; + } else if let Some(default) = &self.default { + writer.write_all(default.as_bytes())?; + } else { + return Err(RenderError::MissingKey(key.to_string())); + } + } + } + } + Ok(()) + } + + pub fn render(&self, values: &dyn Values) -> Result { + let mut buf = Vec::with_capacity( + self.items + .iter() + .map(|item| match item { + Item::Key(_) => 0, + Item::Text(t) => t.len(), + }) + .sum(), + ); + self.render_into(&mut buf, values)?; + + // UNWRAP: We know that the buffer is valid UTF-8 because we only write strings. + Ok(String::from_utf8(buf).unwrap()) + } + + pub fn has_key(&self, key: &str) -> bool { + self.has_keys(&[key]) + } + + pub fn has_keys(&self, keys: &[&str]) -> bool { + self.items.iter().any(|token| match token { + Item::Key(k) => keys.contains(k), + _ => false, + }) + } + + pub fn keys(&self) -> impl Iterator { + self.items.iter().filter_map(|token| match token { + Item::Key(k) => Some(k), + _ => None, + }) + } + + /// Sets the default value for this template. + pub fn set_default(&mut self, default: &dyn Display) { + self.default = Some(Cow::Owned(default.to_string())); + } +} + +impl<'s> Add for Template<'s> { + type Output = Self; + + fn add(mut self, rhs: Self) -> Self::Output { + self.items + .to_mut() + .extend(rhs.items.as_ref().iter().cloned()); + if let Some(default) = rhs.default { + self.default = Some(default); + } + self + } +} + +#[cfg(test)] +mod test { + use crate::Item::{Key, Text}; + + #[test] + fn concat_templates() { + let t1 = crate::template!(Text("Hello"), Key("name")); + let t2 = crate::template!(Text("have a"), Key("adjective"), Text("day")); + assert_eq!( + t1 + t2, + crate::template!( + Text("Hello"), + Key("name"), + Text("have a"), + Key("adjective"), + Text("day") + ), + ); + } +} diff --git a/crates/leon/src/values.rs b/crates/leon/src/values.rs new file mode 100644 index 00000000..3e7d8ca9 --- /dev/null +++ b/crates/leon/src/values.rs @@ -0,0 +1,133 @@ +use std::{ + borrow::{Borrow, Cow}, + collections::{BTreeMap, HashMap}, + hash::{BuildHasher, Hash}, +}; + +pub trait Values { + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option>; +} + +impl Values for &T +where + T: Values, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + T::get_value(self, key) + } +} + +impl Values for [(K, V)] +where + K: AsRef, + V: AsRef, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + self.iter().find_map(|(k, v)| { + if k.as_ref() == key { + Some(Cow::Borrowed(v.as_ref())) + } else { + None + } + }) + } +} + +impl Values for &[(K, V)] +where + K: AsRef, + V: AsRef, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + (*self).get_value(key) + } +} + +impl Values for [(K, V); N] +where + K: AsRef, + V: AsRef, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + self.as_slice().get_value(key) + } +} + +impl Values for Vec<(K, V)> +where + K: AsRef, + V: AsRef, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + self.as_slice().get_value(key) + } +} + +impl Values for HashMap +where + K: Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + self.get(key).map(|v| Cow::Borrowed(v.as_ref())) + } +} + +impl Values for BTreeMap +where + K: Borrow + Ord, + V: AsRef, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + self.get(key).map(|v| Cow::Borrowed(v.as_ref())) + } +} + +/// Workaround to allow using functions as [`Values`]. +/// +/// As this isn't constructible you'll want to use [`vals()`] instead. +pub struct ValuesFn +where + F: for<'s> Fn(&'s str) -> Option> + Send + 'static, +{ + inner: F, +} + +impl Values for ValuesFn +where + F: for<'s> Fn(&'s str) -> Option> + Send + 'static, +{ + fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option> { + (self.inner)(key) + } +} + +impl From for ValuesFn +where + F: for<'s> Fn(&'s str) -> Option> + Send + 'static, +{ + fn from(inner: F) -> Self { + Self { inner } + } +} + +/// Workaround to allow using functions as [`Values`]. +/// +/// Wraps your function so it implements [`Values`]. +/// +/// # Example +/// +/// ``` +/// use leon::{Values, vals}; +/// +/// fn use_values(_values: impl Values) {} +/// +/// use_values(vals(|_| Some("hello".into()))); +/// ``` +pub const fn vals(func: F) -> ValuesFn +where + F: for<'s> Fn(&'s str) -> Option> + Send + 'static, +{ + ValuesFn { inner: func } +}