diff --git a/Cargo.lock b/Cargo.lock index d8c39fb65..2c7608ad0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "1.1.2" @@ -11,6 +26,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.4" @@ -59,6 +89,67 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "async-trait" +version = "0.1.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.1" @@ -74,12 +165,48 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "4.4.7" @@ -111,7 +238,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.38", ] [[package]] @@ -126,6 +253,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "cpufeatures" version = "0.2.11" @@ -145,6 +299,51 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.38", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "deranged" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -155,6 +354,48 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -168,6 +409,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.5" @@ -178,6 +425,131 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -188,6 +560,54 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 1.9.3", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" + [[package]] name = "heck" version = "0.4.1" @@ -201,234 +621,1368 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] -name = "humantime" -version = "2.1.0" +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "log", + "rustls", + "rustls-native-certs", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown 0.14.2", + "serde", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "iri-string" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21859b667d66a4c1dacd9df0863b3efb65785474255face87f5bca39dd8407c0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix", + "windows-sys", +] + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155c4d7e39ad04c172c5e3a99c434ea3b4a7ba7960b38ecd562b270b097cce09" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "octocrab" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfeeafb5fa0da7046229ec3c7b3bd2981aae05c549871192c408d59fc0fffd5" +dependencies = [ + "arc-swap", + "async-trait", + "base64", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-timeout", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a257ad03cd8fb16ad4172fedf8094451e1af1c4b70097636ef2eac9a5f0cc33" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pem" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3163d2912b7c3b52d651a055f2c7eec9ba5cd22d26ef75b8dd3a59980b185923" +dependencies = [ + "base64", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +dependencies = [ + "memchr", + "serde", + "serde_json", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "pest_meta" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[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.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustls" +version = "0.21.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.192" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_json" +version = "1.0.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.1.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] -name = "is-terminal" -version = "0.4.9" +name = "tempfile" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ - "hermit-abi", + "cfg-if", + "fastrand", + "redox_syscall", "rustix", "windows-sys", ] [[package]] -name = "libc" -version = "0.2.149" +name = "termcolor" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +dependencies = [ + "winapi-util", +] [[package]] -name = "linux-raw-sys" -version = "0.4.10" +name = "thiserror" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] [[package]] -name = "log" -version = "0.4.20" +name = "thiserror-impl" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] [[package]] -name = "memchr" -version = "2.6.4" +name = "time" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] [[package]] -name = "once_cell" -version = "1.18.0" +name = "time-core" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] -name = "pest" -version = "2.7.5" +name = "time-macros" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9cee2a55a544be8b89dc6848072af97a20f2422603c10865be2a42b580fff5" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ - "memchr", - "thiserror", - "ucd-trie", + "time-core", ] [[package]] -name = "pest_derive" -version = "2.7.5" +name = "tinyvec" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d78524685f5ef2a3b3bd1cafbc9fcabb036253d9b1463e726a91cd16e2dfc2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ - "pest", - "pest_generator", + "tinyvec_macros", ] [[package]] -name = "pest_generator" -version = "2.7.5" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bd1206e71118b5356dae5ddc61c8b11e28b09ef6a31acbd15ea48a28e0c227" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys", ] [[package]] -name = "pest_meta" -version = "2.7.5" +name = "tokio-io-timeout" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c747191d4ad9e4a4ab9c8798f1e82a39affe7ef9648390b7e5548d18e099de6" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "once_cell", - "pest", - "sha2", + "pin-project-lite", + "tokio", ] [[package]] -name = "proc-macro2" -version = "1.0.69" +name = "tokio-macros" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ - "unicode-ident", + "proc-macro2", + "quote", + "syn 2.0.38", ] [[package]] -name = "quote" -version = "1.0.33" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "proc-macro2", + "native-tls", + "tokio", ] [[package]] -name = "regex" -version = "1.10.2" +name = "tokio-rustls" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", + "rustls", + "tokio", ] [[package]] -name = "regex-automata" -version = "0.4.3" +name = "tokio-util" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", ] [[package]] -name = "regex-syntax" -version = "0.8.2" +name = "toml" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] [[package]] -name = "rustix" -version = "0.38.21" +name = "toml_datetime" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", + "serde", ] [[package]] -name = "serde" -version = "1.0.192" +name = "toml_edit" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" dependencies = [ - "serde_derive", + "indexmap 2.1.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] -name = "serde_derive" -version = "1.0.192" +name = "tower" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "sha2" -version = "0.10.8" +name = "tower-http" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "bitflags 2.4.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", ] [[package]] -name = "strsim" -version = "0.10.0" +name = "tower-layer" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" [[package]] -name = "syn" -version = "2.0.38" +name = "tower-service" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] -name = "termcolor" -version = "1.3.0" +name = "tracing" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "winapi-util", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", ] [[package]] -name = "thiserror" -version = "1.0.50" +name = "tracing-attributes" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "thiserror-impl", + "proc-macro2", + "quote", + "syn 2.0.38", ] [[package]] -name = "thiserror-impl" -version = "1.0.50" +name = "tracing-core" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "proc-macro2", - "quote", - "syn", + "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + [[package]] name = "typenum" version = "1.17.0" @@ -441,34 +1995,174 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" + [[package]] name = "wdl-grammar" version = "0.1.0" dependencies = [ + "async-recursion", + "chrono", "clap", + "colored", + "dirs", "env_logger", + "indexmap 2.1.0", "log", + "octocrab", "pest", "pest_derive", + "reqwest", "serde", + "serde_with", + "tokio", + "toml", +] + +[[package]] +name = "web-sys" +version = "0.3.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +dependencies = [ + "js-sys", + "wasm-bindgen", ] [[package]] @@ -502,6 +2196,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -567,3 +2270,28 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml index 5d4984670..f20a43b06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,12 @@ license = "MIT OR Apache-2.0" edition = "2021" [workspace.dependencies] +clap = { version = "4.4.7", features = ["derive"] } env_logger = "0.10.0" +indexmap = { version = "2.1.0", features = ["serde"] } log = "0.4.20" +pest = { version = "2.7.5", features = ["pretty-print"] } +pest_derive = "2.7.5" serde = { version = "1", features = ["derive"] } +serde_with = { version = "3.4.0" } +toml = "0.8.8" diff --git a/Gauntlet.toml b/Gauntlet.toml new file mode 100644 index 000000000..79d92964f --- /dev/null +++ b/Gauntlet.toml @@ -0,0 +1,47 @@ +version = "v1" + +[[repositories]] +organization = "PacificBiosciences" +name = "HiFi-human-WGS-WDL" + +[[repositories]] +organization = "biowdl" +name = "tasks" + +[[repositories]] +organization = "stjudecloud" +name = "workflows" + +[[repositories]] +organization = "chanzuckerberg" +name = "czid-workflows" + +[[ignored_errors]] +document = "biowdl/tasks:bedtools.wdl" +error = """ + --> 29:67 + | +29 | String memory = \"~{512 + ceil(size([inputBed, faidx], \"MiB\"))}MiB\" + | ^--- + | + = expected WHITESPACE or OPTION""" + +[[ignored_errors]] +document = "biowdl/tasks:bowtie.wdl" +error = """ + --> 40:58 + | +40 | String memory = \"~{5 + ceil(size(indexFiles, \"GiB\"))}GiB\" + | ^--- + | + = expected WHITESPACE or OPTION""" + +[[ignored_errors]] +document = "stjudecloud/workflows:template/task-templates.wdl" +error = """ + --> 17:25 + | +17 | Int memory_gb = <> + | ^--- + | + = expected WHITESPACE, COMMENT, or expression""" diff --git a/wdl-grammar/Cargo.toml b/wdl-grammar/Cargo.toml index c398a05b2..9c664fb07 100644 --- a/wdl-grammar/Cargo.toml +++ b/wdl-grammar/Cargo.toml @@ -5,9 +5,41 @@ edition.workspace = true license.workspace = true [dependencies] -clap = { version = "4.4.6", features = ["derive"] } -env_logger.workspace = true -log.workspace = true -pest = "2.7.5" -pest_derive = "2.7.5" -serde.workspace = true +async-recursion = { version = "1.0.5", optional = true } +chrono = { version = "0.4.31", optional = true } +clap = { workspace = true, optional = true } +colored = { version = "2.0.4", optional = true } +dirs = { version = "5.0.1", optional = true } +env_logger = { workspace = true, optional = true } +indexmap = { workspace = true, optional = true } +log = { workspace = true, optional = true } +octocrab = { version = "0.32.0", optional = true } +pest = { workspace = true } +pest_derive = { workspace = true } +reqwest = { version = "0.11.22", optional = true } +serde = { workspace = true } +serde_with = { workspace = true, optional = true } +tokio = { version = "1.33.0", features = ["full"], optional = true} +toml = { workspace = true, optional = true } + +[features] +binaries = [ + "async-recursion", + "chrono", + "clap", + "colored", + "dirs", + "env_logger", + "indexmap", + "log", + "octocrab", + "reqwest", + "serde_with", + "tokio", + "toml" +] + +[[bin]] +name = "wdl-grammar" +path = "src/main.rs" +required-features = ["binaries"] diff --git a/wdl-grammar/src/bin/wdl-grammar-create-test.rs b/wdl-grammar/src/bin/wdl-grammar-create-test.rs deleted file mode 100644 index 3de37fbc5..000000000 --- a/wdl-grammar/src/bin/wdl-grammar-create-test.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! A command-line tool to automatically generate tests for WDL syntax. -//! -//! This tool is only intended to be used in the development of the -//! `wdl-grammar` package. It was written quickly and relatively sloppily in -//! contrast to the rest of this package—please keep that in mind! - -#![warn(missing_docs)] -#![warn(rust_2018_idioms)] -#![warn(rust_2021_compatibility)] -#![warn(missing_debug_implementations)] -#![deny(rustdoc::broken_intra_doc_links)] - -use std::fs; -use std::path::PathBuf; - -use clap::Parser; -use log::LevelFilter; - -use pest::Parser as _; - -use pest::iterators::Pair; -use wdl_grammar as wdl; - -use wdl::Version; - -/// An error related to the `wdl` command-line tool. -#[derive(Debug)] -pub enum Error { - /// An input/output error. - IoError(std::io::Error), - - /// Attempted to access a file, but it was missing. - FileDoesNotExist(PathBuf), - - /// Unknown rule name. - UnknownRule(String), - - /// An error from Pest. - PestError(Box>), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::IoError(err) => write!(f, "i/o error: {err}"), - Error::FileDoesNotExist(path) => write!(f, "file does not exist: {}", path.display()), - Error::UnknownRule(rule) => { - write!(f, "unknown rule: {rule}") - } - Error::PestError(err) => write!(f, "pest error:\n{err}"), - } - } -} - -impl std::error::Error for Error {} - -type Result = std::result::Result; - -/// A command-line tool to automatically generate tests for WDL syntax. -#[derive(Debug, Parser)] -pub struct Args { - /// The path to the document. - path: PathBuf, - - /// The WDL specification version to use. - #[arg(short = 's', long, default_value_t, value_enum)] - specification_version: Version, - - /// The rule to evaluate. - #[arg(short = 'r', long, default_value = "document")] - rule: String, -} - -fn inner() -> Result<()> { - let args = Args::parse(); - - env_logger::builder() - .filter_level(LevelFilter::Debug) - .init(); - - let rule = match args.specification_version { - Version::V1 => wdl::v1::get_rule(&args.rule) - .map(Ok) - .unwrap_or_else(|| Err(Error::UnknownRule(args.rule.clone())))?, - }; - - let contents = fs::read_to_string(args.path).map_err(Error::IoError)?; - - match args.specification_version { - Version::V1 => { - let parse_tree: pest::iterators::Pairs<'_, wdl::v1::Rule> = - wdl::v1::Parser::parse(rule, &contents) - .map_err(|err| Error::PestError(Box::new(err)))?; - - for pair in parse_tree { - print_create_test_recursive(pair, 0); - } - - Ok(()) - } - } -} - -fn print_create_test_recursive(pair: Pair<'_, wdl::v1::Rule>, indent: usize) { - let span = pair.as_span(); - let comment = pair - .as_str() - .lines() - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect::>() - .join(" "); - - if !comment.is_empty() { - println!("{}// `{}`", " ".repeat(indent), comment); - } - print!( - "{}{:?}({}, {}", - " ".repeat(indent), - pair.as_rule(), - span.start(), - span.end() - ); - - let inner = pair.into_inner(); - - if inner.peek().is_some() { - println!(", ["); - - for pair in inner { - print_create_test_recursive(pair, indent + 2); - println!(","); - } - - print!("{}]", " ".repeat(indent)); - } - - print!(")"); -} - -fn main() { - match inner() { - Ok(_) => {} - Err(err) => eprintln!("{}", err), - } -} diff --git a/wdl-grammar/src/lib.rs b/wdl-grammar/src/lib.rs index 5729bb5a3..65a38f139 100644 --- a/wdl-grammar/src/lib.rs +++ b/wdl-grammar/src/lib.rs @@ -1,20 +1,32 @@ //! A crate for lexing and parsing the Workflow Description Language //! (WDL) using [`pest`](https://pest.rs). +#![feature(let_chains)] #![warn(rust_2018_idioms)] #![warn(rust_2021_compatibility)] #![warn(missing_debug_implementations)] #![deny(rustdoc::broken_intra_doc_links)] +#[cfg(feature = "binaries")] use clap::ValueEnum; +use serde::Deserialize; use serde::Serialize; pub mod v1; -#[derive(Clone, Debug, Default, Serialize, ValueEnum)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(feature = "binaries", derive(ValueEnum))] #[serde(rename_all = "lowercase")] pub enum Version { /// Version 1.x of the WDL specification. #[default] V1, } + +impl std::fmt::Display for Version { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Version::V1 => write!(f, "WDL v1.x"), + } + } +} diff --git a/wdl-grammar/src/main.rs b/wdl-grammar/src/main.rs index 425e54d93..71799f5fa 100644 --- a/wdl-grammar/src/main.rs +++ b/wdl-grammar/src/main.rs @@ -1,134 +1,108 @@ -//! A command-line tool for parsing and exploring Workflow Description Language -//! (WDL) documents. +//! A command-line tool for parsing and testing Workflow Description Language +//! (WDL) grammar. //! -//! This tool is intended to be used as a utility to test and develop the -//! [`wdl`](https://crates.io/wdl) crate. +//! **Note:** this tool is intended to be used as a utility to test and develop +//! the [`wdl_grammar`](https://crates.io/wdl_grammar) crate. It is not intended +//! to be used by a general audience for linting or parsing WDL documents. +#![feature(let_chains)] #![warn(missing_docs)] #![warn(rust_2018_idioms)] #![warn(rust_2021_compatibility)] #![warn(missing_debug_implementations)] +#![warn(clippy::missing_docs_in_private_items)] #![deny(rustdoc::broken_intra_doc_links)] -use std::fs; -use std::path::PathBuf; - use clap::Parser; use clap::Subcommand; use log::LevelFilter; -use pest::Parser as _; - -use wdl_grammar as wdl; - -use wdl::Version; - -/// An error related to the `wdl` command-line tool. -#[derive(Debug)] -pub enum Error { - /// An input/output error. - IoError(std::io::Error), - - /// Attempted to access a file, but it was missing. - FileDoesNotExist(PathBuf), - - /// Unknown rule name. - UnknownRule(String), - - /// An error from Pest. - PestError(Box>), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::IoError(err) => write!(f, "i/o error: {err}"), - Error::FileDoesNotExist(path) => write!(f, "file does not exist: {}", path.display()), - Error::UnknownRule(rule) => { - write!(f, "unknown rule: {rule}") - } - Error::PestError(err) => write!(f, "pest error:\n{err}"), - } - } -} - -impl std::error::Error for Error {} +mod subcommands; -type Result = std::result::Result; +use crate::subcommands::create_test; +use crate::subcommands::gauntlet; +use crate::subcommands::parse; -/// Arguments for the `parse` subcommand. -#[derive(Debug, Parser)] -pub struct ParseArgs { - /// The path to the document. - path: PathBuf, - - /// The WDL specification version to use. - #[arg(short = 's', long, default_value_t, value_enum)] - specification_version: Version, - - /// The rule to evaluate. - #[arg(short = 'r', long, default_value = "document")] - rule: String, -} - -/// Subcommands for the `wdl` command-line tool. +/// Subcommands for the `wdl-grammar` command-line tool. #[derive(Debug, Subcommand)] pub enum Command { - /// Parses the Workflow Description Language document and prints the parse - /// tree. - Parse(ParseArgs), + /// Creates a test for a given input and grammar rule. + CreateTest(create_test::Args), + + /// Performs a gauntlet of parsing tests. + Gauntlet(gauntlet::Args), + + /// Parses an input according to the specified grammar rule. + Parse(parse::Args), } -/// Parse and describe Workflow Description Language documents. +/// Parse and testing Workflow Description Language (WDL) grammar. #[derive(Parser, Debug)] #[command(author, version, about, long_about)] struct Args { /// The subcommand to execute. #[command(subcommand)] command: Command, + + /// Detailed information, including debug information, is logged in the + /// console. + #[arg(short, long, global = true)] + debug: bool, + + /// Enables logging for all modules (not just `wdl-grammar`). + #[arg(short, long, global = true)] + log_all_modules: bool, + + /// Only errors are logged to the console. + #[arg(short, long, global = true)] + quiet: bool, + + /// All available information, including trace information, is logged in the + /// console. + #[arg(short, long, global = true)] + trace: bool, + + /// Additional information is logged in the console. + #[arg(short, long, global = true)] + verbose: bool, } -fn inner() -> Result<()> { +/// The inner function for the binary. +async fn inner() -> Result<(), Box> { let args = Args::parse(); - env_logger::builder() - .filter_level(LevelFilter::Debug) - .init(); + let level = if args.trace { + LevelFilter::max() + } else if args.debug { + LevelFilter::Debug + } else if args.verbose { + LevelFilter::Info + } else if args.quiet { + LevelFilter::Error + } else { + LevelFilter::Warn + }; + + let module = match args.log_all_modules { + true => None, + false => Some("wdl_grammar"), + }; + + env_logger::builder().filter(module, level).init(); match args.command { - Command::Parse(args) => { - let rule = match args.specification_version { - Version::V1 => wdl::v1::get_rule(&args.rule) - .map(Ok) - .unwrap_or_else(|| Err(Error::UnknownRule(args.rule.clone())))?, - }; - - let contents = fs::read_to_string(args.path).map_err(Error::IoError)?; - - let mut parse_tree = match args.specification_version { - Version::V1 => wdl::v1::Parser::parse(rule, &contents) - .map_err(|err| Error::PestError(Box::new(err)))?, - }; - - // For documents, we don't care about the parent element: it is much - // more informative to see the children of the document split by - // spaces. This is a stylistic choice. - if args.rule == "document" { - for element in parse_tree.next().unwrap().into_inner() { - dbg!(element); - } - } else { - dbg!(parse_tree); - }; - } - } + Command::CreateTest(args) => create_test::create_test(args)?, + Command::Gauntlet(args) => gauntlet::gauntlet(args).await?, + Command::Parse(args) => parse::parse(args)?, + }; Ok(()) } -fn main() { - match inner() { +#[tokio::main] +async fn main() { + match inner().await { Ok(_) => {} - Err(err) => eprintln!("{}", err), + Err(err) => eprintln!("error: {}", err), } } diff --git a/wdl-grammar/src/subcommands.rs b/wdl-grammar/src/subcommands.rs new file mode 100644 index 000000000..de7284ae1 --- /dev/null +++ b/wdl-grammar/src/subcommands.rs @@ -0,0 +1,38 @@ +//! Subcommands for the `wdl-grammar` command-line tool. + +use log::debug; + +pub mod create_test; +pub mod gauntlet; +pub mod parse; + +/// An error common to any subcommand. +#[derive(Debug)] +pub enum Error { + /// An input/output error. + InputOutput(std::io::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InputOutput(err) => write!(f, "i/o error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// Gets lines of input from STDIN. +pub fn get_contents_stdin() -> Result { + debug!("Reading from STDIN..."); + + Ok(std::io::stdin() + .lines() + .collect::, _>>() + .map_err(Error::InputOutput)? + .join("\n")) +} diff --git a/wdl-grammar/src/subcommands/create_test.rs b/wdl-grammar/src/subcommands/create_test.rs new file mode 100644 index 000000000..c3760cec8 --- /dev/null +++ b/wdl-grammar/src/subcommands/create_test.rs @@ -0,0 +1,145 @@ +//! `wdl-grammar create-test` + +use clap::Parser; +use pest::iterators::Pair; +use pest::Parser as _; +use pest::RuleType; + +use wdl_grammar as grammar; + +use crate::subcommands::get_contents_stdin; + +/// An error related to the `wdl-grammar create-test` subcommand. +#[derive(Debug)] +pub enum Error { + /// A common error. + Common(super::Error), + + /// Multiple root nodes parsed. + MultipleRootNodes, + + /// A parsing error from Pest. + Parse(Box), + + /// Unknown rule name. + UnknownRule { + /// The name of the rule. + name: String, + + /// The grammar being used. + grammar: grammar::Version, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Common(err) => write!(f, "{err}"), + Error::MultipleRootNodes => write!(f, "multiple root nodes found"), + Error::UnknownRule { name, grammar } => { + write!(f, "unknown rule '{name}' for grammar {grammar}") + } + Error::Parse(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// Arguments for the `wdl-grammar create-test` subcommand. +#[derive(Debug, Parser)] +pub struct Args { + /// The input to parse. + #[clap(value_name = "INPUT")] + input: Option, + + /// The Workflow Description Language (WDL) specification version to use. + #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] + specification_version: grammar::Version, + + /// The parser rule to evaluate. + #[arg(value_name = "RULE", short = 'r', long, default_value = "document")] + rule: String, +} + +/// Main function for this subcommand. +pub fn create_test(args: Args) -> Result<()> { + let rule = match args.specification_version { + grammar::Version::V1 => grammar::v1::get_rule(&args.rule) + .map(Ok) + .unwrap_or_else(|| { + Err(Error::UnknownRule { + name: args.rule.clone(), + grammar: args.specification_version.clone(), + }) + })?, + }; + + let input = args + .input + .map(Ok) + .unwrap_or_else(|| get_contents_stdin().map_err(Error::Common))?; + + let mut parse_tree = match args.specification_version { + grammar::Version::V1 => { + grammar::v1::Parser::parse(rule, &input).map_err(|err| Error::Parse(Box::new(err)))? + } + }; + + let root = match parse_tree.len() { + // SAFETY: this should not be possible, as parsing just successfully + // completed. As such, we should always have at least one parsed + // element. + 0 => unreachable!(), + 1 => parse_tree.next().unwrap(), + _ => return Err(Error::MultipleRootNodes), + }; + + write_test(root, 0); + + Ok(()) +} + +/// Writes a test by recursively traversing the [`Pair`]. +fn write_test(pair: Pair<'_, R>, indent: usize) { + let span = pair.as_span(); + let prefix = " ".repeat(indent); + + let comment = pair + .as_str() + .lines() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect::>() + .join(" "); + + if !comment.is_empty() { + println!("{}// `{}`", prefix, comment); + } + + print!( + "{}{:?}({}, {}", + prefix, + pair.as_rule(), + span.start(), + span.end() + ); + + let inner = pair.into_inner(); + + if inner.peek().is_some() { + println!(", ["); + + for pair in inner { + write_test(pair, indent + 2); + println!(","); + } + + print!("{}]", prefix); + } + + print!(")"); +} diff --git a/wdl-grammar/src/subcommands/gauntlet.rs b/wdl-grammar/src/subcommands/gauntlet.rs new file mode 100644 index 000000000..68cc6f4e6 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet.rs @@ -0,0 +1,295 @@ +//! `wdl-grammar gauntlet` + +use std::collections::HashSet; +use std::path::PathBuf; +use std::process; + +use clap::Parser; +use colored::Colorize as _; +use log::debug; +use log::trace; +use pest::Parser as _; + +pub mod config; +pub mod document; +mod report; +pub mod repository; + +pub use config::Config; +pub use report::Report; +pub use repository::Repository; + +use wdl_grammar as grammar; + +use crate::subcommands::gauntlet::report::Status; +use crate::subcommands::gauntlet::repository::options; +use crate::subcommands::gauntlet::repository::Identifier; + +/// The exit code to emit when any test unexpectedly fails. +const EXIT_CODE_FAILED: i32 = 1; + +/// The exit code to emit when an error was expected but not encountered. +const EXIT_CODE_UNDETECTED_IGNORED_ERRORS: i32 = 2; + +/// An error related to the `wdl-grammar gauntlet` subcommand. +#[derive(Debug)] +pub enum Error { + /// A configuration file error. + Config(config::Error), + + /// An input/output error. + InputOutput(std::io::Error), + + /// An error related to a [`Repository`]. + Repository(repository::Error), + + /// An error related to a repository [`Builder`](repository::Builder). + RepositoryBuilder(repository::builder::Error), + + /// An error related to a repository [`Identifier`]. + RepositoryIdentifier(repository::identifier::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Config(err) => write!(f, "configuration file error: {err}"), + Error::InputOutput(err) => write!(f, "i/o error: {err}"), + Error::Repository(err) => write!(f, "repository error: {err}"), + Error::RepositoryBuilder(err) => write!(f, "repository builder error: {err}"), + Error::RepositoryIdentifier(err) => write!(f, "repository identifier error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// Arguments for the `wdl-grammar gauntlet` subcommand. +#[derive(Debug, Parser)] +pub struct Args { + /// The GitHub repositories to evaluate (e.g., "stjudecloud/workflows"). + repositories: Option>, + + /// The location of the cache directory. + #[arg(long)] + cache_dir: Option, + + /// The location of the config file. + #[arg(short, long)] + config_file: Option, + + /// Only errors are printed to the stderr stream. + #[arg(short, long, global = true)] + quiet: bool, + + /// Overwrites the configuration file. + #[arg(short, long, global = true)] + save_config: bool, + + /// Silences printing detailed error information. + #[arg(long, global = true)] + silence_error_details: bool, + + /// Skips the retreiving of remote objects. + #[arg(long, global = true)] + skip_remote: bool, + + /// The Workflow Description Language (WDL) specification version to use. + #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] + specification_version: grammar::Version, + + /// All available information, including debug information, is logged. + #[arg(short, long, global = true)] + verbose: bool, +} + +/// Main function for this subcommand. +pub async fn gauntlet(args: Args) -> Result<()> { + let path = args.config_file.unwrap_or(Config::default_path()); + let mut config = + Config::load_or_new(path, args.specification_version).map_err(Error::Config)?; + + if let Some(repositories) = args.repositories { + config.repositories_mut().extend( + repositories + .into_iter() + .map(|value| { + value + .parse::() + .map_err(Error::RepositoryIdentifier) + }) + .collect::>>()?, + ); + } + + let mut report = Report::new(std::io::stdout().lock()); + + for (index, repository_identifier) in config.repositories().iter().enumerate() { + let mut repository = + repository::Builder::default().identifier(repository_identifier.clone()); + + if let Some(ref root) = args.cache_dir { + let mut repository_cache_root = root.clone(); + repository_cache_root.push(repository_identifier.organization()); + repository_cache_root.push(repository_identifier.name()); + repository = repository.root(repository_cache_root); + } + + if args.skip_remote { + let options = options::Builder::default().hydrate_remote(false).build(); + repository = repository.options(options) + } + + let mut repository = repository.try_build().map_err(Error::RepositoryBuilder)?; + let results = repository.hydrate().await.map_err(Error::Repository)?; + + report + .title(repository_identifier) + .map_err(Error::InputOutput)?; + report.next_section().map_err(Error::InputOutput)?; + + for (path, content) in results { + let document_identifier = + document::Identifier::new(repository_identifier.clone(), path); + + match config.version() { + grammar::Version::V1 => { + match grammar::v1::Parser::parse(grammar::v1::Rule::document, &content) { + Ok(_) => { + trace!("{}: successfully parsed.", document_identifier); + report + .register(document_identifier, Status::Success) + .map_err(Error::InputOutput)?; + } + Err(err) => { + let actual_error = err.to_string(); + + if let Some(expected_error) = + config.ignored_errors().get(&document_identifier) + { + if expected_error == &actual_error { + trace!( + "{}: removing from expected errors.", + document_identifier + ); + report + .register( + document_identifier, + Status::Ignored(actual_error), + ) + .map_err(Error::InputOutput)?; + } else { + trace!("{}: mismatched error message.", document_identifier); + report + .register( + document_identifier, + Status::Mismatch(actual_error), + ) + .map_err(Error::InputOutput)?; + } + } else { + trace!("{}: not present in expected errors.", document_identifier); + report + .register(document_identifier, Status::Error(actual_error)) + .map_err(Error::InputOutput)?; + } + } + } + } + } + } + + report.next_section().map_err(Error::InputOutput)?; + + if !args.silence_error_details { + report + .report_unexpected_errors_for_repository(repository_identifier) + .map_err(Error::InputOutput)?; + report.next_section().map_err(Error::InputOutput)?; + } + + report + .footer(repository_identifier) + .map_err(Error::InputOutput)?; + report.next_section().map_err(Error::InputOutput)?; + + if index != config.repositories().len() - 1 { + println!(); + } + } + + let detected_errors = report + .results() + .clone() + .into_iter() + .filter(|(_, status)| !status.success()) + .map(|(id, status)| { + ( + id, + match status { + Status::Success => unreachable!(), + Status::Warning => unreachable!(), + Status::Mismatch(msg) => msg, + Status::Error(msg) => msg, + Status::Ignored(msg) => msg, + }, + ) + }) + .collect::>(); + + let ignored_errors = config + .ignored_errors() + .clone() + .into_iter() + .collect::>(); + + let unignored_errors = &detected_errors - &ignored_errors; + let undetected_ignored_errors = &ignored_errors - &detected_errors; + + if args.save_config { + if !undetected_ignored_errors.is_empty() { + debug!( + "removing {} undetected but expected errors.", + undetected_ignored_errors.len() + ); + + *config.ignored_errors_mut() = (&ignored_errors - &undetected_ignored_errors) + .union(&unignored_errors) + .cloned() + .collect(); + } + + config.ignored_errors_mut().extend( + unignored_errors + .into_iter() + .map(|(id, message)| (id.clone(), message.clone())) + .collect::>(), + ); + config.save().map_err(Error::Config)?; + } else if !undetected_ignored_errors.is_empty() { + println!( + "\n{}\n", + "Undetected expected errors: you should remove these from your \ + Config.toml or run this command with the `-s` option!" + .red() + .bold() + ); + + for (document_identifier, error) in undetected_ignored_errors { + println!( + "{}\n\n{}\n", + document_identifier.to_string().italic(), + error + ); + } + + process::exit(EXIT_CODE_UNDETECTED_IGNORED_ERRORS); + } else if !unignored_errors.is_empty() { + process::exit(EXIT_CODE_FAILED); + } + + Ok(()) +} diff --git a/wdl-grammar/src/subcommands/gauntlet/config.rs b/wdl-grammar/src/subcommands/gauntlet/config.rs new file mode 100644 index 000000000..9cd51557e --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/config.rs @@ -0,0 +1,184 @@ +//! Configuration. + +use std::collections::HashSet; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +use grammar::Version; +use log::debug; +use log::log_enabled; +use log::trace; +use wdl_grammar as grammar; + +pub mod inner; + +pub use inner::Inner; + +use crate::subcommands::gauntlet::repository; + +/// The default directory name for the `wdl-grammar` configuration file and +/// cache. +const DEFAULT_CONFIG_DIR: &str = "wdl-grammar"; + +/// The default name for the `wdl-grammar` configuration file. +const DEFAULT_CONFIG_FILE: &str = "Gauntlet.toml"; + +/// An error related to a [`Config`]. +#[derive(Debug)] +pub enum Error { + /// An error serializing TOML. + DeserializeToml(toml::de::Error), + + /// An input/output error. + InputOutput(std::io::Error), + + /// An error serializing TOML. + SerializeToml(toml::ser::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::DeserializeToml(err) => write!(f, "deserialize toml error: {err}"), + Error::InputOutput(err) => write!(f, "i/e error: {err}"), + Error::SerializeToml(err) => write!(f, "serialize toml error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A configuration outer object. +/// +/// This struct holds both (a) the path to the configuration file and (b) the +/// configuration itself. Notably, the path to the configuration file should +/// _not_ be part of the serialized configuration value. Thus, I split the +/// concept of the path and the actual configuration into two different structs. +pub struct Config { + /// The path to the configuration file. + path: PathBuf, + + /// The inner configuration values. + inner: Inner, +} + +impl Config { + /// Gets the default path for the configuration file. + /// + /// * If there exists a file matching the default configuration file name + /// within the current working directory, that is returned. + /// * Otherwise, the default configuration directory is searched for a file + /// matching the default configuration file name. + /// + /// **Note:** the file may not actually exist—it is up to the consumer to + /// check if the file exists before acting on it. + pub fn default_path() -> PathBuf { + let mut path = std::env::current_dir().expect("cannot locate working directory"); + path.push(DEFAULT_CONFIG_FILE); + if path.exists() { + return path; + } + + let mut path = default_config_dir(); + path.push(DEFAULT_CONFIG_FILE); + path + } + + /// Attempts to load configuration values from the provided `path`. + /// + /// * If the `path` exists, the contents of the file will be read and + /// deserialized to the [`Config`] object (pending any errors in + /// deserialization, of course). + /// * If the `path` does _not_ exist, a new, default [`Config`] will be + /// created and returned. + /// + /// In both cases, the `path` will be stored within the [`Config`]. This has + /// the effect of ensuring the value loaded here will be saved to the + /// inteded location (should [`Config::save()`] be called). + pub fn load_or_new(path: PathBuf, version: grammar::Version) -> Result { + if !path.exists() { + return Ok(Self { + path, + inner: Inner::from(version), + }); + } + + debug!("loading from {}.", path.display()); + let contents = std::fs::read_to_string(&path).map_err(Error::InputOutput)?; + let inner = toml::from_str(&contents).map_err(Error::DeserializeToml)?; + + let result = Self { path, inner }; + + if log_enabled!(log::Level::Trace) { + trace!("Loaded configuration file with the following:"); + trace!(" -> {} repositories.", result.repositories().len()); + let num_ignored_errors = result.ignored_errors().len(); + trace!(" -> {} ignored errors.", num_ignored_errors); + } + + Ok(result) + } + + /// Gets the [`Version`] from the [`Config`] by reference. + pub fn version(&self) -> &Version { + &self.inner.version + } + + /// Gets the [`inner::Repositories`] from the [`Config`] by reference. + pub fn repositories(&self) -> &inner::Repositories { + &self.inner.repositories + } + + /// Gets the [`inner::Repositories`] from the [`Config`] by mutable + /// reference. + pub fn repositories_mut(&mut self) -> &mut HashSet { + &mut self.inner.repositories + } + + /// Gets the [`inner::Errors`] from the [`Config`] by reference. + pub fn ignored_errors(&self) -> &inner::Errors { + &self.inner.ignored_errors + } + + /// Gets the [`inner::Errors`] from the [`Config`] by mutable reference. + pub fn ignored_errors_mut(&mut self) -> &mut inner::Errors { + &mut self.inner.ignored_errors + } + + /// Attempts to save the contents of the [`Config`] (in particular, the + /// [`Self::inner`] stored within the [`Config`]) to the path pointed to + /// [`Self::path`]. + pub fn save(&self) -> Result<()> { + if log_enabled!(log::Level::Debug) { + if self.path.exists() { + debug!("overwriting configuration at {}", self.path.display()); + } else { + debug!("saving configuration to {}", self.path.display()); + } + } + + let mut file = File::create(&self.path).map_err(Error::InputOutput)?; + let contents = toml::to_string_pretty(&self.inner).map_err(Error::SerializeToml)?; + + write!(file, "{}", contents).map_err(Error::InputOutput) + } +} + +/// Gets the default configuration directory for this crate. +/// +/// **NOTE:** this function also ensure that the directory exists. +pub fn default_config_dir() -> PathBuf { + // SAFETY: for all of our use cases, this should always unwrap. + let mut path = dirs::home_dir().expect("cannot locate home directory"); + path.push(".config"); + path.push(DEFAULT_CONFIG_DIR); + + // SAFETY: for all of our use cases, this should always unwrap. + std::fs::create_dir_all(&path).expect("could not create config directory"); + + path +} diff --git a/wdl-grammar/src/subcommands/gauntlet/config/inner.rs b/wdl-grammar/src/subcommands/gauntlet/config/inner.rs new file mode 100644 index 000000000..c8c7ae4bb --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/config/inner.rs @@ -0,0 +1,54 @@ +//! An inner representation for the configuration object. +//! +//! This struct holds the configuration values. +use std::collections::HashMap; +use std::collections::HashSet; + +use serde::Deserialize; +use serde::Serialize; +use serde_with::serde_as; +use wdl_grammar as grammar; + +mod repr; + +pub use repr::ErrorsAsReprs; + +use crate::subcommands::gauntlet::document; +use crate::subcommands::gauntlet::repository; + +/// Parsing errors as [`String`]s associated with a [document +/// identifier](document::Identifier). +pub type Errors = HashMap; + +/// A unique set of [repository identifiers](repository::Identifier). +pub type Repositories = HashSet; + + +/// The inner configuration object for a [`Config`](super::Config). +/// +/// This object stores the actual configuration values for this subcommand. +#[serde_as] +#[derive(Debug, Deserialize, Serialize)] +pub struct Inner { + /// The WDL version. + pub(super) version: grammar::Version, + + /// The repositories. + #[serde(default)] + pub(super) repositories: Repositories, + + /// The ignored errors. + #[serde_as(as = "ErrorsAsReprs")] + #[serde(default, skip_serializing_if = "HashMap::is_empty")] + pub(super) ignored_errors: Errors, +} + +impl From for Inner { + fn from(version: grammar::Version) -> Self { + Self { + version, + repositories: Default::default(), + ignored_errors: Default::default(), + } + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/config/inner/repr.rs b/wdl-grammar/src/subcommands/gauntlet/config/inner/repr.rs new file mode 100644 index 000000000..4f1b7c1bb --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/config/inner/repr.rs @@ -0,0 +1,48 @@ +//! Representation of ignored errors as stored in the configuration file. + +use serde::Deserialize; +use serde::Serialize; + +use crate::subcommands::gauntlet::config::inner::Errors; +use crate::subcommands::gauntlet::document; + +/// A representation of an error to ignore as serialized in the configuration +/// file. +/// +/// In short, I wanted to convert the [`Errors`] object to something more +/// visually understandable in the configuration file. Thus, the only purpose of +/// this struct is to serialize and deserialize entries in that +/// [`HashMap`](std::collections::HashMap) in a prettier way. +#[derive(Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Error { + /// The document identifier converted to a [`String`]. + pub document: String, + + /// The error converted to a [`String`]. + pub error: String, +} + +serde_with::serde_conv!( + pub ErrorsAsReprs, + Errors, + |errors: &Errors| { + let mut result = errors + .iter() + .map(|(document, error)| Error { + document: document.to_string(), + error: error.clone(), + }) + .collect::>(); + result.sort(); + result + }, + |errors: Vec| -> Result<_, document::identifier::Error> { + errors + .into_iter() + .map(|repr| { + let identifier = repr.document.parse::()?; + Ok((identifier, repr.error)) + }) + .collect::>() + } +); \ No newline at end of file diff --git a/wdl-grammar/src/subcommands/gauntlet/document.rs b/wdl-grammar/src/subcommands/gauntlet/document.rs new file mode 100644 index 000000000..755d6435b --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/document.rs @@ -0,0 +1,5 @@ +//! Documents. + +pub mod identifier; + +pub use identifier::Identifier; diff --git a/wdl-grammar/src/subcommands/gauntlet/document/identifier.rs b/wdl-grammar/src/subcommands/gauntlet/document/identifier.rs new file mode 100644 index 000000000..eadbd1cf7 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/document/identifier.rs @@ -0,0 +1,109 @@ +//! Identifiers for documents. + +use serde::Deserialize; +use serde::Serialize; +use serde_with::serde_as; +use serde_with::DisplayFromStr; + +use crate::subcommands::gauntlet::repository; + +/// The character that separates the repository from the path in the identifier. +const SEPARATOR: char = ':'; + +/// A parse error related to an [`Identifier`]. +#[derive(Debug)] +pub enum ParseError { + /// Attempted to parse a [`Identifier`] from an invalid format. + InvalidFormat(String), + + /// An invalid repository identifier was provided. + RepositoryIdentifier(repository::identifier::Error), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::InvalidFormat(value) => write!(f, "invalid format: {value}"), + ParseError::RepositoryIdentifier(err) => { + write!(f, "repository identifier error: {err}") + } + } + } +} + +impl std::error::Error for ParseError {} + +/// An error related to an [`Identifier`]. +#[derive(Debug)] +pub enum Error { + /// A parse error. + Parse(ParseError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A document identifier. +#[serde_as] +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Identifier { + /// The repository identifier. + #[serde_as(as = "DisplayFromStr")] + repository: repository::Identifier, + + /// The path within the repository. + path: String, +} + +impl Identifier { + /// Creates a new [`Identifier`]. + pub fn new(repository: repository::Identifier, path: String) -> Self { + Self { repository, path } + } + + /// Gets the [`repository::Identifier`] from this [`Identifier`] by + /// reference. + pub fn repository(&self) -> &repository::Identifier { + &self.repository + } +} + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}{}", self.repository, SEPARATOR, self.path) + } +} + +impl std::str::FromStr for Identifier { + type Err = Error; + + fn from_str(s: &str) -> Result { + let parts = s.split(SEPARATOR).collect::>(); + + if parts.len() != 2 { + return Err(Error::Parse(ParseError::InvalidFormat(s.to_string()))); + } + + let mut parts = parts.into_iter(); + + // SAFETY: we just checked above that two elements exist, so this will + // always unwrap. + let repository = parts + .next() + .unwrap() + .to_string() + .parse::() + .map_err(|err| Error::Parse(ParseError::RepositoryIdentifier(err)))?; + + let path = parts.next().unwrap().to_string(); + + Ok(Self { repository, path }) + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/report.rs b/wdl-grammar/src/subcommands/gauntlet/report.rs new file mode 100644 index 000000000..8e3a6961c --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/report.rs @@ -0,0 +1,319 @@ +//! Reporting. + +use std::collections::HashMap; + +use indexmap::IndexMap; + +use colored::Colorize as _; + +use crate::gauntlet::document; +use crate::subcommands::gauntlet::repository; + +/// The status of a single parsing test. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum Status { + /// The item passed successfully with no warnings. + Success, + + /// The item was successful, but warnings were emitted. + Warning, + + /// The item failed, but the error message did not match the expected error + /// message. + Mismatch(String), + + /// The item failed when it was expected to succeed. + Error(String), + + /// The item was skipped because it failed, but that failure was explicitly + /// ignored. + Ignored(String), +} + +/// A printable section within a report for a repository. +#[derive(Debug, Eq, PartialEq)] +pub enum Section { + /// Title of the section. + Title, + + /// Summarized status of each parsing test. + Summary, + + /// Detailed information on unexpected errors that were encountered. + Errors, + + /// Summarized information about all results reported. + Footer, +} + +impl Status { + /// Gets whether the status is considered a successful parsing test. + /// + /// This is used to determine the numerator when calculating the percentage + /// of tests that passed for a repository. + pub fn success(&self) -> bool { + match self { + Status::Success => true, + Status::Warning => true, + Status::Mismatch(_) => false, + Status::Error(_) => false, + Status::Ignored(_) => false, + } + } + + /// Gets whether the status was considered at all. + /// + /// This is used to determine the denominator when calculating the + /// percentage of tests that passed for a repository. + pub fn considered(&self) -> bool { + match self { + Status::Success => true, + Status::Warning => true, + Status::Mismatch(_) => true, + Status::Error(_) => true, + Status::Ignored(_) => false, + } + } +} + +impl std::fmt::Display for Status { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Status::Success => write!(f, "✅"), + Status::Warning => write!(f, "⚠️"), + Status::Mismatch(_) => write!(f, "🔄️"), + Status::Error(_) => write!(f, "❌"), + Status::Ignored(_) => write!(f, "👀"), + } + } +} + +/// A mapping between [document identifiers](document::Identifier) and the +/// [status](Status) of their parsing test. +type Results = HashMap; + +/// A terminal-based report. +#[derive(Debug)] +pub struct Report { + /// The handle to which to write the report. + inner: T, + + /// The reporting section we are currently on. + section: Section, + + /// The [results of the testing](Results) of the parsing tests. + /// + /// **Note:** these are appended as we parse them, so they need to be + /// filtered by the repository you are currently considering. + results: Results, + + /// Whether or not _anything_ was printed for the current section. + /// + /// This informs us of whether we need to print a space between sections + /// when we transition to the next section. In other words, if we didn't + /// print anything in a section, we don't want to separate the first and + /// third sections by _two_ spaces. + printed: bool, +} + +impl Report { + /// Creates a new [`Report`] that points to a [writer](std::io::Write). + pub fn new(inner: T) -> Self { + Self { + inner, + section: Section::Title, + results: Default::default(), + printed: false, + } + } + + /// Gets the [`Results`] for this [`Report`] by reference. + /// + /// **Note:** see the note at [Self::results] for a caveat on interpretation + /// of these results. + pub fn results(&self) -> &Results { + &self.results + } + + /// Transitions to the next section of the report. + /// + /// **Note:** when a report rolls over to the next repository, the + /// [`Section::Footer`] simply transitions to a [`Section::Title`]. + pub fn next_section(&mut self) -> std::io::Result<()> { + self.section = match self.section { + Section::Title => Section::Summary, + Section::Summary => { + if self.results.is_empty() { + write!(self.inner, "⚠️ No items reported for this repository!")?; + } + + Section::Errors + } + Section::Errors => Section::Footer, + Section::Footer => Section::Title, + }; + + if self.printed && self.section != Section::Title { + writeln!(self.inner)?; + } + + self.printed = false; + + Ok(()) + } + + /// Prints the title for a repository report. + pub fn title(&mut self, name: impl std::fmt::Display) -> std::io::Result<()> { + if self.section != Section::Title { + panic!( + "cannot print a new title when report phase is {:?}", + self.section + ); + } + + let name = name.to_string(); + writeln!(self.inner, "{}", name.bold().underline())?; + self.printed = true; + + Ok(()) + } + + /// Registers and prints a single parse test result for a repository report. + pub fn register( + &mut self, + identifier: document::Identifier, + status: Status, + ) -> std::io::Result<()> { + if self.section != Section::Summary { + panic!( + "cannot register a status when the report phase is {:?}", + self.section + ); + } + + writeln!(self.inner, "{} {}", status, identifier)?; + self.results.insert(identifier, status); + self.printed = true; + + Ok(()) + } + + /// Reports all unexpected errors for a repository report. + pub fn report_unexpected_errors_for_repository( + &mut self, + repository_identifier: &repository::Identifier, + ) -> std::io::Result<()> { + if self.section != Section::Errors { + panic!( + "cannot report unexpected errors when the report phase is {:?}", + self.section + ); + } + + for (id, message) in self + .results + .iter() + .filter(|(id, _)| id.repository() == repository_identifier) + .filter_map(|(id, status)| match status { + Status::Error(msg) => Some((id, msg)), + _ => None, + }) + { + writeln!(self.inner, "{}\n\n{}\n", id.to_string().italic(), message)?; + self.printed = true; + } + + Ok(()) + } + + /// Prints the summary footer for a repository report. + pub fn footer( + &mut self, + repository_identifier: &repository::Identifier, + ) -> std::io::Result<()> { + if self.section != Section::Footer { + panic!( + "cannot report footer when the report phase is {:?}", + self.section + ); + } + + if self.results.is_empty() { + return Ok(()); + } + + let results = self + .results + .iter() + .filter(|(identifer, _)| identifer.repository() == repository_identifier) + .fold(IndexMap::::new(), |mut hm, (_, status)| { + *hm.entry(status.clone()).or_default() += 1; + hm + }); + + let passed = results.iter().filter(|(status, _)| status.success()).fold( + 0usize, + |mut acc, (_, count)| { + acc += count; + acc + }, + ); + + let considered = results + .iter() + .filter(|(status, _)| status.considered()) + .fold(0usize, |mut acc, (_, count)| { + acc += count; + acc + }); + + write!(self.inner, "Passed {}/{} tests", passed, considered)?; + + let mut with = Vec::new(); + + match results + .iter() + .flat_map(|(status, count)| match status { + Status::Ignored(_) => Some(*count), + _ => None, + }) + .sum::() + { + 0 => {} + 1 => with.push(String::from("1 ignored error")), + v => with.push(format!("{} ignored errors", v)), + }; + + match results + .iter() + .flat_map(|(status, count)| match status { + Status::Mismatch(_) => Some(*count), + _ => None, + }) + .sum::() + { + 0 => {} + 1 => with.push(String::from("1 mismatch error")), + v => with.push(format!("{} mismatched errors", v)), + }; + + match results.get(&Status::Warning).copied() { + Some(1) => with.push(String::from("1 error with warnings")), + Some(v) => with.push(format!("{} errors with warnings", v)), + None => {} + } + + if !with.is_empty() { + write!(self.inner, " (with {})", with.join(" and "))?; + } + + writeln!( + self.inner, + " ({:.1}%)", + (passed as f64 / considered as f64) * 100.0 + )?; + self.printed = true; + + Ok(()) + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository.rs b/wdl-grammar/src/subcommands/gauntlet/repository.rs new file mode 100644 index 000000000..f5b674053 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository.rs @@ -0,0 +1,340 @@ +//! A local repository of files from a remote GitHub repository. + +use async_recursion::async_recursion; +use chrono::Utc; +use indexmap::IndexMap; +use log::debug; +use log::error; +use log::info; + +use octocrab::etag::EntityTag; +use octocrab::models::repos::ContentItems; +use octocrab::Octocrab; +use reqwest::header::ETAG; +use reqwest::Client; + +pub mod builder; +pub mod cache; +pub mod identifier; +pub mod options; + +pub use builder::Builder; +pub use cache::Cache; +pub use identifier::Identifier; +pub use options::Options; + +/// The URL to ping when checking if GitHub has applied rate limiting. +const RATE_LIMIT_PING_URL: &str = "https://api.github.com"; + +/// The time to sleep between requests when checking if GitHub has applied rate +/// limiting. +const RATE_LIMIT_SLEEP_TIME: i64 = 60; + +/// The substring to look for in the response to detect whether GitHub has +/// applied rate limiting. +const RATE_LIMIT_EXCEEDED: &str = "API rate limit exceeded"; + +/// The HTTP response header indicating when rate limiting will be lifted by +/// GitHub. +const RATE_LIMIT_RESET_HEADER: &str = "X-RateLimit-Reset"; + +/// The user agent to set when sending HTTP requests. +const USER_AGENT: &str = "wdl-grammar gauntlet"; + +/// An error related to a [`Repository`]. +#[derive(Debug)] +pub enum Error { + /// An error related to the cache. + Cache(cache::Error), + + /// Missing an expected header. + MissingHeader(&'static str), + + /// An error related to [`octocrab`]. + Octocrab(octocrab::Error), + + /// An error from [`reqwest`]. + Reqwest(reqwest::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Cache(err) => write!(f, "cache error: {err}"), + Error::MissingHeader(header) => write!(f, "missing header: {header}"), + Error::Octocrab(err) => write!(f, "octocrab error: {err}"), + Error::Reqwest(err) => write!(f, "reqwest error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +pub type Result = std::result::Result; + +/// A repository of GitHub files. +#[derive(Debug)] +pub struct Repository { + /// The local cache of the repository files. + cache: Cache, + + /// The GitHub client. + client: Octocrab, + + /// The name for the [`Repository`] expressed as an [`Identifier`]. + identifier: Identifier, + + /// The options for operating the [`Repository`]. + options: Options, +} + +impl Repository { + /// Gets the cache from the [`Repository`] by reference. + #[allow(dead_code)] + pub fn cache(&self) -> &Cache { + &self.cache + } + + /// Gets the client from the [`Repository`] by reference. + #[allow(dead_code)] + pub fn client(&self) -> &Octocrab { + &self.client + } + + /// Gets the repository identifier from the [`Repository`] by reference. + #[allow(dead_code)] + pub fn identifier(&self) -> &Identifier { + &self.identifier + } + + /// Gets the options from the [`Repository`] by reference. + #[allow(dead_code)] + pub fn options(&self) -> &Options { + &self.options + } + + /// Hydrates the repository by contacting GitHub. + /// + /// This occurs by traversing the files in the repository, comparing the + /// `etag` HTTP response header to the one stored locally to see if any of + /// the files have changed, updating any files that have changed and, + /// finally, returning a map of those files and their contents. + /// + /// **Note:** only files with a `.wdl` extension are considered. + async fn hydrate_from_remote(&mut self) -> Result> { + info!("{}: hydrating from remote.", self.identifier); + + let content = get_remote_repo_content(&self.client, &self.identifier, None).await?; + + dive_for_wdl(self, content).await + } + + /// Hydrates the repository simply by looking at local files. + async fn hydrate_from_cache(&mut self) -> Result> { + info!("{}: hydrating from local cache.", self.identifier); + + let mut map = IndexMap::new(); + + for (path, _) in self.cache.registry().entries() { + // SAFETY: the first unwrap is safe because, since we are assuming a + // well-formed cache, this should always unwrap. + // + // The second unwrap is safe because we just checked that the path + // exists in the registry, so retreiving the value for that path + // will always unwrap. + let entry = self.cache.get(path).unwrap().unwrap(); + map.insert(path.clone(), entry.contents().to_string()); + } + + Ok(map) + } + + /// Hydrates the repository according to the [`Options`] set. + pub async fn hydrate(&mut self) -> Result> { + match self.options.hydrate_remote { + true => self.hydrate_from_remote().await, + false => self.hydrate_from_cache().await, + } + } +} + +/// Dives into a [`ContentItems`] to pull out any `.wdl` files. This function is +/// called recursively as directories are encountered within the repository. +#[async_recursion] +async fn dive_for_wdl( + repository: &mut Repository, + content: ContentItems, +) -> Result> { + let mut result = IndexMap::new(); + + for item in content.items { + if let Some(download_url) = item.download_url + && item.path.ends_with(".wdl") + { + let entry = match repository.cache.get(&item.path) { + Ok(entry) => entry, + Err(err) => return Err(Error::Cache(err)), + }; + + let etag = retrieve_etag(&download_url).await?; + + if let Some(entry) = entry { + // SAFETY: this should always unwrap, as we are getting the etag + // directly from the GitHub server. + if entry.etag() == &etag.parse::().unwrap() { + debug!("{}: etags match, using cached version.", item.path); + result.insert(item.path, entry.contents().to_owned()); + continue; + } else { + debug!( + "{}: etags don't match, overwriting with latest version.", + item.path + ); + } + } else { + debug!("{}: cache entry not found, downloading.", item.path); + } + + let response = reqwest::get(download_url).await.map_err(Error::Reqwest)?; + let contents = response.text().await.map_err(Error::Reqwest)?; + + repository + .cache + .insert(&item.path, etag, &contents) + .map_err(Error::Cache)?; + result.insert(item.path, contents); + } else if item.r#type == "dir" { + result.extend( + dive_for_wdl( + repository, + get_remote_repo_content( + &repository.client, + &repository.identifier, + Some(&item.path), + ) + .await?, + ) + .await?, + ) + } + } + + Ok(result) +} + +/// A function to pull out content for a particular path within a GitHub +/// repository. If the `path` is [`None`], the root of the repository is +/// searched. +/// +/// **Note:** rate limiting is handled at this level. +#[async_recursion] +async fn get_remote_repo_content<'a: 'async_recursion>( + client: &Octocrab, + identifier: &Identifier, + path: Option<&'a str>, +) -> Result { + debug!( + "{}: searching for files{}", + identifier, + path.map(|s| format!(" at path `{}`", s)) + .unwrap_or_default() + ); + + let binding = client.repos(identifier.organization(), identifier.name()); + let mut request = binding.get_content(); + + if let Some(path) = path { + request = request.path(path); + } + + match request.send().await { + Ok(result) => Ok(result), + Err(err) => match &err { + octocrab::Error::GitHub { source, .. } => { + if source.message.contains(RATE_LIMIT_EXCEEDED) { + wait_for_timeout().await?; + get_remote_repo_content(client, identifier, path).await + } else { + Err(Error::Octocrab(err)) + } + } + _ => Err(Error::Octocrab(err)), + }, + } +} + +/// A simple function to loop while we wait for rate-limiting applied by GitHub +/// to lift. +async fn wait_for_timeout() -> Result<()> { + let client = Client::builder().user_agent(USER_AGENT).build().unwrap(); + + let response = client + .head(RATE_LIMIT_PING_URL) + .send() + .await + .map_err(Error::Reqwest)?; + + let timestamp = response + .headers() + .get(RATE_LIMIT_RESET_HEADER) + .map(Ok) + .unwrap_or(Err(Error::MissingHeader(RATE_LIMIT_RESET_HEADER))) + .map(|s| s.to_str().unwrap().parse::().unwrap())?; + + let mut first_loop = true; + + loop { + let duration = timestamp + .checked_sub(Utc::now().timestamp()) + .unwrap_or_else(|| panic!("overflow when computing duration")); + + if duration == 0 || duration.is_negative() { + break; + } + + // SAFETY: this should always cast, as we just checked above if the + // duration was negative and broke out of the loop if so. + let sleep_for = std::cmp::min(duration, RATE_LIMIT_SLEEP_TIME) as u64; + + if first_loop { + error!( + "rate limit: rate limit activated, sleeping for {} seconds in {} second intervals.", + duration, RATE_LIMIT_SLEEP_TIME + ); + } + + error!( + "rate limit: sleeping for {} seconds ({} seconds remaining).", + sleep_for, duration + ); + + std::thread::sleep(std::time::Duration::from_secs(sleep_for)); + first_loop = false; + } + + Ok(()) +} + +/// A utility function to grab an `etag` HTTP response header from a URL. +async fn retrieve_etag(download_url: &str) -> Result { + let response = Client::builder() + .build() + .map_err(Error::Reqwest)? + .head(download_url) + .send() + .await + .map_err(Error::Reqwest)?; + + Ok(response + .headers() + .get(ETAG) + .map(Ok) + .unwrap_or(Err(Error::MissingHeader(ETAG.as_str())))? + .to_str() + // SAFETY: for GitHub URLs (which is the only thing this method is used + // to retrieve), the `etag` header will always be present, so this will + // always unwrap. + .unwrap() + .to_string()) +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/builder.rs b/wdl-grammar/src/subcommands/gauntlet/repository/builder.rs new file mode 100644 index 000000000..a06e8b619 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/builder.rs @@ -0,0 +1,128 @@ +//! A builder for a [`Repository`]. + +use std::path::Path; +use std::path::PathBuf; + +use log::debug; +use octocrab::Octocrab; + +use crate::subcommands::gauntlet::config::default_config_dir; +use crate::subcommands::gauntlet::repository::cache; +use crate::subcommands::gauntlet::repository::cache::Cache; +use crate::subcommands::gauntlet::repository::options; +use crate::subcommands::gauntlet::repository::Identifier; +use crate::subcommands::gauntlet::repository::Options; +use crate::subcommands::gauntlet::Repository; + +/// The environment variables within which a GitHub personal access token can be +/// stored. See (this +/// link)[https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens] +/// for more details. +const GITHUB_TOKEN_ENV: &[&str] = &["GITHUB_TOKEN", "GH_TOKEN"]; + +/// An error related to a [`Builder`]. +#[derive(Debug)] +pub enum Error { + /// An error related to the cache. + Cache(cache::Error), + + /// An repository identifier was never specified. + MissingRepositoryIdentifier, + + /// An error related to [`octocrab`]. + Octocrab(octocrab::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Cache(err) => write!(f, "cache error: {err}"), + Error::MissingRepositoryIdentifier => write!(f, "missing repository identifier"), + Error::Octocrab(err) => write!(f, "octocrab error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A repository of GitHub files. +#[derive(Debug, Default)] +pub struct Builder { + /// The root location where the GitHub files are cached locally. + root: Option, + + /// The name of the [`Repository`] expressed as an [`Identifier`]. + identifier: Option, + + /// The options for operating the [`Repository`]. + options: Option, +} + +impl Builder { + /// Sets the root path where the [`Repository`] will cache files locally. + pub fn root(mut self, path: impl AsRef) -> Self { + let path = path.as_ref().to_path_buf(); + self.root = Some(path); + self + } + + /// Sets the name of the [`Repository`] expressed as an [`Identifier`]. + pub fn identifier(mut self, identifier: Identifier) -> Self { + self.identifier = Some(identifier); + self + } + + + /// Sets the options by which the [`Repository`] will operate. + pub fn options(mut self, options: Options) -> Self { + self.options = Some(options); + self + } + + /// Consumes `self` and attempts to build a [`Repository`] + pub fn try_build(self) -> Result { + let token = GITHUB_TOKEN_ENV + .iter() + .filter_map(|var| match std::env::var(var) { + Ok(value) => Some(value), + Err(_) => None, + }) + .collect::>() + .pop(); + + let mut builder = Octocrab::builder(); + + if let Some(token) = token { + debug!("GitHub token detected."); + builder = builder.personal_token(token); + } + + let client = builder.build().map_err(Error::Octocrab)?; + + let identifier = match self.identifier { + Some(repository) => repository, + None => return Err(Error::MissingRepositoryIdentifier), + }; + + let root = self.root.unwrap_or_else(|| { + let mut path = default_config_dir(); + path.push(identifier.organization()); + path.push(identifier.name()); + path + }); + + let options = self.options.unwrap_or(options::Builder::default().build()); + + let cache = Cache::try_from(root.as_ref()).map_err(Error::Cache)?; + + Ok(Repository { + cache, + client, + identifier, + options, + }) + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/cache.rs b/wdl-grammar/src/subcommands/gauntlet/repository/cache.rs new file mode 100644 index 000000000..6d574ef08 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/cache.rs @@ -0,0 +1,146 @@ +//! A cache of local repository files. + +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; + +mod entry; +pub mod registry; + +pub use entry::Entry; +use log::debug; +pub use registry::Registry; + +/// An error related to a [`Cache`]. +#[derive(Debug)] +pub enum Error { + /// An input/output error. + InputOutput(std::io::Error), + + /// The provided path is missing. + MissingFile(PathBuf), + + /// The provided path is missing a parent. + MissingParent(PathBuf), + + /// A registry error + Registry(registry::Error), + + /// The root path is a file. + RootIsFile(PathBuf), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InputOutput(err) => write!(f, "i/o error: {err}"), + Error::MissingFile(path) => write!(f, "missing file: {}", path.display()), + Error::MissingParent(path) => write!(f, "missing parent: {}", path.display()), + Error::Registry(err) => write!(f, "registry error: {err}"), + Error::RootIsFile(root) => write!(f, "root is file: {}", root.display()), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +pub type Result = std::result::Result; + +/// A cache of local repository files. +#[derive(Debug)] +pub struct Cache { + /// The root directory. + root: PathBuf, + + /// The [registry file](Registry). + registry: Registry, +} + +impl Cache { + /// Gets a path within the [`Cache`]. + fn path(&self, path: impl AsRef) -> PathBuf { + let mut result = self.root.clone(); + result.push(path.as_ref()); + result + } + + /// Gets the [registry file](Registry) for this [`Cache`] by reference. + pub fn registry(&self) -> &Registry { + &self.registry + } + + /// Attempts to get an entry within the [`Cache`] if it exists. + pub fn get(&self, path: impl AsRef) -> Result> { + let path = path.as_ref(); + + let etag = match self.registry.entries().get(path) { + Some(etag) => etag.clone(), + None => return Ok(None), + }; + + let path = self.path(path); + + if !path.exists() { + return Err(Error::MissingFile(path)); + } + + let contents = std::fs::read_to_string(path).map_err(Error::InputOutput)?; + Ok(Some(Entry::new(etag, contents))) + } + + /// Inserts a file and its associated `etag` into the [`Cache`] at the + /// provided `path`. + pub fn insert( + &mut self, + path: impl AsRef, + etag: impl AsRef, + contents: impl AsRef, + ) -> Result<()> { + self.registry + .try_insert_etag(&path, etag) + .map_err(Error::Registry)?; + + let path = self.path(&path); + + match path.parent() { + Some(parent) => std::fs::create_dir_all(parent).map_err(Error::InputOutput)?, + None => return Err(Error::MissingParent(path.clone())), + }; + + let mut file = File::create(path).map_err(Error::InputOutput)?; + file.write_all(contents.as_ref().as_bytes()) + .map_err(Error::InputOutput)?; + + self.registry.save().map_err(Error::Registry)?; + + Ok(()) + } +} + +impl TryFrom<&Path> for Cache { + type Error = Error; + + fn try_from(root: &Path) -> std::result::Result { + let root = root.to_path_buf(); + + let mut registry_path = root.clone(); + registry_path.push("Registry.toml"); + let registry = Registry::try_from(registry_path).map_err(Error::Registry)?; + + if root.is_file() { + return Err(Error::RootIsFile(root)); + } + + debug!("Creating cache at {}", root.display()); + + if !root.exists() { + std::fs::create_dir_all(&root).map_err(Error::InputOutput)?; + } + + registry.save().map_err(Error::Registry)?; + + Ok(Self { root, registry }) + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/cache/entry.rs b/wdl-grammar/src/subcommands/gauntlet/repository/cache/entry.rs new file mode 100644 index 000000000..eab75c24c --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/cache/entry.rs @@ -0,0 +1,31 @@ +//! A single file entry within a [`Cache`](super::Cache). + +use octocrab::etag::EntityTag; + +/// A single file entry within a [`Cache`](super::Cache). +#[derive(Debug)] +pub struct Entry { + /// The cached contents of the file. + contents: String, + + /// The cached `etag` HTTP response header for the remote file. + etag: EntityTag, +} + +impl Entry { + /// Creates a new [`Entry`]. + pub fn new(etag: EntityTag, contents: String) -> Self { + Self { contents, etag } + } + + /// Gets the cached file contents of the [`Entry`] by reference. + pub fn contents(&self) -> &str { + self.contents.as_str() + } + + /// Gets the cached [`etag` HTTP response header](EntityTag) of the + /// [`Entry`] by reference. + pub fn etag(&self) -> &EntityTag { + &self.etag + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/cache/registry.rs b/wdl-grammar/src/subcommands/gauntlet/repository/cache/registry.rs new file mode 100644 index 000000000..4450528d1 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/cache/registry.rs @@ -0,0 +1,219 @@ +//! A file that registers the remote files cached locally and their `etag`s. + +use std::fs::File; + +use std::io::Write; +use std::path::Path; +use std::path::PathBuf; + +use indexmap::IndexMap; +use octocrab::etag::EntityTag; + +use toml::map::Map; +use toml::Value; + +/// A parse error related to a [`Registry`]. +#[derive(Debug)] +pub enum ParseError { + /// An error parsing an entity tag. + EntityTag(String), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::EntityTag(reason) => write!(f, "entity tag: {reason}"), + } + } +} + +impl std::error::Error for ParseError {} + +/// An error related to a [`Registry`]. +#[derive(Debug)] +pub enum Error { + /// An input/output error. + InputOutput(std::io::Error), + + /// The provided path is missing a parent. + MissingParent(PathBuf), + + /// A parse error. + Parse(ParseError), + + /// Attempted to save the results to a file, but this [`Registry`] is an + /// in-memory registry. + SaveOnUnbackedRegistry, + + /// A TOML deserialization error. + TomlDeserialization(toml::de::Error), + + /// A TOML serialization error. + TomlSerialization(toml::ser::Error), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::InputOutput(err) => write!(f, "i/o error: {err}"), + Error::MissingParent(path) => write!(f, "missing parent: {}", path.display()), + Error::Parse(err) => write!(f, "parse error: {err}"), + Error::SaveOnUnbackedRegistry => write!(f, "cannot save an in-memory registry to file"), + Error::TomlDeserialization(err) => write!(f, "toml deserialization error: {err}"), + Error::TomlSerialization(err) => write!(f, "toml serialization error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// A file-backed registry file containing a mapping from paths to `etag`s. +/// +/// This is useful when creating a cache, as it will store the retrieved etag +/// values for all of the files we store in the cache. We can then use this +/// registry to determine whether a file needs to be redownloaded. +#[derive(Debug, Default)] +pub struct Registry { + /// The path to the registry file. + path: Option, + + /// The mapping of paths to [`etag`](EntityTag)s. + entries: IndexMap, +} + +impl Registry { + /// Gets a reference to the inner entries. + pub fn entries(&self) -> &IndexMap { + &self.entries + } + + /// Attempts to insert an entity tag into the [`Registry`]. + pub fn try_insert_etag( + &mut self, + path: impl AsRef, + value: impl AsRef, + ) -> Result> { + let etag = value + .as_ref() + .parse() + .map_err(|reason| Error::Parse(ParseError::EntityTag(reason)))?; + Ok(self.entries.insert(path.as_ref().to_string(), etag)) + } + + /// Saves a [`Registry`] to its backed file. + pub fn save(&self) -> Result<()> { + let path = self + .path + .as_ref() + .map(Ok) + .unwrap_or(Err(Error::SaveOnUnbackedRegistry))?; + + let map = self + .entries + .iter() + .map(|(key, value)| (key.to_owned(), toml::Value::String(value.to_string()))) + .collect::>(); + + let contents = toml::to_string_pretty(&map).map_err(Error::TomlSerialization)?; + + let mut file = File::create(path).map_err(Error::InputOutput)?; + file.write_all(contents.as_bytes()) + .map_err(Error::InputOutput) + } +} + +impl TryFrom for Registry { + type Error = Error; + + fn try_from(path: PathBuf) -> Result { + match path.exists() { + true => { + let contents = std::fs::read_to_string(&path).map_err(Error::InputOutput)?; + let entries = entries_from_string(&contents)?; + + Ok(Self { + path: Some(path), + entries, + }) + } + false => { + let parent = path + .parent() + .map(Ok) + .unwrap_or(Err(Error::MissingParent(path.clone())))?; + std::fs::create_dir_all(parent).map_err(Error::InputOutput)?; + + Ok(Self { + path: Some(path), + entries: Default::default(), + }) + } + } + } +} + +impl TryFrom<&Path> for Registry { + type Error = Error; + + fn try_from(path: &Path) -> Result { + let path = path.to_path_buf(); + Self::try_from(path) + } +} + +/// Pulls a list of entries from the contents of a TOML file and returns them +/// within an [`IndexMap`]. This is used when loading a [`Registry`] from an +/// existing file. +fn entries_from_string(contents: &str) -> Result> { + contents + .parse::() + .map_err(Error::TomlDeserialization)? + .into_iter() + .map(|(key, value)| { + match value { + Value::String(value) => { + let value = value + .parse::() + .map_err(|reason| Error::Parse(ParseError::EntityTag(reason)))?; + Ok((key, value)) + } + // SAFETY: none of these other value types will be + // created by this code. As such, if any other type is + // encountered, then that must be because of human + // intervention in the file. + _ => unreachable!(), + } + }) + .collect::>>() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_builds_from_a_toml_string_correctly() -> Result<()> { + let entries = entries_from_string(r#"hello = "\"W/abcd1234\"""#)?; + + assert_eq!(entries.len(), 1); + assert_eq!( + entries.get("hello").unwrap().to_string(), + String::from(r#""W/abcd1234""#) + ); + + Ok(()) + } + + #[test] + #[should_panic] + fn it_fails_when_an_unexpected_element_exists_in_the_map() { + entries_from_string( + r#"[section] + hello = "\"W/abcd1234\"""#, + ) + .unwrap(); + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/identifier.rs b/wdl-grammar/src/subcommands/gauntlet/repository/identifier.rs new file mode 100644 index 000000000..b2e285a71 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/identifier.rs @@ -0,0 +1,90 @@ +//! Identifiers for repositories. + +use serde::Deserialize; +use serde::Serialize; + +/// The character that separates the organization from the repository name. +const SEPARATOR: char = '/'; + +/// A parse error related to an [`Identifier`]. +#[derive(Debug)] +pub enum ParseError { + /// Attempted to parse a [`Identifier`] from an invalid format. + InvalidFormat(String), +} + +impl std::fmt::Display for ParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ParseError::InvalidFormat(value) => write!(f, "invalid format: {value}"), + } + } +} + +impl std::error::Error for ParseError {} + +/// An error related to an [`Identifier`]. +#[derive(Debug)] +pub enum Error { + /// A parse error. + Parse(ParseError), +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::Parse(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A repository identifier. +#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] +pub struct Identifier { + /// The organization of the repository identifier. + organization: String, + + /// The name of the repository identifier. + name: String, +} + +impl Identifier { + /// Gets the repository name of this [`Identifier`] by reference. + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// Gets the organization name of this [`Identifier`] by reference. + pub fn organization(&self) -> &str { + self.organization.as_str() + } +} + +impl std::fmt::Display for Identifier { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}{}{}", self.organization, SEPARATOR, self.name) + } +} + +impl std::str::FromStr for Identifier { + type Err = Error; + + fn from_str(s: &str) -> Result { + let parts = s.split(SEPARATOR).collect::>(); + + if parts.len() != 2 { + return Err(Error::Parse(ParseError::InvalidFormat(s.to_string()))); + } + + let mut parts = parts.into_iter(); + + // SAFETY: we just checked above that two elements exist, so this will + // always unwrap. + let organization = parts.next().unwrap().to_string(); + let name = parts.next().unwrap().to_string(); + + Ok(Self { organization, name }) + } +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/options.rs b/wdl-grammar/src/subcommands/gauntlet/repository/options.rs new file mode 100644 index 000000000..b92172f11 --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/options.rs @@ -0,0 +1,12 @@ +//! Options for operating a [`Repository`](super::Repository). + +mod builder; + +pub use builder::Builder; + +/// Options for operating a [`Repository`](super::Repository). +#[derive(Debug)] +pub struct Options { + /// Whether or not to hydrate a repository from its remote files. + pub hydrate_remote: bool, +} diff --git a/wdl-grammar/src/subcommands/gauntlet/repository/options/builder.rs b/wdl-grammar/src/subcommands/gauntlet/repository/options/builder.rs new file mode 100644 index 000000000..a516e78cb --- /dev/null +++ b/wdl-grammar/src/subcommands/gauntlet/repository/options/builder.rs @@ -0,0 +1,36 @@ +//! A builder for an [`Options`]. + +use crate::subcommands::gauntlet::repository::Options; + +/// A builder for an [`Options`]. +#[derive(Debug)] +pub struct Builder { + /// Whether or not to hydrate a repository from its remote files. + hydrate_remote: bool, +} + +impl Builder { + /// Sets whether or not the + /// [`Repository`](crate::subcommands::gauntlet::Repository) will hydrate + /// itself from remote sources (or, in contrast, if it will rely purely on + /// the local files it already has cached). + pub fn hydrate_remote(mut self, value: bool) -> Self { + self.hydrate_remote = value; + self + } + + /// Consumes `self` to create a new [`Options`]. + pub fn build(self) -> Options { + Options { + hydrate_remote: self.hydrate_remote, + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self { + hydrate_remote: true, + } + } +} diff --git a/wdl-grammar/src/subcommands/parse.rs b/wdl-grammar/src/subcommands/parse.rs new file mode 100644 index 000000000..d2dced9ed --- /dev/null +++ b/wdl-grammar/src/subcommands/parse.rs @@ -0,0 +1,110 @@ +//! `wdl-grammar parse` + +use clap::Parser; +use pest::Parser as _; + +use wdl_grammar as grammar; + +use crate::subcommands::get_contents_stdin; + +/// An error related to the `wdl-grammar parse` subcommand. +#[derive(Debug)] +pub enum Error { + /// An empty parse tree was encountered. + ChildrenOnlyWithEmptyParseTree, + + /// A common error. + Common(super::Error), + + /// A parsing error from Pest. + Parse(Box), + + /// Unknown rule name. + UnknownRule { + /// The name of the rule. + name: String, + + /// The grammar being used. + grammar: grammar::Version, + }, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::ChildrenOnlyWithEmptyParseTree => { + write!(f, "cannot print children with empty parse tree") + } + Error::Common(err) => write!(f, "{err}"), + Error::UnknownRule { name, grammar } => { + write!(f, "unknown rule '{name}' for grammar {grammar}") + } + Error::Parse(err) => write!(f, "parse error: {err}"), + } + } +} + +impl std::error::Error for Error {} + +/// A [`Result`](std::result::Result) with an [`Error`]. +type Result = std::result::Result; + +/// Arguments for the `wdl-grammar parse` subcommand. +#[derive(Debug, Parser)] +pub struct Args { + /// The input to parse. + #[clap(value_name = "INPUT")] + input: Option, + + /// The Workflow Description Language (WDL) specification version to use. + #[arg(value_name = "VERSION", short = 's', long, default_value_t, value_enum)] + specification_version: grammar::Version, + + /// The parser rule to evaluate. + #[arg(value_name = "RULE", short = 'r', long, default_value = "document")] + rule: String, + + /// Skips the parent element and prints each child. + #[arg(short, long, global = true)] + children_only: bool, +} + +/// Main function for this subcommand. +pub fn parse(args: Args) -> Result<()> { + let rule = match args.specification_version { + grammar::Version::V1 => grammar::v1::get_rule(&args.rule) + .map(Ok) + .unwrap_or_else(|| { + Err(Error::UnknownRule { + name: args.rule.clone(), + grammar: args.specification_version.clone(), + }) + })?, + }; + + let input = args + .input + .map(Ok) + .unwrap_or_else(|| get_contents_stdin().map_err(Error::Common))?; + + let mut parse_tree = match args.specification_version { + grammar::Version::V1 => { + grammar::v1::Parser::parse(rule, &input).map_err(|err| Error::Parse(Box::new(err)))? + } + }; + + if args.children_only { + let children = match parse_tree.next() { + Some(root) => root.into_inner(), + None => return Err(Error::ChildrenOnlyWithEmptyParseTree), + }; + + for child in children { + dbg!(child); + } + } else { + dbg!(parse_tree); + }; + + Ok(()) +} diff --git a/wdl-grammar/src/v1/tests/atoms.rs b/wdl-grammar/src/v1/tests/atoms.rs index 21dda2b1e..98a300cab 100644 --- a/wdl-grammar/src/v1/tests/atoms.rs +++ b/wdl-grammar/src/v1/tests/atoms.rs @@ -1,3 +1,2 @@ mod one_or_more; mod option; -mod zero_or_more; diff --git a/wdl-grammar/src/v1/tests/atoms/zero_or_more.rs b/wdl-grammar/src/v1/tests/atoms/zero_or_more.rs deleted file mode 100644 index 9679561d2..000000000 --- a/wdl-grammar/src/v1/tests/atoms/zero_or_more.rs +++ /dev/null @@ -1,28 +0,0 @@ -use pest::consumes_to; -use pest::fails_with; -use pest::parses_to; - -use crate::v1::Parser as WdlParser; -use crate::v1::Rule; - -#[test] -fn it_fails_to_parse_an_empty_zero_or_more() { - fails_with! { - parser: WdlParser, - input: "", - rule: Rule::ZERO_OR_MORE, - positives: vec![Rule::ZERO_OR_MORE], - negatives: vec![], - pos: 0 - } -} - -#[test] -fn it_successfully_parses_zero_or_more() { - parses_to! { - parser: WdlParser, - input: "*", - rule: Rule::ZERO_OR_MORE, - tokens: [ZERO_OR_MORE(0, 1)] - } -} diff --git a/wdl-grammar/src/v1/tests/expression/suffix/apply.rs b/wdl-grammar/src/v1/tests/expression/suffix/apply.rs index 006776a60..d7ada85af 100644 --- a/wdl-grammar/src/v1/tests/expression/suffix/apply.rs +++ b/wdl-grammar/src/v1/tests/expression/suffix/apply.rs @@ -17,18 +17,6 @@ fn it_fails_to_parse_an_empty_apply() { } } -#[test] -fn it_fails_to_parse_an_apply_with_no_elements() { - fails_with! { - parser: WdlParser, - input: "()", - rule: Rule::apply, - positives: vec![Rule::WHITESPACE, Rule::COMMENT, Rule::expression], - negatives: vec![], - pos: 1 - } -} - #[test] fn it_fails_to_parse_an_apply_with_just_a_comma() { fails_with! { @@ -53,6 +41,16 @@ fn it_fails_to_parse_a_value_that_is_not_apply() { } } +#[test] +fn it_successfully_parses_an_apply_with_no_elements() { + parses_to! { + parser: WdlParser, + input: "()", + rule: Rule::apply, + tokens: [apply(0, 2)] + } +} + #[test] fn it_successfully_parses_an_apply_with_one_element() { parses_to! { diff --git a/wdl-grammar/src/v1/tests/primitives/char.rs b/wdl-grammar/src/v1/tests/primitives/char.rs index 1347f8617..ad7c7841b 100644 --- a/wdl-grammar/src/v1/tests/primitives/char.rs +++ b/wdl-grammar/src/v1/tests/primitives/char.rs @@ -17,10 +17,11 @@ fn it_fails_to_parse_an_empty_char_special() { input: "", rule: Rule::char_special, positives: vec![ + Rule::char_escaped_invalid, Rule::char_escaped, Rule::char_octal, Rule::char_hex, - Rule::char_unicode + Rule::char_unicode, ], negatives: vec![], pos: 0 diff --git a/wdl-grammar/src/v1/tests/primitives/string.rs b/wdl-grammar/src/v1/tests/primitives/string.rs index ce7a02b2a..8c9485331 100644 --- a/wdl-grammar/src/v1/tests/primitives/string.rs +++ b/wdl-grammar/src/v1/tests/primitives/string.rs @@ -38,9 +38,9 @@ fn it_fails_to_parse_a_single_double_quote() { parser: WdlParser, input: "\"", rule: Rule::string, - positives: vec![Rule::string], + positives: vec![Rule::char_escaped], negatives: vec![], - pos: 0 + pos: 1 } } @@ -50,9 +50,9 @@ fn it_fails_to_parse_a_single_single_quote() { parser: WdlParser, input: "\'", rule: Rule::string, - positives: vec![Rule::string], + positives: vec![Rule::char_escaped], negatives: vec![], - pos: 0 + pos: 1 } } diff --git a/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs b/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs index a0c3c4f9a..23e3bcfbb 100644 --- a/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs +++ b/wdl-grammar/src/v1/tests/primitives/string/double_quoted_string.rs @@ -36,10 +36,10 @@ fn it_fails_to_parse_a_single_double_quote() { input: "\"", rule: Rule::double_quoted_string, positives: vec![ - Rule::double_quoted_string + Rule::char_escaped ], negatives: vec![], - pos: 0 + pos: 1 } } @@ -50,10 +50,10 @@ fn it_fails_to_parse_a_string_with_a_newline() { input: "\"Hello,\nworld!\"", rule: Rule::double_quoted_string, positives: vec![ - Rule::double_quoted_string + Rule::char_escaped ], negatives: vec![], - pos: 0 + pos: 7 } } diff --git a/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs b/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs index ccbb5fe40..8eec59509 100644 --- a/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs +++ b/wdl-grammar/src/v1/tests/primitives/string/single_quoted_string.rs @@ -36,10 +36,10 @@ fn it_fails_to_parse_a_single_double_quote() { input: "\'", rule: Rule::single_quoted_string, positives: vec![ - Rule::single_quoted_string + Rule::char_escaped ], negatives: vec![], - pos: 0 + pos: 1 } } @@ -50,10 +50,10 @@ fn it_fails_to_parse_a_string_with_a_newline() { input: "'Hello,\nworld!'", rule: Rule::single_quoted_string, positives: vec![ - Rule::single_quoted_string + Rule::char_escaped ], negatives: vec![], - pos: 0 + pos: 7 } } diff --git a/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs b/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs index 10a1f06cf..4a4c443ae 100644 --- a/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs +++ b/wdl-grammar/src/v1/tests/workflow_elements/conditional.rs @@ -35,23 +35,40 @@ fn it_successfully_parses_conditional_without_space() { parser: WdlParser, input: "if(true){Int a=10}", rule: Rule::workflow_conditional, - tokens: [workflow_conditional(0, 18, [ + tokens: [ + // `if(true){Int a=10}` + workflow_conditional(0, 18, [ + // `true` expression(3, 7, [ - boolean(3, 7) + // `true` + boolean(3, 7), ]), + // `Int a=10` workflow_execution_statement(9, 17, [ - workflow_private_declarations(9, 17, [ - bound_declaration(9, 17, [ - int_type(9, 12), - WHITESPACE(12, 13, [SPACE(12, 13)]), - identifier(13, 14), - expression(15, 17, [ - integer(15, 17, [ - integer_decimal(15, 17) - ]) - ]) - ]) + // `Int a=10` + workflow_private_declarations(9, 17, [ + // `Int a=10` + bound_declaration(9, 17, [ + // `Int` + wdl_type(9, 12, [ + // `Int` + int_type(9, 12), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + // `a` + identifier(13, 14), + // `10` + expression(15, 17, [ + // `10` + integer(15, 17, [ + // `10` + integer_decimal(15, 17), + ]), + ]), ]), + ]), ]), ])] } @@ -77,25 +94,43 @@ fn it_successfully_excludes_trailing_whitespace() { parser: WdlParser, input: "if(true){Int a=10} ", rule: Rule::workflow_conditional, - tokens: [workflow_conditional(0, 18, [ + tokens: [ + // `if(true){Int a=10}` + workflow_conditional(0, 18, [ + // `true` expression(3, 7, [ - boolean(3, 7) + // `true` + boolean(3, 7), ]), + // `Int a=10` workflow_execution_statement(9, 17, [ - workflow_private_declarations(9, 17, [ - bound_declaration(9, 17, [ - int_type(9, 12), - WHITESPACE(12, 13, [SPACE(12, 13)]), - identifier(13, 14), - expression(15, 17, [ - integer(15, 17, [ - integer_decimal(15, 17) - ]) - ]) - ]) + // `Int a=10` + workflow_private_declarations(9, 17, [ + // `Int a=10` + bound_declaration(9, 17, [ + // `Int` + wdl_type(9, 12, [ + // `Int` + int_type(9, 12), + ]), + WHITESPACE(12, 13, [ + SPACE(12, 13), + ]), + // `a` + identifier(13, 14), + // `10` + expression(15, 17, [ + // `10` + integer(15, 17, [ + // `10` + integer_decimal(15, 17), + ]), + ]), ]), + ]), ]), - ])] + ]) + ] } } @@ -105,29 +140,58 @@ fn it_successfully_parses_conditional_with_space() { parser: WdlParser, input: "if ( true ) { Int a=10 }", rule: Rule::workflow_conditional, - tokens: [workflow_conditional(0, 24, [ - WHITESPACE(2, 3, [SPACE(2, 3)]), - WHITESPACE(4, 5, [SPACE(4, 5)]), + tokens: [ + // `if ( true ) { Int a=10 }` + workflow_conditional(0, 24, [ + WHITESPACE(2, 3, [ + SPACE(2, 3), + ]), + WHITESPACE(4, 5, [ + SPACE(4, 5), + ]), + // `true` expression(5, 9, [ - boolean(5, 9) + // `true` + boolean(5, 9), + ]), + WHITESPACE(9, 10, [ + SPACE(9, 10), + ]), + WHITESPACE(11, 12, [ + SPACE(11, 12), + ]), + WHITESPACE(13, 14, [ + SPACE(13, 14), ]), - WHITESPACE(9, 10, [SPACE(9, 10)]), - WHITESPACE(11, 12, [SPACE(11, 12)]), - WHITESPACE(13, 14, [SPACE(13, 14)]), + // `Int a=10` workflow_execution_statement(14, 23, [ - workflow_private_declarations(14, 23, [ - bound_declaration(14, 22, [ - int_type(14, 17), - WHITESPACE(17, 18, [SPACE(17, 18)]), - identifier(18, 19), - expression(20, 22, [ - integer(20, 22, [ - integer_decimal(20, 22) - ]) - ]) + // `Int a=10` + workflow_private_declarations(14, 23, [ + // `Int a=10` + bound_declaration(14, 22, [ + // `Int` + wdl_type(14, 17, [ + // `Int` + int_type(14, 17), + ]), + WHITESPACE(17, 18, [ + SPACE(17, 18), + ]), + // `a` + identifier(18, 19), + // `10` + expression(20, 22, [ + // `10` + integer(20, 22, [ + // `10` + integer_decimal(20, 22), ]), - WHITESPACE(22, 23, [SPACE(22, 23)]), + ]), ]), + WHITESPACE(22, 23, [ + SPACE(22, 23), + ]), + ]), ]), ])] } @@ -138,18 +202,20 @@ fn it_successfully_parses_conditional_with_multiple_statements() { parses_to! { parser: WdlParser, input: "if(true){ - Int a=10 - call my_task{input:foo=a} - call no_inputs{} - }", + Int a=10 + call my_task{input:foo=a} + call no_inputs{} +}", rule: Rule::workflow_conditional, - tokens: [workflow_conditional(0, 107, [ + tokens: [ + // `if(true){ Int a=10 call my_task{input:foo=a} call no_inputs{} }` + workflow_conditional(0, 75, [ + // `true` expression(3, 7, [ + // `true` boolean(3, 7), ]), - // `` WHITESPACE(9, 10, [ - // `` NEWLINE(9, 10), ]), WHITESPACE(10, 11, [ @@ -164,181 +230,102 @@ fn it_successfully_parses_conditional_with_multiple_statements() { WHITESPACE(13, 14, [ SPACE(13, 14), ]), - WHITESPACE(14, 15, [ - SPACE(14, 15), - ]), - WHITESPACE(15, 16, [ - SPACE(15, 16), - ]), - WHITESPACE(16, 17, [ - SPACE(16, 17), - ]), - WHITESPACE(17, 18, [ - SPACE(17, 18), - ]), - WHITESPACE(18, 19, [ - SPACE(18, 19), - ]), - WHITESPACE(19, 20, [ - SPACE(19, 20), - ]), - WHITESPACE(20, 21, [ - SPACE(20, 21), - ]), - WHITESPACE(21, 22, [ - SPACE(21, 22), - ]), - workflow_execution_statement(22, 43, [ - workflow_private_declarations(22, 43, [ - bound_declaration(22, 30, [ - int_type(22, 25), - WHITESPACE(25, 26, [ - SPACE(25, 26), + // `Int a=10` + workflow_execution_statement(14, 27, [ + // `Int a=10` + workflow_private_declarations(14, 27, [ + // `Int a=10` + bound_declaration(14, 22, [ + // `Int` + wdl_type(14, 17, [ + // `Int` + int_type(14, 17), + ]), + WHITESPACE(17, 18, [ + SPACE(17, 18), ]), - identifier(26, 27), - expression(28, 30, [ - integer(28, 30, [ - integer_decimal(28, 30), + // `a` + identifier(18, 19), + // `10` + expression(20, 22, [ + // `10` + integer(20, 22, [ + // `10` + integer_decimal(20, 22), ]), ]), ]), - // `` - WHITESPACE(30, 31, [ - // `` - NEWLINE(30, 31), - ]), - WHITESPACE(31, 32, [ - SPACE(31, 32), - ]), - WHITESPACE(32, 33, [ - SPACE(32, 33), + WHITESPACE(22, 23, [ + NEWLINE(22, 23), ]), - WHITESPACE(33, 34, [ - SPACE(33, 34), + WHITESPACE(23, 24, [ + SPACE(23, 24), ]), - WHITESPACE(34, 35, [ - SPACE(34, 35), + WHITESPACE(24, 25, [ + SPACE(24, 25), ]), - WHITESPACE(35, 36, [ - SPACE(35, 36), + WHITESPACE(25, 26, [ + SPACE(25, 26), ]), - WHITESPACE(36, 37, [ - SPACE(36, 37), - ]), - WHITESPACE(37, 38, [ - SPACE(37, 38), - ]), - WHITESPACE(38, 39, [ - SPACE(38, 39), - ]), - WHITESPACE(39, 40, [ - SPACE(39, 40), - ]), - WHITESPACE(40, 41, [ - SPACE(40, 41), - ]), - WHITESPACE(41, 42, [ - SPACE(41, 42), - ]), - WHITESPACE(42, 43, [ - SPACE(42, 43), + WHITESPACE(26, 27, [ + SPACE(26, 27), ]), ]), ]), - workflow_execution_statement(43, 68, [ - workflow_call(43, 68, [ - WHITESPACE(47, 48, [ - SPACE(47, 48), + // `call my_task{input:foo=a}` + workflow_execution_statement(27, 52, [ + // `call my_task{input:foo=a}` + workflow_call(27, 52, [ + WHITESPACE(31, 32, [ + SPACE(31, 32), ]), - identifier(48, 55), - workflow_call_body(55, 68, [ - workflow_call_input(62, 67, [ - identifier(62, 65), - expression(66, 67, [ - identifier(66, 67), + // `my_task` + identifier(32, 39), + // `{input:foo=a}` + workflow_call_body(39, 52, [ + // `foo=a` + workflow_call_input(46, 51, [ + // `foo` + identifier(46, 49), + // `a` + expression(50, 51, [ + // `a` + identifier(50, 51), ]), ]), ]), ]), ]), - // `` - WHITESPACE(68, 69, [ - // `` - NEWLINE(68, 69), + WHITESPACE(52, 53, [ + NEWLINE(52, 53), ]), - WHITESPACE(69, 70, [ - SPACE(69, 70), + WHITESPACE(53, 54, [ + SPACE(53, 54), ]), - WHITESPACE(70, 71, [ - SPACE(70, 71), + WHITESPACE(54, 55, [ + SPACE(54, 55), ]), - WHITESPACE(71, 72, [ - SPACE(71, 72), + WHITESPACE(55, 56, [ + SPACE(55, 56), ]), - WHITESPACE(72, 73, [ - SPACE(72, 73), + WHITESPACE(56, 57, [ + SPACE(56, 57), ]), - WHITESPACE(73, 74, [ - SPACE(73, 74), - ]), - WHITESPACE(74, 75, [ - SPACE(74, 75), - ]), - WHITESPACE(75, 76, [ - SPACE(75, 76), - ]), - WHITESPACE(76, 77, [ - SPACE(76, 77), - ]), - WHITESPACE(77, 78, [ - SPACE(77, 78), - ]), - WHITESPACE(78, 79, [ - SPACE(78, 79), - ]), - WHITESPACE(79, 80, [ - SPACE(79, 80), - ]), - WHITESPACE(80, 81, [ - SPACE(80, 81), - ]), - workflow_execution_statement(81, 97, [ - workflow_call(81, 97, [ - WHITESPACE(85, 86, [ - SPACE(85, 86), + // `call no_inputs{}` + workflow_execution_statement(57, 73, [ + // `call no_inputs{}` + workflow_call(57, 73, [ + WHITESPACE(61, 62, [ + SPACE(61, 62), ]), - identifier(86, 95), - workflow_call_body(95, 97), + // `no_inputs` + identifier(62, 71), + // `{}` + workflow_call_body(71, 73), ]), ]), - // `` - WHITESPACE(97, 98, [ - // `` - NEWLINE(97, 98), - ]), - WHITESPACE(98, 99, [ - SPACE(98, 99), - ]), - WHITESPACE(99, 100, [ - SPACE(99, 100), - ]), - WHITESPACE(100, 101, [ - SPACE(100, 101), - ]), - WHITESPACE(101, 102, [ - SPACE(101, 102), - ]), - WHITESPACE(102, 103, [ - SPACE(102, 103), - ]), - WHITESPACE(103, 104, [ - SPACE(103, 104), - ]), - WHITESPACE(104, 105, [ - SPACE(104, 105), - ]), - WHITESPACE(105, 106, [ - SPACE(105, 106), + WHITESPACE(73, 74, [ + NEWLINE(73, 74), ]), ]) ] diff --git a/wdl-grammar/src/v1/wdl.pest b/wdl-grammar/src/v1/wdl.pest index a39911f44..3c1020e6f 100644 --- a/wdl-grammar/src/v1/wdl.pest +++ b/wdl-grammar/src/v1/wdl.pest @@ -1,41 +1,40 @@ -//============// +// ============// // Whitespace // -//============// +// ============// // Pest provides relatively good support for whitespace out of the box. However, // we decided that we are our parse tree to include details on the specific // tokens that are used—to differentiate between spaces, tabs, newlines, // carriage return-newlines, and then carriage returns. In this way, we can // examine the parse tree when writing our linter. -SPACE = { " " } -TAB = { "\t" } +SPACE = { " " } +TAB = { "\t" } INDENT = _{ SPACE | TAB } -NEWLINE = { "\n" } -CARRIAGE_RETURN_NEWLINE = { "\r\n" } -CARRIAGE_RETURN = { "\r" } -LINE_ENDING = _{ NEWLINE | CARRIAGE_RETURN_NEWLINE | CARRIAGE_RETURN } +NEWLINE = { "\n" } +CARRIAGE_RETURN_NEWLINE = { "\r\n" } +CARRIAGE_RETURN = { "\r" } +LINE_ENDING = _{ NEWLINE | CARRIAGE_RETURN_NEWLINE | CARRIAGE_RETURN } WHITESPACE = ${ LINE_ENDING | INDENT } -//==========// +// ==========// // Comments // -//==========// +// ==========// -COMMENT = { "#" ~ (!LINE_ENDING ~ ANY)* } +COMMENT = { "#" ~ (!LINE_ENDING ~ ANY)* } -//=======// +// =======// // Atoms // -//=======// +// =======// -OPTION = { "?" } -ZERO_OR_MORE = { "*" } -ONE_OR_MORE = { "+" } -COMMA = { "," } +OPTION = { "?" } +ONE_OR_MORE = { "+" } +COMMA = { "," } -//==========// +// ==========// // Literals // -//==========// +// ==========// // None. none = { "None" } @@ -64,18 +63,28 @@ float = ${ float_with_decimal | float_without_decimal | float_si number = _{ float | integer } // Character. -// +// // DIVERGE: the specification states lists a tab (`\t`) within the table of // characters that it states *must* be escaped when included in a string. This // seems non-sensical to me, and I see no other implementations that enforce // this check. Thus, I am not including a tab in that list. -char_escaped = @{ "\\" ~ ("\\" | "\"" | "\'" | "n" | "r" | "b" | "t" | "f" | "a" | "v" | "?") } + +// LENIENT: the `char_escaped_invalid` rule (and its inclusion in the +// `char_escaped` rule) are not to spec. However, we would like to support +// parsing of these invalid string characters so that we can return lint errors +// for them (rather than fail parsing, which returns a relatively unhelpful +// error message at the time of writing). +// +// If you wish to remove this leniency, you can remove the +// `char_escaped_invalid` rule and its inclusion in the `char_escaped` rule. +char_escaped_invalid = @{ "\\" ~ ANY } +char_escaped = ${ "\\" ~ ("\\" | "\"" | "\'" | "n" | "r" | "b" | "t" | "f" | "a" | "v" | "?") } char_octal = @{ "\\" ~ ASCII_OCT_DIGIT{1, 3} ~ !ASCII_OCT_DIGIT } char_hex = @{ "\\x" ~ ASCII_HEX_DIGIT+ } char_unicode_four = @{ "\\" ~ ("u" | "U") ~ ASCII_HEX_DIGIT{4} ~ !ASCII_HEX_DIGIT } char_unicode_eight = @{ "\\" ~ ("u" | "U") ~ ASCII_HEX_DIGIT{8} ~ !ASCII_HEX_DIGIT } char_unicode = @{ char_unicode_four | char_unicode_eight } -char_special = _{ char_escaped | char_hex | char_unicode | char_octal } +char_special = _{ char_escaped | char_hex | char_unicode | char_octal | char_escaped_invalid } char_other_double_quote = @{ !("\\" | "\"" | "\n") ~ ANY } char_other_single_quote = @{ !("\\" | "\'" | "\n") ~ ANY } @@ -103,24 +112,24 @@ identifier = @{ ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_")* } // Literal. literal = _{ boolean | number | string | none | identifier } -//=============// +// =============// // Expressions // -//=============// +// =============// // Prefix. -or = { "||" } -and = { "&&" } -add = { "+" } -sub = { "-" } -mul = { "*" } +or = { "||" } +and = { "&&" } +add = { "+" } +sub = { "-" } +mul = { "*" } div = { "/" } -remainder = { "%" } -eq = { "==" } -neq = { "!=" } -lte = { "<=" } -gte = { ">=" } -lt = { "<" } -gt = { ">" } +remainder = { "%" } +eq = { "==" } +neq = { "!=" } +lte = { "<=" } +gte = { ">=" } +lt = { "<" } +gt = { ">" } infix = _{ or | and | add | sub | mul | div | remainder | eq | neq | lte | gte | lt | gt } @@ -131,14 +140,14 @@ unary_signed = { "+" | "-" } prefix = _{ negation | unary_signed } // Postfix. -member = ${ "." ~ identifier } +member = ${ "." ~ identifier } index = !{ "[" ~ expression ~ "]" } -apply = !{ "(" ~ expression ~ (COMMA ~ expression)* ~ COMMA? ~ ")" } +apply = !{ "(" ~ (expression ~ (COMMA ~ expression)* ~ COMMA?)* ~ ")" } postfix = _{ member | index | apply } // Core elements. -// +// // DIVERGE: for the struct, object, and map rules below, the comma is // designated as optional (`?`) when parsing. This is following a long // discussion within our team regarding the inconsistent treatment of required @@ -146,82 +155,96 @@ postfix = _{ member | index | apply } // make comma delimiters optional when parsing and enforce style rules via a // linter built on top of this parser. -identifier_based_kv_key = { identifier } -expression_based_kv_key = { expression } -kv_value = { expression } +identifier_based_kv_key = { identifier } +expression_based_kv_key = { expression } +kv_value = { expression } identifier_based_kv_pair = { identifier_based_kv_key ~ ":" ~ kv_value } expression_based_kv_pair = { expression_based_kv_key ~ ":" ~ kv_value } group = !{ "(" ~ expression ~ ")" } -if = ${ "if" ~ (WHITESPACE | COMMENT)+ ~ expression ~ (WHITESPACE | COMMENT)+ - ~ "then" ~ (WHITESPACE | COMMENT)+ ~ expression ~ (WHITESPACE | COMMENT)+ - ~ "else" ~ (WHITESPACE | COMMENT)+ ~ expression +if = ${ + "if" ~ (WHITESPACE | COMMENT)+ ~ expression ~ (WHITESPACE | COMMENT)+ ~ "then" ~ (WHITESPACE | COMMENT)+ ~ expression ~ (WHITESPACE | COMMENT)+ ~ "else" ~ (WHITESPACE | COMMENT)+ ~ expression } object_literal = !{ "object" ~ "{" ~ (identifier_based_kv_pair ~ (COMMA? ~ identifier_based_kv_pair)* ~ COMMA?)* ~ "}" } struct_literal = !{ identifier ~ "{" ~ (identifier_based_kv_pair ~ (COMMA? ~ identifier_based_kv_pair)* ~ COMMA?)* ~ "}" } map_literal = !{ "{" ~ (expression_based_kv_pair ~ (COMMA? ~ expression_based_kv_pair)* ~ COMMA?)* ~ "}" } -array_literal = !{ "[" ~ (expression ~ (COMMA ~ expression)* ~ COMMA?) ~ "]" } +array_literal = !{ "[" ~ (expression ~ (COMMA ~ expression)* ~ COMMA?)* ~ "]" } pair_literal = !{ "(" ~ expression ~ COMMA ~ expression ~ ")" } -core = _{ - group | - if | - object_literal | - struct_literal | - map_literal | - array_literal | - pair_literal | - literal | - identifier +core = _{ + group + | if + | object_literal + | struct_literal + | map_literal + | array_literal + | pair_literal + | literal + | identifier } // Expression. -// +// // NOTE: this rule is defined as compound-atomic (`$`) for the following reason: -// +// // Whitespace is a problem when your rule ends with an optional element. Many of // these expressions do, which means, when the element is not there, the // expression consumes all of the whitespace that falls of the end of the // statement. This is not a good practice, so we manually define where the // whitespace can be in these cases. -// +// // As such, you will see that none of the permutations of the rule below end in // an optional token. That is by design to avoid the problem above. -expression = ${ - prefix? ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix? ~ - ( - (WHITESPACE | COMMENT)* ~ infix ~ - (WHITESPACE | COMMENT)* ~ prefix? - ~ (WHITESPACE | COMMENT)* ~ core - ~ (WHITESPACE | COMMENT)* ~ postfix - | (WHITESPACE | COMMENT)* ~ infix - ~ (WHITESPACE | COMMENT)* ~ prefix? - ~ (WHITESPACE | COMMENT)* ~ core - )+ | - prefix? ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix | - prefix? ~ (WHITESPACE | COMMENT)* ~ core +expression = ${ + prefix* ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix* ~ ( + (WHITESPACE | COMMENT)* ~ infix ~ (WHITESPACE | COMMENT)* ~ prefix* ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix+ | + (WHITESPACE | COMMENT)* ~ infix ~ (WHITESPACE | COMMENT)* ~ prefix* ~ (WHITESPACE | COMMENT)* ~ core + )+ + | prefix* ~ (WHITESPACE | COMMENT)* ~ core ~ (WHITESPACE | COMMENT)* ~ postfix+ + | prefix* ~ (WHITESPACE | COMMENT)* ~ core } // Types. -// +// // NOTE: techically the spec calls the optional `+` the "non-empty" operator. // Since we have already defined this as the "one or more" operator and they // mean effectively the same thing, I've just kept this nomeclature. -array_type = { "Array" ~ - ("[" ~ wdl_type ~ "]" ~ ONE_OR_MORE) - | ("[" ~ wdl_type ~ "]") +array_type = ${ + "Array" ~ + (WHITESPACE | COMMENT)* ~ + ( + ("[" ~ (WHITESPACE | COMMENT)* ~ wdl_type_inner ~ (WHITESPACE | COMMENT)* ~ "]" ~ ONE_OR_MORE) | + ("[" ~ (WHITESPACE | COMMENT)* ~ wdl_type_inner ~ (WHITESPACE | COMMENT)* ~ "]") + ) } -map_type = { "Map" ~ "[" ~ wdl_type ~ COMMA ~ wdl_type ~ "]" } -pair_type = { "Pair" ~ "[" ~ wdl_type ~ COMMA ~ wdl_type ~ "]" } -string_type = { "String" } -file_type = { "File" } -bool_type = { "Boolean " } -int_type = { "Int" } -float_type = { "Float" } -object_type = { "Object" } - -type_base = _{ - map_type +// NOTE: The `map_type` and `pair_type` rules **must** be marked as non-atomic, as the +// `unbound_declaration` and `bound_declaration` rules that use them are marked +// as compound-atomic. +map_type = !{ "Map" ~ "[" ~ wdl_type_inner ~ COMMA ~ wdl_type_inner ~ "]" } +pair_type = !{ "Pair" ~ "[" ~ wdl_type_inner ~ COMMA ~ wdl_type_inner ~ "]" } +string_type = { "String" } +file_type = { "File" } +bool_type = { "Boolean" } +int_type = { "Int" } +float_type = { "Float" } +object_type = { "Object" } + +// NOTE: this rule was created separately from the below `wdl_type` rule to +// address the situation outlined in the comment there. In short, when a WDL +// type is embedded in things like a map or an array, you don't want to require +// that there is a space following the identifier. +wdl_type_inner = ${ + (map_type ~ OPTION) + | (array_type ~ OPTION) + | (pair_type ~ OPTION) + | (string_type ~ OPTION) + | (file_type ~ OPTION) + | (bool_type ~ OPTION) + | (int_type ~ OPTION) + | (float_type ~ OPTION) + | (object_type ~ OPTION) + | (identifier ~ OPTION) + | map_type | array_type | pair_type | string_type @@ -230,55 +253,73 @@ type_base = _{ | int_type | float_type | object_type + | identifier } -wdl_type = _{ - type_base ~ OPTION? - | type_base -} - -unbound_declaration = { - wdl_type ~ identifier -} - -bound_declaration = { - wdl_type ~ identifier ~ "=" ~ expression -} +// NOTE: this rule requires a positive predicate whitespace because there +// **must** be a whitespace between the `wdl_type` and the `identifier` for +// `bound_declaration`s and `unbound_declaration`s. Else, you get weird things +// happening in these rules. +// +// For example, when considering `IntermediateFiles`, `Int` matching the integer +// `wdl_type` and `ermediateFiles` matching the `identifier`. +wdl_type = ${ + (map_type ~ OPTION ~ &WHITESPACE) + | (array_type ~ OPTION ~ &WHITESPACE) + | (pair_type ~ OPTION ~ &WHITESPACE) + | (string_type ~ OPTION ~ &WHITESPACE) + | (file_type ~ OPTION ~ &WHITESPACE) + | (bool_type ~ OPTION ~ &WHITESPACE) + | (int_type ~ OPTION ~ &WHITESPACE) + | (float_type ~ OPTION ~ &WHITESPACE) + | (object_type ~ OPTION ~ &WHITESPACE) + | (identifier ~ OPTION ~ &WHITESPACE) + | (map_type ~ &WHITESPACE) + | (array_type ~ &WHITESPACE) + | (pair_type ~ &WHITESPACE) + | (string_type ~ &WHITESPACE) + | (file_type ~ &WHITESPACE) + | (bool_type ~ &WHITESPACE) + | (int_type ~ &WHITESPACE) + | (float_type ~ &WHITESPACE) + | (object_type ~ &WHITESPACE) + | (identifier ~ &WHITESPACE) +} + +unbound_declaration = { wdl_type ~ identifier } + +bound_declaration = { wdl_type ~ identifier ~ "=" ~ expression } declaration = _{ bound_declaration | unbound_declaration } -struct = {"struct" ~ identifier ~ "{" ~ (unbound_declaration)* ~ "}" } +struct = { "struct" ~ identifier ~ "{" ~ (unbound_declaration)* ~ "}" } // Imports. import_as = @{ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier } import_alias = @{ - "alias" ~ (WHITESPACE | COMMENT)+ ~ identifier ~ (WHITESPACE | COMMENT)+ - ~ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier + "alias" ~ (WHITESPACE | COMMENT)+ ~ identifier ~ (WHITESPACE | COMMENT)+ ~ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier } import = ${ - "import" ~ (WHITESPACE | COMMENT)+ ~ string ~ ((WHITESPACE | COMMENT)+ ~ import_as)? - ~ ((WHITESPACE | COMMENT)+ ~ import_alias)* + "import" ~ (WHITESPACE | COMMENT)+ ~ string ~ ((WHITESPACE | COMMENT)+ ~ import_as)? ~ ((WHITESPACE | COMMENT)+ ~ import_alias)* } -//================================// +// ================================// // Common Workflow/Tasks Elements // -//================================// +// ================================// // NOTE: the specification states the following in the workflow section: -// -// Tasks and workflows have several elements in common. These sections have -// nearly the same usage in workflows as they do in tasks, so we just link to -// their earlier descriptions. -// -// - Input section. -// - Private declarations. -// - Output section. -// - Metadata section. -// - Parameter metadata section. -// +// +// Tasks and workflows have several elements in common. These sections have +// nearly the same usage in workflows as they do in tasks, so we just link to +// their earlier descriptions. +// +// - Input section. +// - Private declarations. +// - Output section. +// // As such, I will use a common set of silent rules to define any single rule // that can be aliased. Note that the cascading rules for metadata sections and // parameter metadata sections cannot be aliased in this way, as the @@ -295,40 +336,42 @@ common_output = _{ "output" ~ "{" ~ (bound_declaration)* ~ "}" } common_private_declarations = _{ (bound_declaration)+ } // Common metadata elements. -// +// // DIVERGE: the specification says that this is equal sign (`=`) delimited, but // the examples show it actually being colon (`:`) delimited. As such, I've used // colon here. common_metadata_kv = _{ identifier ~ ":" ~ common_metadata_value } common_metadata_value = _{ - string | number | boolean | "null" | common_metadata_object | common_metadata_array + string + | number + | boolean + | "null" + | common_metadata_object + | common_metadata_array } common_metadata_object = _{ "{}" | "{" ~ common_metadata_kv ~ (COMMA ~ common_metadata_kv)* ~ "}" } -common_metadata_array = _{ "[]" | "[" ~ common_metadata_value ~ (COMMA ~ common_metadata_value)* ~ "]" } +common_metadata_array = _{ "[]" | "[" ~ common_metadata_value ~ (COMMA ~ common_metadata_value)* ~ "]" } -common_metadata = _{ "meta" ~ "{" ~ common_metadata_kv* ~ "}" } -common_parameter_metadata = _{ "parameter_meta" ~ "{" ~ common_metadata_kv* ~ "}" } - -//=======// +// =======// // Tasks // -//=======// +// =======// // Task runtimes. -// +// // DIVERGE: given the below logic concerning optional commas to delimit members // of `task_metadata_object`s, we determined it would be strange to not also // allow commas to delimit members of the runtime objects. As // such, though the specification states that not delimiting is not allowed, we // allow these keys to be optionally delimited by a comma. -// +// // NOTE: the `task_runtime_mapping_inner` and `task_runtime_mapping` are // structured in this way to avoid consuming whitespace at the end of the line // by adding an optional comma rule. task_runtime_mapping_inner = _{ identifier ~ ":" ~ expression } -task_runtime_mapping = { task_runtime_mapping_inner ~ COMMA | task_runtime_mapping_inner } -task_runtime = { "runtime" ~ "{" ~ (task_runtime_mapping)* ~ "}" } +task_runtime_mapping = { task_runtime_mapping_inner ~ COMMA | task_runtime_mapping_inner } +task_runtime = { "runtime" ~ "{" ~ (task_runtime_mapping)* ~ "}" } // Task input. task_input = { common_input } @@ -336,88 +379,77 @@ task_input = { common_input } // Task output. task_output = { common_output } +// Task expression placeholders. +// +// DIVERGE: the specification states that any expression placeholder conforms to +// the pattern `option="value"`. However, it is clear from the examples in the +// spec that single quoted strings are also allowed. Thus, we allow for either +// single or double quoted strings here—we will leave the selection of which to +// use up to a linting question. +// +// LENIENT: the specification is pretty clear that no spaces are allowed between +// the option and the equals sign or the equals sign and the value. However, +// many tools choose to allow spaces here. As such, we will allow spaces in +// between these elements, but we will throw a lint warning for these cases. +expression_placeholder_sep = { "sep" ~ "=" ~ string } +expression_placeholder_boolean = { boolean ~ "=" ~ string } +expression_placeholder_default = { "default" ~ "=" ~ string } + +expression_placeholder = { + expression_placeholder_sep | + expression_placeholder_boolean | + expression_placeholder_default +} +expression_placeholders = { expression_placeholder+ } + // Task commands, curly. -command_curly_begin = { "{" } -command_curly_end = { "}" } +command_curly_begin = { "command" ~ "{" } +command_curly_end = { "}" } command_curly_interpolation_start = _{ "~{" | "${" } -command_curly_interpolation_end = _{ "}" } +command_curly_interpolation_end = _{ "}" } command_curly_literal_contents = _{ - ( - !command_curly_begin ~ - !command_curly_end ~ - !command_curly_interpolation_start ~ - ANY - )+ + (!command_curly_begin ~ !command_curly_end ~ !command_curly_interpolation_start ~ ANY)+ } -command_curly_interpolated_token = { - ( - !command_curly_begin ~ - !command_curly_end ~ - !command_curly_interpolation_start ~ - !command_curly_interpolation_end ~ - ANY - )+ -} +command_curly_interpolated_expression = { command_curly_interpolated_contents | expression } command_curly_interpolated_contents = { command_curly_interpolation_start ~ - command_curly_interpolated_token ~ + expression_placeholders* ~ + command_curly_interpolated_expression ~ command_curly_interpolation_end } command_curly = { - command_curly_begin ~ - ( - command_curly_interpolated_contents | - command_curly_literal_contents - )* ~ - command_curly_end + command_curly_begin ~ (command_curly_interpolated_contents | command_curly_literal_contents)* ~ command_curly_end } // Task commands, heredoc. -command_heredoc_begin = { "<<<" } -command_heredoc_end = { ">>>" } +command_heredoc_begin = { "command" ~ "<<<" } +command_heredoc_end = { ">>>" } command_heredoc_interpolation_start = _{ "~{" } -command_heredoc_interpolation_end = _{ "}" } +command_heredoc_interpolation_end = _{ "}" } command_heredoc_literal_contents = _{ - ( - !command_heredoc_begin ~ - !command_heredoc_end ~ - !command_heredoc_interpolation_start ~ - ANY - )+ + (!command_heredoc_begin ~ !command_heredoc_end ~ !command_heredoc_interpolation_start ~ ANY)+ } -command_heredoc_interpolated_token = { - ( - !command_heredoc_begin ~ - !command_heredoc_end ~ - !command_heredoc_interpolation_start ~ - !command_heredoc_interpolation_end ~ - ANY - )+ -} +command_heredoc_interpolated_expression = { command_heredoc_interpolated_contents | expression } command_heredoc_interpolated_contents = { command_heredoc_interpolation_start ~ - command_heredoc_interpolated_token ~ + expression_placeholders* ~ + command_heredoc_interpolated_expression ~ command_heredoc_interpolation_end } command_heredoc = { - command_heredoc_begin ~ - ( - command_heredoc_interpolated_contents | - command_heredoc_literal_contents - )* ~ - command_heredoc_end + command_heredoc_begin ~ (command_heredoc_interpolated_contents | command_heredoc_literal_contents)* ~ command_heredoc_end } -task_command = { "command" ~ (command_heredoc | command_curly) } +task_command = { (command_heredoc | command_curly) } // Task private declarations. task_private_declarations = { common_private_declarations } @@ -426,20 +458,25 @@ task_private_declarations = { common_private_declarations } task_metadata_kv = { identifier ~ ":" ~ task_metadata_value } task_metadata_value = { - string | number | boolean | "null" | task_metadata_object | task_metadata_array + string + | number + | boolean + | "null" + | task_metadata_object + | task_metadata_array } // DIVERGE: the specification indicates that the members of both objects and // arrays should have the same delimiter. From the spec, they specify `,` as the // delimiter (with no quotes)—this leads to ambiguity as to whether a literal // comma is needed to delimit these items. -// +// // In the case of an array, it seems obvious that a comma is needed to delimit // items. It is not as obvious whether commas should be required for objects. // Notably, both of these constructs exist elsewhere in the specification (e.g., // object literals and array literals), and a comma delimiter is _required_ in // those cases. -// +// // For the object rule below, the comma is designated as optional (`?`) when // delimiting items. This is following a long discussion within our team // regarding the inconsistent treatment of required comma delimiters for @@ -447,37 +484,37 @@ task_metadata_value = { // delimiters optional when parsing and enforce style rules via a linter built // on top of this parser. task_metadata_object = { "{}" | "{" ~ task_metadata_kv ~ (COMMA? ~ task_metadata_kv)* ~ "}" } -task_metadata_array = { "[]" | "[" ~ task_metadata_value ~ (COMMA? ~ task_metadata_value)* ~ "]" } +task_metadata_array = { "[]" | "[" ~ task_metadata_value ~ (COMMA? ~ task_metadata_value)* ~ "]" } // DIVERGE: given the above logic concerning optional commas to delimit members // of `task_metadata_object`s, we determined it would be strange to not also // allow commas to delimit members of the the top-level objects themselves. As // such, though the specification states that not delimiting is not allowed, we // allow these keys to be optionally delimited by a comma. -// +// // Below are the old rules used in case we ever need them. -// +// // task_metadata = { "meta" ~ "{" ~ task_metadata_kv* ~ "}" } // task_parameter_metadata = { "parameter_meta" ~ "{" ~ task_metadata_kv* ~ "}" -task_metadata = { "meta" ~ "{" ~ task_metadata_kv? ~ (COMMA? ~ task_metadata_kv)* ~ "}" } +task_metadata = { "meta" ~ "{" ~ task_metadata_kv? ~ (COMMA? ~ task_metadata_kv)* ~ "}" } task_parameter_metadata = { "parameter_meta" ~ "{" ~ task_metadata_kv? ~ (COMMA? ~ task_metadata_kv)* ~ "}" } // Task elements. task_element = { - task_input | - task_output | - task_command | - task_runtime | - task_private_declarations | - task_parameter_metadata | - task_metadata + task_input + | task_output + | task_command + | task_runtime + | task_private_declarations + | task_parameter_metadata + | task_metadata } -task = {"task" ~ identifier ~ "{" ~ task_element+ ~ "}" } +task = { "task" ~ identifier ~ "{" ~ task_element+ ~ "}" } -//===========// +// ===========// // Workflows // -//===========// +// ===========// // Workflow input. workflow_input = { common_input } @@ -490,80 +527,63 @@ workflow_private_declarations = { common_private_declarations } // Workflow call qualified_identifier = ${ identifier ~ ("." ~ identifier)+ } -workflow_call_input = { +workflow_call_input = { (identifier ~ "=" ~ expression) - | (identifier ~ "=" ~ identifier) - | identifier + | (identifier ~ "=" ~ identifier) + | identifier } // DIVERGE: spec is ambiguous about whether whitespace is // allowed between the opening and closing brackets (`{}`) // when the call body is empty. We have opted to allow whitespace. -workflow_call_body = !{ "{" - ~ ("input:" ~ workflow_call_input ~ (COMMA ~ workflow_call_input)*)? - ~ COMMA? ~ "}" +workflow_call_body = !{ + "{" ~ ("input:" ~ workflow_call_input ~ (COMMA ~ workflow_call_input)*)? ~ COMMA? ~ "}" } -workflow_call_as = ${ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier } +workflow_call_as = ${ "as" ~ (WHITESPACE | COMMENT)+ ~ identifier } workflow_call_after = ${ "after" ~ (WHITESPACE | COMMENT)+ ~ identifier } -workflow_call = ${ - ("call" - ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) - ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? - ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)* - ~ ((WHITESPACE | COMMENT)* ~ workflow_call_body) - ) - | ("call" - ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) - ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? - ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)+ - ) - | ("call" - ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) - ~ (WHITESPACE | COMMENT)+ ~ workflow_call_as - ) - | ("call" - ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) - ) +workflow_call = ${ + ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)* ~ ((WHITESPACE | COMMENT)* ~ workflow_call_body)) + | ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_as)? ~ ((WHITESPACE | COMMENT)+ ~ workflow_call_after)+) + | ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier) ~ (WHITESPACE | COMMENT)+ ~ workflow_call_as) + | ("call" ~ (WHITESPACE | COMMENT)+ ~ (qualified_identifier | identifier)) } // Workflow scatter workflow_scatter_iteration_statement = { "(" ~ identifier ~ "in" ~ expression ~ ")" } -workflow_scatter = { - "scatter" - ~ workflow_scatter_iteration_statement - ~ "{" - ~ workflow_execution_statement* - ~ "}" +workflow_scatter = { + "scatter" ~ workflow_scatter_iteration_statement ~ "{" ~ workflow_execution_statement* ~ "}" } // Workflow conditional workflow_conditional = { "if" ~ "(" ~ expression ~ ")" ~ "{" ~ workflow_execution_statement* ~ "}" } // Workflow execution statements -workflow_execution_statement = { ( - workflow_conditional - | workflow_scatter - | workflow_call - | workflow_private_declarations -) } +workflow_execution_statement = { + (workflow_conditional | workflow_scatter | workflow_call | workflow_private_declarations) +} // Workflow metadata and parameter metadata. workflow_metadata_kv = { identifier ~ ":" ~ workflow_metadata_value } workflow_metadata_value = { - string | number | boolean | "null" | workflow_metadata_object | workflow_metadata_array + string + | number + | boolean + | "null" + | workflow_metadata_object + | workflow_metadata_array } // DIVERGE: the specification indicates that the members of both objects and // arrays should have the same delimiter. From the spec, they specify `,` as the // delimiter (with no quotes)—this leads to ambiguity as to whether a literal // comma is needed to delimit these items. -// +// // In the case of an array, it seems obvious that a comma is needed to delimit // items. It is not as obvious whether commas should be required for objects. // Notably, both of these constructs exist elsewhere in the specification (e.g., // object literals and array literals), and a comma delimiter is _required_ in // those cases. -// +// // For the object rule below, the comma is designated as optional (`?`) when // delimiting items. This is following a long discussion within our team // regarding the inconsistent treatment of required comma delimiters for @@ -571,27 +591,27 @@ workflow_metadata_value = { // delimiters optional when parsing and enforce style rules via a linter built // on top of this parser. workflow_metadata_object = { "{}" | "{" ~ workflow_metadata_kv ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } -workflow_metadata_array = { "[]" | "[" ~ workflow_metadata_value ~ (COMMA ~ workflow_metadata_value)* ~ "]" } +workflow_metadata_array = { "[]" | "[" ~ workflow_metadata_value ~ (COMMA ~ workflow_metadata_value)* ~ "]" } // DIVERGE: given the above logic concerning optional commas to delimit members // of `workflow_metadata_object`s, we determined it would be strange to not also // allow commas to delimit members of the the top-level objects themselves. As // such, though the specification states that not delimiting is not allowed, we // allow these keys to be optionally delimited by a comma. -// +// // Below are the old rules used in case we ever need them. -// +// // workflow_metadata = { "meta" ~ "{" ~ workflow_metadata_kv* ~ "}" } // workflow_parameter_metadata = { "parameter_meta" ~ "{" ~ workflow_metadata_kv* ~ "}" -workflow_metadata = { "meta" ~ "{" ~ workflow_metadata_kv? ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } +workflow_metadata = { "meta" ~ "{" ~ workflow_metadata_kv? ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } workflow_parameter_metadata = { "parameter_meta" ~ "{" ~ workflow_metadata_kv? ~ (COMMA? ~ workflow_metadata_kv)* ~ "}" } workflow_element = _{ - workflow_input | - workflow_output | - workflow_execution_statement | - workflow_parameter_metadata | - workflow_metadata + workflow_input + | workflow_output + | workflow_execution_statement + | workflow_parameter_metadata + | workflow_metadata } workflow = { "workflow" ~ identifier ~ "{" ~ workflow_element* ~ "}" }