hurlfmt-6.1.1/.cargo_vcs_info.json0000644000000001560000000000100125240ustar { "git": { "sha1": "ae921ffbd836db667995c17262d11b58c2ee7e87" }, "path_in_vcs": "packages/hurlfmt" }hurlfmt-6.1.1/Cargo.lock0000644000000377030000000000100105070ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" dependencies = [ "anstyle", "once_cell", "windows-sys", ] [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ "bit-vec", ] [[package]] name = "bit-vec" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clap" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_lex" version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "colored" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ "windows-sys", ] [[package]] name = "errno" version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", "windows-sys", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "getrandom" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", "r-efi", "wasi 0.14.2+wasi-0.2.4", ] [[package]] name = "hurl_core" version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "505b5e449d27ba650b225aa4fd189b6db237cb90e674308ce26fa7277168fe80" dependencies = [ "colored", "libxml", "regex", ] [[package]] name = "hurlfmt" version = "6.1.1" dependencies = [ "base64", "clap", "hurl_core", "proptest", "regex", ] [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" [[package]] name = "libxml" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fe73cdec2bcb36d25a9fe3f607ffcd44bb8907ca0100c4098d1aa342d1e7bec" dependencies = [ "libc", "pkg-config", "vcpkg", ] [[package]] name = "linux-raw-sys" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "proc-macro2" version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ "bit-set", "bit-vec", "bitflags", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", "regex-syntax", "rusty-fork", "tempfile", "unarray", ] [[package]] name = "quick-error" version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" [[package]] name = "rand" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha", "rand_core", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.15", ] [[package]] name = "rand_xorshift" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" dependencies = [ "rand_core", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys", ] [[package]] name = "rusty-fork" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" dependencies = [ "fnv", "quick-error", "tempfile", "wait-timeout", ] [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" dependencies = [ "fastrand", "getrandom 0.3.2", "once_cell", "rustix", "windows-sys", ] [[package]] name = "terminal_size" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ "rustix", "windows-sys", ] [[package]] name = "unarray" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicode-ident" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "wait-timeout" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ "libc", ] [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] [[package]] name = "zerocopy" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", "syn", ] hurlfmt-6.1.1/Cargo.toml0000644000000031020000000000100105140ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.85.0" name = "hurlfmt" version = "6.1.1" authors = [ "Fabrice Reix ", "Jean-Christophe Amiel ", "Filipe Pinto ", ] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "Format Hurl files" homepage = "https://hurl.dev" documentation = "https://hurl.dev" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/Orange-OpenSource/hurl" [lib] name = "hurlfmt" path = "src/lib.rs" [[bin]] name = "hurlfmt" path = "src/main.rs" [[test]] name = "json" path = "tests/json.rs" [dependencies.base64] version = "0.22.1" [dependencies.clap] version = "4.5.32" features = [ "cargo", "wrap_help", ] [dependencies.hurl_core] version = "6.1.1" [dependencies.regex] version = "1.11.1" [dev-dependencies.proptest] version = "1.6.0" [lints.clippy] empty_structs_with_brackets = "deny" manual_string_new = "deny" semicolon_if_nothing_returned = "deny" wildcard-imports = "deny" [lints.rust] warnings = "deny" hurlfmt-6.1.1/Cargo.toml.orig000064400000000000000000000012211046102023000141750ustar 00000000000000[package] name = "hurlfmt" version = "6.1.1" authors = ["Fabrice Reix ", "Jean-Christophe Amiel ", "Filipe Pinto "] edition = "2021" license = "Apache-2.0" description = "Format Hurl files" documentation = "https://hurl.dev" homepage = "https://hurl.dev" repository = "https://github.com/Orange-OpenSource/hurl" rust-version = "1.85.0" [dependencies] base64 = "0.22.1" clap = { version = "4.5.32", features = ["cargo", "wrap_help"] } hurl_core = { version = "6.1.1", path = "../hurl_core" } regex = "1.11.1" [dev-dependencies] proptest = "1.6.0" [lints] workspace = true hurlfmt-6.1.1/README.md000064400000000000000000000002321046102023000125660ustar 00000000000000hurlfmt ===================================== The hurlfmt crate provides the `hurlfmt` binary. It can format hurl files and to export them to JSON/HTML. hurlfmt-6.1.1/src/cli/error.rs000064400000000000000000000013161046102023000143500ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ #[derive(Clone, Debug, PartialEq, Eq)] pub struct CliError { pub message: String, } hurlfmt-6.1.1/src/cli/logger.rs000064400000000000000000000052521046102023000145010ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::error::{DisplaySourceError, OutputFormat}; use hurl_core::input::Input; use hurl_core::text::{Format, Style, StyledString}; /// A simple logger to log app related event (start, high levels error, etc...). pub struct Logger { /// Format of the message in the terminal: ANSI or plain. format: Format, } impl Logger { /// Creates a new logger using `color`. pub fn new(color: bool) -> Self { let format = if color { Format::Ansi } else { Format::Plain }; Logger { format } } /// Prints an error `message` on standard error. pub fn error(&self, message: &str) { let mut s = StyledString::new(); s.push_with("error", Style::new().red().bold()); s.push(": "); s.push_with(message, Style::new().bold()); eprintln!("{}", s.to_string(self.format)); } /// Displays a Hurl parsing error. pub fn error_parsing(&self, content: &str, file: &Input, error: &E) { // FIXME: peut-être qu'on devrait faire rentrer le prefix `error:` qui est // fournit par `self.error_rich` dans la méthode `error.to_string` let message = error.to_string( &file.to_string(), content, None, OutputFormat::Terminal(self.format == Format::Ansi), ); let mut s = StyledString::new(); s.push_with("error", Style::new().red().bold()); s.push(": "); s.push(&message); s.push("\n"); eprintln!("{}", s.to_string(self.format)); } /// Displays a lint warning. pub fn warn_lint(&self, content: &str, file: &Input, error: &E) { let message = error.to_string( &file.to_string(), content, None, OutputFormat::Terminal(self.format == Format::Ansi), ); let mut s = StyledString::new(); s.push_with("warning", Style::new().yellow().bold()); s.push(": "); s.push(&message); s.push("\n"); eprintln!("{}", s.to_string(self.format)); } } hurlfmt-6.1.1/src/cli/mod.rs000064400000000000000000000013001046102023000137670ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ pub use self::logger::Logger; pub mod error; mod logger; pub mod options; hurlfmt-6.1.1/src/cli/options/commands.rs000064400000000000000000000052131046102023000165130ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ // Generated by bin/spec/options/generate_source.py - Do not modify pub fn input_files() -> clap::Arg { clap::Arg::new("input_files") .value_name("FILES") .help("Set the input file to use") .required(false) .index(1) .num_args(1..) } pub fn check() -> clap::Arg { clap::Arg::new("check") .long("check") .help("Run in check mode") .conflicts_with("output") .action(clap::ArgAction::SetTrue) } pub fn color() -> clap::Arg { clap::Arg::new("color") .long("color") .help("Colorize Output") .conflicts_with("no_color") .conflicts_with("in_place") .action(clap::ArgAction::SetTrue) } pub fn in_place() -> clap::Arg { clap::Arg::new("in_place") .long("in-place") .help("Modify files in place") .conflicts_with("output") .conflicts_with("color") .action(clap::ArgAction::SetTrue) } pub fn input_format() -> clap::Arg { clap::Arg::new("input_format") .long("in") .value_name("FORMAT") .default_value("hurl") .help("Specify input format: hurl or curl") .num_args(1) } pub fn no_color() -> clap::Arg { clap::Arg::new("no_color") .long("no-color") .help("Do not colorize output") .conflicts_with("color") .action(clap::ArgAction::SetTrue) } pub fn output() -> clap::Arg { clap::Arg::new("output") .long("output") .short('o') .value_name("FILE") .help("Write to FILE instead of stdout") .num_args(1) } pub fn output_format() -> clap::Arg { clap::Arg::new("output_format") .long("out") .value_name("FORMAT") .default_value("hurl") .help("Specify output format: hurl, json or html") .conflicts_with("check") .num_args(1) } pub fn standalone() -> clap::Arg { clap::Arg::new("standalone") .long("standalone") .help("Standalone HTML") .conflicts_with("no_color") .action(clap::ArgAction::SetTrue) } hurlfmt-6.1.1/src/cli/options/matches.rs000064400000000000000000000106461046102023000163440ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use std::io; use std::io::IsTerminal; use std::path::{Path, PathBuf}; use clap::ArgMatches; use hurl_core::input::Input; use super::OptionsError; use crate::cli::options::{InputFormat, OutputFormat}; pub fn check(arg_matches: &ArgMatches) -> bool { has_flag(arg_matches, "check") } pub fn color(arg_matches: &ArgMatches) -> bool { if has_flag(arg_matches, "color") { true } else if has_flag(arg_matches, "no_color") || has_flag(arg_matches, "in_place") { false } else { io::stdout().is_terminal() } } pub fn input_format(arg_matches: &ArgMatches) -> Result { match get_string(arg_matches, "input_format").unwrap().as_str() { "hurl" => Ok(InputFormat::Hurl), "curl" => Ok(InputFormat::Curl), v => Err(OptionsError::Error(format!("Invalid input format {v}"))), } } pub fn output_format(arg_matches: &ArgMatches) -> Result { match get_string(arg_matches, "output_format").unwrap().as_str() { "hurl" => Ok(OutputFormat::Hurl), "json" => Ok(OutputFormat::Json), "html" => Ok(OutputFormat::Html), v => Err(OptionsError::Error(format!("Invalid output format {v}"))), } } pub fn in_place(arg_matches: &ArgMatches) -> Result { if has_flag(arg_matches, "in_place") { if get_string(arg_matches, "input_format") != Some("hurl".to_string()) { Err(OptionsError::Error( "You can use --in-place only hurl format!".to_string(), )) } else if get_string(arg_matches, "input_files").is_none() { Err(OptionsError::Error( "You can not use --in-place with standard input stream!".to_string(), )) } else { Ok(true) } } else { Ok(false) } } /// Returns the input files from the positional arguments and input stream pub fn input_files(arg_matches: &ArgMatches) -> Result, OptionsError> { let mut files = vec![]; if let Some(filenames) = get_strings(arg_matches, "input_files") { for filename in &filenames { let filename = Path::new(filename); if !filename.exists() { return Err(OptionsError::Error(format!( "error: Cannot access '{}': No such file or directory", filename.display() ))); } let file = Input::from(filename); files.push(file); } } if files.is_empty() && !io::stdin().is_terminal() { let input = match Input::from_stdin() { Ok(input) => input, Err(err) => return Err(OptionsError::Error(err.to_string())), }; files.push(input); } Ok(files) } pub fn output_file(arg_matches: &ArgMatches) -> Option { get_string(arg_matches, "output").map(|s| Path::new(&s).to_path_buf()) } pub fn standalone(arg_matches: &ArgMatches) -> Result { if has_flag(arg_matches, "standalone") { if get_string(arg_matches, "output_format") != Some("html".to_string()) { Err(OptionsError::Error( "use --standalone option only with html output".to_string(), )) } else { Ok(true) } } else { Ok(false) } } fn has_flag(matches: &ArgMatches, name: &str) -> bool { matches.get_one::(name) == Some(&true) } pub fn get_string(matches: &ArgMatches, name: &str) -> Option { matches.get_one::(name).map(|x| x.to_string()) } /// Returns an optional list of `String` from the command line `matches` given the option `name`. pub fn get_strings(matches: &ArgMatches, name: &str) -> Option> { matches .get_many::(name) .map(|v| v.map(|x| x.to_string()).collect()) } hurlfmt-6.1.1/src/cli/options/mod.rs000064400000000000000000000062161046102023000154750ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ mod commands; mod matches; use std::env; use std::path::PathBuf; use clap::ArgMatches; use hurl_core::input::Input; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Options { pub check: bool, pub color: bool, pub in_place: bool, pub input_files: Vec, pub input_format: InputFormat, pub output_file: Option, pub output_format: OutputFormat, pub standalone: bool, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum InputFormat { Curl, Hurl, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum OutputFormat { Hurl, Json, Html, } #[derive(Clone, Debug, PartialEq, Eq)] pub enum OptionsError { Info(String), Error(String), } impl From for OptionsError { fn from(error: clap::Error) -> Self { match error.kind() { clap::error::ErrorKind::DisplayVersion => OptionsError::Info(error.to_string()), clap::error::ErrorKind::DisplayHelp => OptionsError::Info(error.to_string()), _ => OptionsError::Error(error.to_string()), } } } pub fn parse() -> Result { let mut command = clap::Command::new("hurlfmt") .version(clap::crate_version!()) .disable_colored_help(true) .about("Format Hurl files") .arg(commands::check()) .arg(commands::color()) .arg(commands::in_place()) .arg(commands::input_files()) .arg(commands::input_format()) .arg(commands::no_color()) .arg(commands::output()) .arg(commands::output_format()) .arg(commands::standalone()); let arg_matches = command.try_get_matches_from_mut(env::args_os())?; let opts = parse_matches(&arg_matches)?; if opts.input_files.is_empty() { let help = command.render_help().to_string(); return Err(OptionsError::Error(help)); } Ok(opts) } fn parse_matches(arg_matches: &ArgMatches) -> Result { let check = matches::check(arg_matches); let color = matches::color(arg_matches); let in_place = matches::in_place(arg_matches)?; let input_files = matches::input_files(arg_matches)?; let input_format = matches::input_format(arg_matches)?; let output_file = matches::output_file(arg_matches); let output_format = matches::output_format(arg_matches)?; let standalone = matches::standalone(arg_matches)?; Ok(Options { check, color, in_place, input_files, input_format, output_file, output_format, standalone, }) } hurlfmt-6.1.1/src/command/check.rs000064400000000000000000000036371046102023000151530ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::input::Input; use hurl_core::parser::{self, ParseError}; use crate::{format, linter}; /// Represents a check error. pub enum CheckError { IO { filename: String, message: String, }, Parse { content: String, input_file: Input, error: ParseError, }, Unformatted(String), } /// Run the check command for a list of input files pub fn run(input_files: &[Input]) -> Vec { let mut errors = vec![]; for input_file in input_files { if let Err(e) = run_check(input_file) { errors.push(e); } } errors } /// Run the check command for one input file fn run_check(input_file: &Input) -> Result<(), CheckError> { let content = input_file.read_to_string().map_err(|e| CheckError::IO { filename: input_file.to_string(), message: e.to_string(), })?; let hurl_file = parser::parse_hurl_file(&content).map_err(|error| CheckError::Parse { content: content.clone(), input_file: input_file.clone(), error, })?; let hurl_file = linter::lint_hurl_file(&hurl_file); let formatted = format::format_text(&hurl_file, false); if formatted == content { Ok(()) } else { Err(CheckError::Unformatted(input_file.to_string())) } } hurlfmt-6.1.1/src/command/export.rs000064400000000000000000000050161046102023000154100ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::input::Input; use hurl_core::parser::{self, ParseError}; use crate::cli::options::{InputFormat, OutputFormat}; use crate::{curl, format, linter}; /// Represents an export error. pub enum ExportError { IO { filename: String, message: String, }, Parse { content: String, input_file: Input, error: ParseError, }, Curl(String), } /// Run the export command for a list of input files pub fn run( input_files: &[Input], input_format: &InputFormat, output_format: &OutputFormat, standalone: bool, color: bool, ) -> Vec> { input_files .iter() .map(|input_file| run_export(input_file, input_format, output_format, standalone, color)) .collect() } /// Run the export command for one input file fn run_export( input_file: &Input, input_format: &InputFormat, output_format: &OutputFormat, standalone: bool, color: bool, ) -> Result { let content = input_file.read_to_string().map_err(|e| ExportError::IO { filename: input_file.to_string(), message: e.to_string(), })?; // Parse input curl or Hurl file let input = match input_format { InputFormat::Hurl => content.to_string(), InputFormat::Curl => curl::parse(&content).map_err(ExportError::Curl)?, }; let hurl_file = parser::parse_hurl_file(&input).map_err(|error| ExportError::Parse { content: input.clone(), input_file: input_file.clone(), error, })?; let output = match output_format { OutputFormat::Hurl => { let hurl_file = linter::lint_hurl_file(&hurl_file); format::format_text(&hurl_file, color) } OutputFormat::Json => format::format_json(&hurl_file), OutputFormat::Html => hurl_core::format::format_html(&hurl_file, standalone), }; Ok(output) } hurlfmt-6.1.1/src/command/format.rs000064400000000000000000000046141046102023000153620ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use std::fs; use std::io::Write; use std::path::{Path, PathBuf}; use hurl_core::input::Input; use hurl_core::parser::{self, ParseError}; use crate::{format, linter}; /// Represents a check error. pub enum FormatError { IO { filename: String, message: String, }, Parse { content: String, input_file: Input, error: ParseError, }, } /// Run the format command for a list of input files pub fn run(input_files: &[PathBuf]) -> Vec { let mut errors = vec![]; for input_file in input_files { if let Err(e) = run_format(input_file) { errors.push(e); } } errors } /// Run the format command for one input file fn run_format(input_file: &Path) -> Result<(), FormatError> { let content = fs::read_to_string(input_file.display().to_string()).map_err(|e| FormatError::IO { filename: input_file.display().to_string(), message: e.to_string(), })?; let hurl_file = parser::parse_hurl_file(&content).map_err(|error| FormatError::Parse { content: content.clone(), input_file: Input::new(input_file.display().to_string().as_str()), error, })?; let hurl_file = linter::lint_hurl_file(&hurl_file); let formatted = format::format_text(&hurl_file, false); let mut file = match std::fs::File::create(input_file) { Err(e) => { return Err(FormatError::IO { filename: input_file.display().to_string(), message: e.to_string(), }) } Ok(file) => file, }; file.write_all(formatted.as_bytes()) .map_err(|e| FormatError::IO { filename: input_file.display().to_string(), message: e.to_string(), })?; Ok(()) } hurlfmt-6.1.1/src/command/mod.rs000064400000000000000000000012441046102023000146450ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ pub mod check; pub mod export; pub mod format; hurlfmt-6.1.1/src/curl/args.rs000064400000000000000000000151241046102023000143530ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::reader::Reader; /// Split a `str` into a vec of String params pub fn split(s: &str) -> Result, String> { let mut params = vec![]; let mut parser = Parser::new(s); while let Some(param) = parser.param()? { params.push(param); } Ok(params) } struct Parser { reader: Reader, } impl Parser { fn new(s: &str) -> Parser { let reader = Reader::new(s); Parser { reader } } fn delimiter(&mut self) -> Option<(char, bool)> { if self.reader.peek() == Some('\'') { _ = self.reader.read(); Some(('\'', false)) } else if self.reader.peek() == Some('$') { let save = self.reader.cursor(); _ = self.reader.read(); if self.reader.peek() == Some('\'') { _ = self.reader.read(); Some(('\'', true)) } else { self.reader.seek(save); None } } else { None } } fn col(&self) -> usize { self.reader.cursor().pos.column } fn param(&mut self) -> Result, String> { _ = self.reader.read_while(|c| c == ' '); if self.reader.is_eof() { return Ok(None); } let mut value = String::new(); if let Some((delimiter, escaping)) = self.delimiter() { while let Some(c1) = self.reader.read() { if c1 == '\\' && escaping { let c2 = match self.reader.read() { Some('n') => '\n', Some('t') => '\t', Some('r') => '\r', Some(c) => c, _ => { let col = self.col(); return Err(format!("Invalid escape at column {col}")); } }; value.push(c2); } else if c1 == delimiter { return Ok(Some(value)); } else { value.push(c1); } } let col = self.col(); Err(format!("Missing delimiter {delimiter} at column {col}")) } else { loop { match self.reader.read() { Some('\\') => { if let Some(c) = self.reader.read() { value.push(c); } else { let col = self.col(); return Err(format!("Invalid escape at column {col}")); } } Some(' ') => return Ok(Some(value)), Some(c) => { value.push(c); } _ => return Ok(Some(value)), } } } } } #[cfg(test)] mod test { use crate::curl::args; use crate::curl::args::Parser; #[test] fn test_split() { let expected = vec!["AAA".to_string(), "BBB".to_string()]; assert_eq!(args::split(r#"AAA BBB"#).unwrap(), expected); assert_eq!(args::split(r#"AAA BBB"#).unwrap(), expected); assert_eq!(args::split(r#" AAA BBB "#).unwrap(), expected); assert_eq!(args::split(r#"AAA 'BBB'"#).unwrap(), expected); assert_eq!(args::split(r#"AAA $'BBB'"#).unwrap(), expected); let expected = vec!["'".to_string()]; assert_eq!(args::split(r"$'\''").unwrap(), expected); } #[test] fn test_split_error() { assert_eq!( args::split(r#"AAA 'BBB"#).err().unwrap(), "Missing delimiter ' at column 9".to_string() ); } #[test] fn test_param_without_quote() { let mut parser = Parser::new("value"); assert_eq!(parser.param().unwrap().unwrap(), "value".to_string()); assert_eq!(parser.col(), 6); let mut parser = Parser::new(" value "); assert_eq!(parser.param().unwrap().unwrap(), "value".to_string()); assert_eq!(parser.col(), 8); } #[test] fn test_param_with_quote() { let mut parser = Parser::new("'value'"); assert_eq!(parser.param().unwrap().unwrap(), "value".to_string()); assert_eq!(parser.col(), 8); let mut parser = Parser::new(" 'value' "); assert_eq!(parser.param().unwrap().unwrap(), "value".to_string()); assert_eq!(parser.col(), 9); let mut parser = Parser::new("'\\n'"); assert_eq!(parser.param().unwrap().unwrap(), "\\n".to_string()); assert_eq!(parser.col(), 5); } #[test] fn test_dollar_prefix() { let mut parser = Parser::new("$'Test: \\''"); assert_eq!(parser.param().unwrap().unwrap(), "Test: '".to_string()); assert_eq!(parser.col(), 12); let mut parser = Parser::new("$'\\n'"); assert_eq!(parser.param().unwrap().unwrap(), "\n".to_string()); assert_eq!(parser.col(), 6); } #[test] fn test_param_missing_closing_quote() { let mut parser = Parser::new("'value"); assert_eq!( parser.param().err().unwrap(), "Missing delimiter ' at column 7".to_string() ); assert_eq!(parser.col(), 7); } #[test] fn test_no_more_param() { assert_eq!(Parser::new("").param().unwrap(), None); assert_eq!(Parser::new(" ").param().unwrap(), None); } #[test] fn test_delimiter() { let mut parser = Parser::new("value"); assert_eq!(parser.delimiter(), None); assert_eq!(parser.col(), 1); let mut parser = Parser::new("'value'"); assert_eq!(parser.delimiter().unwrap(), ('\'', false)); assert_eq!(parser.col(), 2); let mut parser = Parser::new("$'value'"); assert_eq!(parser.delimiter().unwrap(), ('\'', true)); assert_eq!(parser.col(), 3); let mut parser = Parser::new("$value"); assert_eq!(parser.delimiter(), None); assert_eq!(parser.col(), 1); } } hurlfmt-6.1.1/src/curl/commands.rs000064400000000000000000000043021046102023000152140ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use clap::{value_parser, ArgAction}; pub fn compressed() -> clap::Arg { clap::Arg::new("compressed").long("compressed").num_args(0) } pub fn data() -> clap::Arg { clap::Arg::new("data") .long("data") .short('d') .value_name("data") .num_args(1) } pub fn headers() -> clap::Arg { clap::Arg::new("headers") .long("header") .short('H') .value_name("NAME:VALUE") .action(ArgAction::Append) .num_args(1) } pub fn insecure() -> clap::Arg { clap::Arg::new("insecure") .long("insecure") .short('k') .num_args(0) } pub fn location() -> clap::Arg { clap::Arg::new("location") .long("location") .short('L') .num_args(0) } pub fn max_redirects() -> clap::Arg { clap::Arg::new("max_redirects") .long("max-redirs") .value_name("NUM") .allow_hyphen_values(true) .value_parser(value_parser!(i32).range(-1..)) .num_args(1) } pub fn method() -> clap::Arg { clap::Arg::new("method") .long("request") .short('X') .value_name("METHOD") .num_args(1) } pub fn retry() -> clap::Arg { clap::Arg::new("retry") .long("retry") .value_name("seconds") .value_parser(value_parser!(i32)) .num_args(1) } pub fn url() -> clap::Arg { clap::Arg::new("url") .long("url") .value_name("url") .num_args(1) } pub fn url_param() -> clap::Arg { clap::Arg::new("url_param") .help("Sets the url to use") .required(false) .num_args(1) } hurlfmt-6.1.1/src/curl/matches.rs000064400000000000000000000072001046102023000150370ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use clap::ArgMatches; use super::HurlOption; pub fn body(arg_matches: &ArgMatches) -> Option { match get_string(arg_matches, "data") { None => None, Some(v) => { if let Some(filename) = v.strip_prefix('@') { Some(format!("file, {filename};")) } else { Some(format!("```\n{v}\n```")) } } } } pub fn method(arg_matches: &ArgMatches) -> String { match get_string(arg_matches, "method") { None => { if arg_matches.contains_id("data") { "POST".to_string() } else { "GET".to_string() } } Some(v) => v, } } pub fn url(arg_matches: &ArgMatches) -> String { let s = if let Some(value) = get_string(arg_matches, "url") { value } else { get_string(arg_matches, "url_param").unwrap() }; if !s.starts_with("http") { format!("https://{s}") } else { s } } pub fn headers(arg_matches: &ArgMatches) -> Vec { let mut headers = get_strings(arg_matches, "headers").unwrap_or_default(); if !has_content_type(&headers) { if let Some(data) = get_string(arg_matches, "data") { if !data.starts_with('@') { headers.push("Content-Type: application/x-www-form-urlencoded".to_string()); } } } headers } pub fn options(arg_matches: &ArgMatches) -> Vec { let mut options = vec![]; if has_flag(arg_matches, "compressed") { options.push(HurlOption::new("compressed", "true")); } if has_flag(arg_matches, "location") { options.push(HurlOption::new("location", "true")); } if has_flag(arg_matches, "insecure") { options.push(HurlOption::new("insecure", "true")); } if let Some(value) = get::(arg_matches, "max_redirects") { options.push(HurlOption::new("max-redirs", value.to_string().as_str())); } if let Some(value) = get::(arg_matches, "retry") { options.push(HurlOption::new("retry", value.to_string().as_str())); } options } fn has_content_type(headers: &Vec) -> bool { for header in headers { if header.starts_with("Content-Type") { return true; } } false } fn has_flag(matches: &ArgMatches, name: &str) -> bool { matches.get_one::(name) == Some(&true) } /// Returns an optional value of type `T` from the command line `matches` given the option `name`. fn get(matches: &ArgMatches, name: &str) -> Option { matches.get_one::(name).cloned() } fn get_string(matches: &ArgMatches, name: &str) -> Option { matches.get_one::(name).map(|x| x.to_string()) } /// Returns an optional list of `String` from the command line `matches` given the option `name`. fn get_strings(matches: &ArgMatches, name: &str) -> Option> { matches .get_many::(name) .map(|v| v.map(|x| x.to_string()).collect()) } hurlfmt-6.1.1/src/curl/mod.rs000064400000000000000000000205771046102023000142060ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use std::fmt; mod args; mod commands; mod matches; #[derive(Clone, Debug, PartialEq, Eq)] pub struct HurlOption { name: String, value: String, } impl HurlOption { pub fn new(name: &str, value: &str) -> HurlOption { HurlOption { name: name.to_string(), value: value.to_string(), } } } impl fmt::Display for HurlOption { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}: {}", self.name, self.value) } } pub fn parse(s: &str) -> Result { let cleaned_s = s.replace("\\\n", "").replace("\\\r\n", ""); let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n") .unwrap() .split(&cleaned_s) .filter(|s| !s.is_empty()) .collect(); let mut s = String::new(); for (i, line) in lines.iter().enumerate() { let hurl_str = parse_line(line).map_err(|message| { format!("Can not parse curl command at line {}: {message}", i + 1) })?; s.push_str(format!("{hurl_str}\n").as_str()); } Ok(s) } fn parse_line(s: &str) -> Result { let mut command = clap::Command::new("curl") .arg(commands::compressed()) .arg(commands::data()) .arg(commands::headers()) .arg(commands::insecure()) .arg(commands::location()) .arg(commands::max_redirects()) .arg(commands::method()) .arg(commands::retry()) .arg(commands::url()) .arg(commands::url_param()); let params = args::split(s)?; let arg_matches = match command.try_get_matches_from_mut(params) { Ok(r) => r, Err(e) => return Err(e.to_string()), }; let method = matches::method(&arg_matches); let url = matches::url(&arg_matches); let headers = matches::headers(&arg_matches); let options = matches::options(&arg_matches); let body = matches::body(&arg_matches); let s = format(&method, &url, headers, &options, body); Ok(s) } fn format( method: &str, url: &str, headers: Vec, options: &[HurlOption], body: Option, ) -> String { let mut s = format!("{method} {url}"); for header in headers { if let Some(stripped) = header.strip_suffix(";") { s.push_str(format!("\n{}:", stripped).as_str()); } else { s.push_str(format!("\n{header}").as_str()); } } if !options.is_empty() { s.push_str("\n[Options]"); for option in options { s.push_str(format!("\n{option}").as_str()); } } if let Some(body) = body { s.push('\n'); s.push_str(body.as_str()); } let asserts = additional_asserts(options); if !asserts.is_empty() { s.push_str("\nHTTP *"); s.push_str("\n[Asserts]"); for assert in asserts { s.push_str(format!("\n{assert}").as_str()); } } s.push('\n'); s } fn has_option(options: &[HurlOption], name: &str) -> bool { for option in options { if option.name == name { return true; } } false } fn additional_asserts(options: &[HurlOption]) -> Vec { let mut asserts = vec![]; if has_option(options, "retry") { asserts.push("status < 500".to_string()); } asserts } #[cfg(test)] mod test { use crate::curl::*; #[test] fn test_parse() { let hurl_str = r#"GET http://localhost:8000/hello GET http://localhost:8000/custom-headers Fruit:Raspberry "#; assert_eq!( parse( r#"curl http://localhost:8000/hello curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' "# ) .unwrap(), hurl_str ); } #[test] fn test_parse_with_escape() { let hurl_str = r#"GET http://localhost:8000/custom_headers Fruit:Raspberry Fruit:Banana "#; assert_eq!( parse( r#"curl http://localhost:8000/custom_headers \ -H 'Fruit:Raspberry' \ -H 'Fruit:Banana' "#, ) .unwrap(), hurl_str ); } #[test] fn test_hello() { let hurl_str = r#"GET http://localhost:8000/hello "#; assert_eq!( parse_line("curl http://localhost:8000/hello").unwrap(), hurl_str ); } #[test] fn test_headers() { let hurl_str = r#"GET http://localhost:8000/custom-headers Fruit:Raspberry Fruit: Banana Test: ' "#; assert_eq!( parse_line("curl http://localhost:8000/custom-headers -H 'Fruit:Raspberry' -H 'Fruit: Banana' -H $'Test: \\''").unwrap(), hurl_str ); assert_eq!( parse_line("curl http://localhost:8000/custom-headers --header Fruit:Raspberry -H 'Fruit: Banana' -H $'Test: \\'' ").unwrap(), hurl_str ); } #[test] fn test_empty_headers() { let hurl_str = r#"GET http://localhost:8000/empty-headers Empty-Header: "#; assert_eq!( parse_line("curl http://localhost:8000/empty-headers -H 'Empty-Header;'").unwrap(), hurl_str ); } #[test] fn test_post_hello() { let hurl_str = r#"POST http://localhost:8000/hello Content-Type: text/plain ``` hello ``` "#; assert_eq!( parse_line(r#"curl -d $'hello' -H 'Content-Type: text/plain' -X POST http://localhost:8000/hello"#).unwrap(), hurl_str ); } #[test] fn test_post_format_params() { let hurl_str = r#"POST http://localhost:3000/data Content-Type: application/x-www-form-urlencoded ``` param1=value1¶m2=value2 ``` "#; assert_eq!( parse_line("curl http://localhost:3000/data -d 'param1=value1¶m2=value2'").unwrap(), hurl_str ); assert_eq!( parse_line("curl -X POST http://localhost:3000/data -H 'Content-Type: application/x-www-form-urlencoded' --data 'param1=value1¶m2=value2'").unwrap(), hurl_str ); } #[test] fn test_post_json() { let hurl_str = r#"POST http://localhost:3000/data Content-Type: application/json ``` {"key1":"value1", "key2":"value2"} ``` "#; assert_eq!( hurl_str, parse_line(r#"curl -d '{"key1":"value1", "key2":"value2"}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap() ); let hurl_str = r#"POST http://localhost:3000/data Content-Type: application/json ``` { "key1": "value1", "key2": "value2" } ``` "#; assert_eq!( parse_line(r#"curl -d $'{\n "key1": "value1",\n "key2": "value2"\n}' -H 'Content-Type: application/json' -X POST http://localhost:3000/data"#).unwrap(), hurl_str ); } #[test] fn test_post_file() { let hurl_str = r#"POST http://example.com/ file, filename; "#; assert_eq!( parse_line(r#"curl --data @filename http://example.com/"#).unwrap(), hurl_str ); } #[test] fn test_redirect() { let hurl_str = r#"GET http://localhost:8000/redirect-absolute [Options] location: true "#; assert_eq!( parse_line(r#"curl -L http://localhost:8000/redirect-absolute"#).unwrap(), hurl_str ); } #[test] fn test_insecure() { let hurl_str = r#"GET https://localhost:8001/hello [Options] insecure: true "#; assert_eq!( parse_line(r#"curl -k https://localhost:8001/hello"#).unwrap(), hurl_str ); } #[test] fn test_max_redirects() { let hurl_str = r#"GET https://localhost:8001/hello [Options] max-redirs: 10 "#; assert_eq!( parse_line(r#"curl https://localhost:8001/hello --max-redirs 10"#).unwrap(), hurl_str ); } } hurlfmt-6.1.1/src/format/json.rs000064400000000000000000001053461046102023000147210ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use base64::engine::general_purpose; use base64::Engine; use hurl_core::ast::{ Assert, Base64, Body, BooleanOption, Bytes, Capture, CertificateAttributeName, Comment, Cookie, CountOption, DurationOption, Entry, EntryOption, File, FileParam, Filter, FilterValue, Hex, HurlFile, JsonListElement, JsonValue, KeyValue, MultilineString, MultilineStringKind, MultipartParam, NaturalOption, OptionKind, Placeholder, Predicate, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex, RegexValue, Request, Response, StatusValue, VersionValue, }; use hurl_core::typing::{Count, Duration, ToSource}; use crate::format::serialize_json::JValue; pub fn format(hurl_file: &HurlFile) -> String { hurl_file.to_json().format() } pub trait ToJson { fn to_json(&self) -> JValue; } impl ToJson for HurlFile { fn to_json(&self) -> JValue { JValue::Object(vec![( "entries".to_string(), JValue::List(self.entries.iter().map(|e| e.to_json()).collect()), )]) } } impl ToJson for Entry { fn to_json(&self) -> JValue { let mut attributes = vec![("request".to_string(), self.request.to_json())]; if let Some(response) = &self.response { attributes.push(("response".to_string(), response.to_json())); } JValue::Object(attributes) } } impl ToJson for Request { fn to_json(&self) -> JValue { let mut attributes = vec![ ( "method".to_string(), JValue::String(self.method.to_string()), ), ("url".to_string(), JValue::String(self.url.to_string())), ]; add_headers(&mut attributes, &self.headers); if !self.querystring_params().is_empty() { let params = self .querystring_params() .iter() .map(|p| p.to_json()) .collect(); attributes.push(("query_string_params".to_string(), JValue::List(params))); } if !self.form_params().is_empty() { let params = self.form_params().iter().map(|p| p.to_json()).collect(); attributes.push(("form_params".to_string(), JValue::List(params))); } if !self.multipart_form_data().is_empty() { let params = self .multipart_form_data() .iter() .map(|p| p.to_json()) .collect(); attributes.push(("multipart_form_data".to_string(), JValue::List(params))); } if !self.cookies().is_empty() { let cookies = self.cookies().iter().map(|c| c.to_json()).collect(); attributes.push(("cookies".to_string(), JValue::List(cookies))); } if !self.options().is_empty() { let options = self.options().iter().map(|c| c.to_json()).collect(); attributes.push(("options".to_string(), JValue::List(options))); } if let Some(body) = &self.body { attributes.push(("body".to_string(), body.to_json())); } // Request comments (can be used to check custom commands) let comments: Vec<_> = self .line_terminators .iter() .filter_map(|l| l.comment.as_ref()) .collect(); if !comments.is_empty() { let comments = comments.iter().map(|c| c.to_json()).collect(); attributes.push(("comments".to_string(), JValue::List(comments))); } JValue::Object(attributes) } } impl ToJson for Response { /// Transforms this response to a JSON object. fn to_json(&self) -> JValue { let mut attributes = vec![]; if let Some(v) = get_json_version(&self.version.value) { attributes.push(("version".to_string(), JValue::String(v))); } if let StatusValue::Specific(n) = self.status.value { attributes.push(("status".to_string(), JValue::Number(n.to_string()))); } add_headers(&mut attributes, &self.headers); if !self.captures().is_empty() { let captures = self.captures().iter().map(|c| c.to_json()).collect(); attributes.push(("captures".to_string(), JValue::List(captures))); } if !self.asserts().is_empty() { let asserts = self.asserts().iter().map(|a| a.to_json()).collect(); attributes.push(("asserts".to_string(), JValue::List(asserts))); } if let Some(body) = &self.body { attributes.push(("body".to_string(), body.to_json())); } JValue::Object(attributes) } } fn add_headers(attributes: &mut Vec<(String, JValue)>, headers: &[KeyValue]) { if !headers.is_empty() { let headers = JValue::List(headers.iter().map(|h| h.to_json()).collect()); attributes.push(("headers".to_string(), headers)); } } impl ToJson for Body { fn to_json(&self) -> JValue { self.value.to_json() } } impl ToJson for Bytes { fn to_json(&self) -> JValue { match self { Bytes::Base64(value) => value.to_json(), Bytes::Hex(value) => value.to_json(), Bytes::File(value) => value.to_json(), Bytes::Json(value) => JValue::Object(vec![ ("type".to_string(), JValue::String("json".to_string())), ("value".to_string(), value.to_json()), ]), Bytes::Xml(value) => JValue::Object(vec![ ("type".to_string(), JValue::String("xml".to_string())), ("value".to_string(), JValue::String(value.clone())), ]), Bytes::OnelineString(value) => JValue::Object(vec![ ("type".to_string(), JValue::String("text".to_string())), ("value".to_string(), JValue::String(value.to_string())), ]), Bytes::MultilineString(multi) => { // TODO: check these values. Maybe we want to have the same // export when using: // // ~~~ // GET https://foo.com // ```base64 // SGVsbG8gd29ybGQ= // ``` // // or // // ~~~ // GET https://foo.com // base64,SGVsbG8gd29ybGQ=; // ~~~ let lang = match multi { MultilineString { kind: MultilineStringKind::Text(_), .. } => "text", MultilineString { kind: MultilineStringKind::Json(_), .. } => "json", MultilineString { kind: MultilineStringKind::Xml(_), .. } => "xml", MultilineString { kind: MultilineStringKind::GraphQl(_), .. } => "graphql", }; JValue::Object(vec![ ("type".to_string(), JValue::String(lang.to_string())), ("value".to_string(), JValue::String(multi.to_string())), ]) } } } } impl ToJson for Base64 { fn to_json(&self) -> JValue { let value = general_purpose::STANDARD.encode(&self.value); JValue::Object(vec![ ("encoding".to_string(), JValue::String("base64".to_string())), ("value".to_string(), JValue::String(value)), ]) } } impl ToJson for Hex { fn to_json(&self) -> JValue { let value = general_purpose::STANDARD.encode(&self.value); JValue::Object(vec![ ("encoding".to_string(), JValue::String("base64".to_string())), ("value".to_string(), JValue::String(value)), ]) } } impl ToJson for File { fn to_json(&self) -> JValue { JValue::Object(vec![ ("type".to_string(), JValue::String("file".to_string())), ( "filename".to_string(), JValue::String(self.filename.to_string()), ), ]) } } fn get_json_version(version_value: &VersionValue) -> Option { match version_value { VersionValue::Version1 => Some("HTTP/1.0".to_string()), VersionValue::Version11 => Some("HTTP/1.1".to_string()), VersionValue::Version2 => Some("HTTP/2".to_string()), VersionValue::Version3 => Some("HTTP/3".to_string()), VersionValue::VersionAny => None, } } impl ToJson for KeyValue { fn to_json(&self) -> JValue { let attributes = vec![ ("name".to_string(), JValue::String(self.key.to_string())), ("value".to_string(), JValue::String(self.value.to_string())), ]; JValue::Object(attributes) } } impl ToJson for MultipartParam { fn to_json(&self) -> JValue { match self { MultipartParam::Param(param) => param.to_json(), MultipartParam::FileParam(param) => param.to_json(), } } } impl ToJson for FileParam { fn to_json(&self) -> JValue { let mut attributes = vec![ ("name".to_string(), JValue::String(self.key.to_string())), ( "filename".to_string(), JValue::String(self.value.filename.to_string()), ), ]; if let Some(content_type) = &self.value.content_type { attributes.push(( "content_type".to_string(), JValue::String(content_type.to_string()), )); } JValue::Object(attributes) } } impl ToJson for Cookie { fn to_json(&self) -> JValue { let attributes = vec![ ("name".to_string(), JValue::String(self.name.to_string())), ("value".to_string(), JValue::String(self.value.to_string())), ]; JValue::Object(attributes) } } impl ToJson for EntryOption { fn to_json(&self) -> JValue { let value = match &self.kind { OptionKind::AwsSigV4(value) => JValue::String(value.to_string()), OptionKind::CaCertificate(filename) => JValue::String(filename.to_string()), OptionKind::ClientCert(filename) => JValue::String(filename.to_string()), OptionKind::ClientKey(filename) => JValue::String(filename.to_string()), OptionKind::Compressed(value) => value.to_json(), OptionKind::ConnectTo(value) => JValue::String(value.to_string()), OptionKind::ConnectTimeout(value) => value.to_json(), OptionKind::Delay(value) => value.to_json(), OptionKind::FollowLocation(value) => value.to_json(), OptionKind::FollowLocationTrusted(value) => value.to_json(), OptionKind::Header(value) => JValue::String(value.to_string()), OptionKind::Http10(value) => value.to_json(), OptionKind::Http11(value) => value.to_json(), OptionKind::Http2(value) => value.to_json(), OptionKind::Http3(value) => value.to_json(), OptionKind::Insecure(value) => value.to_json(), OptionKind::IpV4(value) => value.to_json(), OptionKind::IpV6(value) => value.to_json(), OptionKind::LimitRate(value) => value.to_json(), OptionKind::MaxRedirect(value) => value.to_json(), OptionKind::NetRc(value) => value.to_json(), OptionKind::NetRcFile(filename) => JValue::String(filename.to_string()), OptionKind::NetRcOptional(value) => value.to_json(), OptionKind::Output(filename) => JValue::String(filename.to_string()), OptionKind::PathAsIs(value) => value.to_json(), OptionKind::Proxy(value) => JValue::String(value.to_string()), OptionKind::Repeat(value) => value.to_json(), OptionKind::Resolve(value) => JValue::String(value.to_string()), OptionKind::Retry(value) => value.to_json(), OptionKind::RetryInterval(value) => value.to_json(), OptionKind::Skip(value) => value.to_json(), OptionKind::UnixSocket(value) => JValue::String(value.to_string()), OptionKind::User(value) => JValue::String(value.to_string()), OptionKind::Variable(value) => { JValue::String(format!("{}={}", value.name, value.value.to_source())) } OptionKind::Verbose(value) => value.to_json(), OptionKind::VeryVerbose(value) => value.to_json(), }; // If the value contains the unit such as `{ "value": 10, "unit": "second" }` // The JSON for this option should still have one level // for example: { "name": "delay", "value": 10, "unit", "second" } let attributes = if let JValue::Object(mut attributes) = value { attributes.push(( "name".to_string(), JValue::String(self.kind.identifier().to_string()), )); attributes } else { vec![ ( "name".to_string(), JValue::String(self.kind.identifier().to_string()), ), ("value".to_string(), value), ] }; JValue::Object(attributes) } } impl ToJson for BooleanOption { fn to_json(&self) -> JValue { match self { BooleanOption::Literal(value) => JValue::Boolean(*value), BooleanOption::Placeholder(placeholder) => placeholder.to_json(), } } } impl ToJson for CountOption { fn to_json(&self) -> JValue { match self { CountOption::Literal(value) => value.to_json(), CountOption::Placeholder(placeholder) => placeholder.to_json(), } } } impl ToJson for Count { fn to_json(&self) -> JValue { match self { Count::Finite(n) => JValue::Number(n.to_string()), Count::Infinite => JValue::Number("-1".to_string()), } } } impl ToJson for DurationOption { fn to_json(&self) -> JValue { match self { DurationOption::Literal(value) => value.to_json(), DurationOption::Placeholder(placeholder) => placeholder.to_json(), } } } impl ToJson for Duration { fn to_json(&self) -> JValue { if let Some(unit) = self.unit { let mut attributes = vec![("value".to_string(), JValue::Number(self.value.to_string()))]; attributes.push(("unit".to_string(), JValue::String(unit.to_string()))); JValue::Object(attributes) } else { JValue::Number(self.value.to_string()) } } } impl ToJson for Capture { fn to_json(&self) -> JValue { let mut attributes = vec![ ("name".to_string(), JValue::String(self.name.to_string())), ("query".to_string(), self.query.to_json()), ]; if !self.filters.is_empty() { let filters = JValue::List(self.filters.iter().map(|(_, f)| f.to_json()).collect()); attributes.push(("filters".to_string(), filters)); } if self.redact { attributes.push(("redact".to_string(), JValue::Boolean(true))); } JValue::Object(attributes) } } impl ToJson for Assert { fn to_json(&self) -> JValue { let mut attributes = vec![("query".to_string(), self.query.to_json())]; if !self.filters.is_empty() { let filters = JValue::List(self.filters.iter().map(|(_, f)| f.to_json()).collect()); attributes.push(("filters".to_string(), filters)); } attributes.push(("predicate".to_string(), self.predicate.to_json())); JValue::Object(attributes) } } impl ToJson for Query { fn to_json(&self) -> JValue { let attributes = query_value_attributes(&self.value); JValue::Object(attributes) } } fn query_value_attributes(query_value: &QueryValue) -> Vec<(String, JValue)> { let mut attributes = vec![]; let att_type = JValue::String(query_value.identifier().to_string()); attributes.push(("type".to_string(), att_type)); match query_value { QueryValue::Jsonpath { expr, .. } => { attributes.push(("expr".to_string(), JValue::String(expr.to_string()))); } QueryValue::Header { name, .. } => { attributes.push(("name".to_string(), JValue::String(name.to_string()))); } QueryValue::Cookie { expr, .. } => { attributes.push(("expr".to_string(), JValue::String(expr.to_string()))); } QueryValue::Xpath { expr, .. } => { attributes.push(("expr".to_string(), JValue::String(expr.to_string()))); } QueryValue::Regex { value, .. } => { attributes.push(("expr".to_string(), value.to_json())); } QueryValue::Variable { name, .. } => { attributes.push(("name".to_string(), JValue::String(name.to_string()))); } QueryValue::Certificate { attribute_name: field, .. } => { attributes.push(("expr".to_string(), field.to_json())); } _ => {} }; attributes } impl ToJson for RegexValue { fn to_json(&self) -> JValue { match self { RegexValue::Template(template) => JValue::String(template.to_string()), RegexValue::Regex(regex) => regex.to_json(), } } } impl ToJson for Regex { fn to_json(&self) -> JValue { let attributes = vec![ ("type".to_string(), JValue::String("regex".to_string())), ("value".to_string(), JValue::String(self.to_string())), ]; JValue::Object(attributes) } } impl ToJson for CertificateAttributeName { fn to_json(&self) -> JValue { JValue::String(self.identifier().to_string()) } } impl ToJson for Predicate { fn to_json(&self) -> JValue { let mut attributes = vec![]; if self.not { attributes.push(("not".to_string(), JValue::Boolean(true))); } let identifier = self.predicate_func.value.identifier(); attributes.push(("type".to_string(), JValue::String(identifier.to_string()))); match &self.predicate_func.value { PredicateFuncValue::Equal { value, .. } => add_predicate_value(&mut attributes, value), PredicateFuncValue::NotEqual { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::GreaterThan { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::GreaterThanOrEqual { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::LessThan { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::LessThanOrEqual { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::StartWith { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::EndWith { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::Contain { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::Include { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::Match { value, .. } => { add_predicate_value(&mut attributes, value); } PredicateFuncValue::IsInteger | PredicateFuncValue::IsFloat | PredicateFuncValue::IsBoolean | PredicateFuncValue::IsString | PredicateFuncValue::IsCollection | PredicateFuncValue::IsDate | PredicateFuncValue::IsIsoDate | PredicateFuncValue::Exist | PredicateFuncValue::IsEmpty | PredicateFuncValue::IsNumber | PredicateFuncValue::IsIpv4 | PredicateFuncValue::IsIpv6 => {} } JValue::Object(attributes) } } fn add_predicate_value(attributes: &mut Vec<(String, JValue)>, predicate_value: &PredicateValue) { let (value, encoding) = json_predicate_value(predicate_value); attributes.push(("value".to_string(), value)); if let Some(encoding) = encoding { attributes.push(("encoding".to_string(), JValue::String(encoding))); } } fn json_predicate_value(predicate_value: &PredicateValue) -> (JValue, Option) { match predicate_value { PredicateValue::String(value) => (JValue::String(value.to_string()), None), PredicateValue::MultilineString(value) => (JValue::String(value.value().to_string()), None), PredicateValue::Bool(value) => (JValue::Boolean(*value), None), PredicateValue::Null => (JValue::Null, None), PredicateValue::Number(value) => (JValue::Number(value.to_string()), None), PredicateValue::File(value) => (value.to_json(), None), PredicateValue::Hex(value) => { let base64_string = general_purpose::STANDARD.encode(value.value.clone()); (JValue::String(base64_string), Some("base64".to_string())) } PredicateValue::Base64(value) => { let base64_string = general_purpose::STANDARD.encode(value.value.clone()); (JValue::String(base64_string), Some("base64".to_string())) } PredicateValue::Placeholder(value) => (JValue::String(value.to_string()), None), PredicateValue::Regex(value) => { (JValue::String(value.to_string()), Some("regex".to_string())) } } } impl ToJson for JsonValue { fn to_json(&self) -> JValue { match self { JsonValue::Null => JValue::Null, JsonValue::Number(s) => JValue::Number(s.to_string()), JsonValue::String(s) => JValue::String(s.to_string()), JsonValue::Boolean(v) => JValue::Boolean(*v), JsonValue::List { elements, .. } => { JValue::List(elements.iter().map(|e| e.to_json()).collect()) } JsonValue::Object { elements, .. } => JValue::Object( elements .iter() .map(|elem| (elem.name.to_string(), elem.value.to_json())) .collect(), ), JsonValue::Placeholder(exp) => JValue::String(format!("{{{{{exp}}}}}")), } } } impl ToJson for JsonListElement { fn to_json(&self) -> JValue { self.value.to_json() } } impl ToJson for Filter { fn to_json(&self) -> JValue { self.value.to_json() } } impl ToJson for FilterValue { fn to_json(&self) -> JValue { let mut attributes = vec![]; let att_name = "type".to_string(); let att_value = JValue::String(self.identifier().to_string()); attributes.push((att_name, att_value)); match self { FilterValue::Decode { encoding, .. } => { attributes.push(("encoding".to_string(), JValue::String(encoding.to_string()))); } FilterValue::Format { fmt, .. } => { attributes.push(("fmt".to_string(), JValue::String(fmt.to_string()))); } FilterValue::JsonPath { expr, .. } => { attributes.push(("expr".to_string(), JValue::String(expr.to_string()))); } FilterValue::Nth { n, .. } => { attributes.push(("n".to_string(), JValue::Number(n.to_string()))); } FilterValue::Regex { value, .. } => { attributes.push(("expr".to_string(), value.to_json())); } FilterValue::Replace { old_value, new_value, .. } => { attributes.push(("old_value".to_string(), old_value.to_json())); attributes.push(( "new_value".to_string(), JValue::String(new_value.to_string()), )); } FilterValue::Split { sep, .. } => { attributes.push(("sep".to_string(), JValue::String(sep.to_string()))); } FilterValue::ToDate { fmt, .. } => { attributes.push(("fmt".to_string(), JValue::String(fmt.to_string()))); } FilterValue::XPath { expr, .. } => { attributes.push(("expr".to_string(), JValue::String(expr.to_string()))); } _ => {} } JValue::Object(attributes) } } impl ToJson for Placeholder { fn to_json(&self) -> JValue { JValue::String(format!("{{{{{}}}}}", self)) } } impl ToJson for Comment { fn to_json(&self) -> JValue { JValue::String(self.value.to_string()) } } impl ToJson for NaturalOption { fn to_json(&self) -> JValue { match self { NaturalOption::Literal(value) => JValue::Number(value.to_string()), NaturalOption::Placeholder(placeholder) => placeholder.to_json(), } } } #[cfg(test)] pub mod tests { use hurl_core::ast::{ LineTerminator, Method, Number, PredicateFunc, SourceInfo, Status, Template, TemplateElement, Version, Whitespace, I64, }; use hurl_core::reader::Pos; use hurl_core::typing::ToSource; use super::*; fn whitespace() -> Whitespace { Whitespace { value: String::new(), source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } } fn line_terminator() -> LineTerminator { LineTerminator { space0: whitespace(), comment: None, newline: whitespace(), } } #[test] pub fn test_request() { assert_eq!( Request { line_terminators: vec![], space0: whitespace(), method: Method::new("GET"), space1: whitespace(), url: Template::new( None, vec![TemplateElement::String { value: "http://example.com".to_string(), source: "not_used".to_source(), }], SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), ), line_terminator0: line_terminator(), headers: vec![KeyValue { line_terminators: vec![], space0: whitespace(), key: Template::new( None, vec![TemplateElement::String { value: "Foo".to_string(), source: "unused".to_source(), }], SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)) ), space1: whitespace(), space2: whitespace(), value: Template::new( None, vec![TemplateElement::String { value: "Bar".to_string(), source: "unused".to_source(), }], SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)) ), line_terminator0: line_terminator(), }], sections: vec![], body: None, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } .to_json(), JValue::Object(vec![ ("method".to_string(), JValue::String("GET".to_string())), ( "url".to_string(), JValue::String("http://example.com".to_string()) ), ( "headers".to_string(), JValue::List(vec![JValue::Object(vec![ ("name".to_string(), JValue::String("Foo".to_string())), ("value".to_string(), JValue::String("Bar".to_string())) ])]) ) ]) ); } #[test] pub fn test_response() { assert_eq!( Response { line_terminators: vec![], version: Version { value: VersionValue::Version11, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }, space0: whitespace(), status: Status { value: StatusValue::Specific(200), source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }, space1: whitespace(), line_terminator0: line_terminator(), headers: vec![], sections: vec![], body: None, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } .to_json(), JValue::Object(vec![ ( "version".to_string(), JValue::String("HTTP/1.1".to_string()) ), ("status".to_string(), JValue::Number("200".to_string())) ]) ); assert_eq!( Response { line_terminators: vec![], version: Version { value: VersionValue::VersionAny, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }, space0: whitespace(), status: Status { value: StatusValue::Any, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }, space1: whitespace(), line_terminator0: line_terminator(), headers: vec![], sections: vec![], body: None, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } .to_json(), JValue::Object(vec![]) ); } fn header_query() -> Query { Query { source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), value: QueryValue::Header { space0: whitespace(), name: Template::new( None, vec![TemplateElement::String { value: "Content-Length".to_string(), source: "Content-Length".to_source(), }], SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), ), }, } } fn header_capture() -> Capture { Capture { line_terminators: vec![], space0: whitespace(), name: Template::new( None, vec![TemplateElement::String { value: "size".to_string(), source: "unused".to_source(), }], SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), ), space1: whitespace(), space2: whitespace(), query: header_query(), filters: vec![], space3: whitespace(), redact: false, line_terminator0: line_terminator(), } } fn header_assert() -> Assert { Assert { line_terminators: vec![], space0: whitespace(), query: header_query(), filters: vec![], space1: whitespace(), predicate: equal_int_predicate(10), line_terminator0: line_terminator(), } } fn equal_int_predicate(value: i64) -> Predicate { Predicate { not: false, space0: whitespace(), predicate_func: PredicateFunc { source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), value: PredicateFuncValue::Equal { space0: whitespace(), value: PredicateValue::Number(Number::Integer(I64::new( value, value.to_string().to_source(), ))), }, }, } } #[test] pub fn test_query() { assert_eq!( header_query().to_json(), JValue::Object(vec![ ("type".to_string(), JValue::String("header".to_string())), ( "name".to_string(), JValue::String("Content-Length".to_string()) ), ]) ); } #[test] pub fn test_capture() { assert_eq!( header_capture().to_json(), JValue::Object(vec![ ("name".to_string(), JValue::String("size".to_string())), ( "query".to_string(), JValue::Object(vec![ ("type".to_string(), JValue::String("header".to_string())), ( "name".to_string(), JValue::String("Content-Length".to_string()) ), ]) ), ]) ); } #[test] pub fn test_predicate() { assert_eq!( equal_int_predicate(10).to_json(), JValue::Object(vec![ ("type".to_string(), JValue::String("==".to_string())), ("value".to_string(), JValue::Number("10".to_string())) ]), ); } #[test] pub fn test_assert() { assert_eq!( header_assert().to_json(), JValue::Object(vec![ ( "query".to_string(), JValue::Object(vec![ ("type".to_string(), JValue::String("header".to_string())), ( "name".to_string(), JValue::String("Content-Length".to_string()) ), ]) ), ( "predicate".to_string(), JValue::Object(vec![ ("type".to_string(), JValue::String("==".to_string())), ("value".to_string(), JValue::Number("10".to_string())) ]) ) ]), ); } } hurlfmt-6.1.1/src/format/mod.rs000064400000000000000000000014521046102023000145200ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ pub use self::json::format as format_json; pub use self::text::format as format_text; pub use self::token::{Token, Tokenizable}; mod json; mod serialize_json; mod text; mod token; hurlfmt-6.1.1/src/format/serialize_json.rs000064400000000000000000000105271046102023000167640ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ /** * Serde-json can not be easily used for serialization here because of the orphan rule. * It seems easier just to reimplement it from scratch (around 50 lines of code) */ #[allow(dead_code)] #[derive(Clone, Debug, PartialEq, Eq)] pub enum JValue { Number(String), String(String), Boolean(bool), List(Vec), Object(Vec<(String, JValue)>), Null, } impl JValue { pub fn format(&self) -> String { match self { JValue::Null => "null".to_string(), JValue::Number(n) => n.to_string(), JValue::String(s) => format!("\"{}\"", s.chars().map(format_char).collect::()), JValue::Boolean(b) => b.to_string(), JValue::List(elem) => { let s = elem .iter() .map(|e| e.format()) .collect::>() .join(","); format!("[{s}]") } JValue::Object(key_values) => { let s = key_values .iter() .map(|(k, v)| { format!( "\"{}\":{}", k.chars().map(format_char).collect::(), v.format() ) }) .collect::>() .join(","); format!("{{{s}}}") } } } } fn format_char(c: char) -> String { if c == '"' { "\\\"".to_string() } else if c == '\\' { "\\\\".to_string() } else if c == '\x08' { "\\b".to_string() } else if c == '\x0c' { "\\f".to_string() } else if c == '\n' { "\\n".to_string() } else if c == '\r' { "\\r".to_string() } else if c == '\t' { "\\t".to_string() } else if c.is_control() { format!("\\u{:04x}", c as u32) } else { c.to_string() } } #[cfg(test)] pub mod tests { use super::*; #[test] pub fn test_format_char() { assert_eq!(format_char('a'), "a"); assert_eq!(format_char('"'), "\\\""); // \" assert_eq!(format_char('\n'), "\\n"); assert_eq!(format_char('\x07'), "\\u0007"); } #[test] pub fn format_scalars() { assert_eq!(JValue::Null.format(), "null"); assert_eq!(JValue::Number("1.0".to_string()).format(), "1.0"); assert_eq!(JValue::String("hello".to_string()).format(), "\"hello\""); assert_eq!(JValue::Boolean(true).format(), "true"); } #[test] pub fn format_string() { assert_eq!(JValue::String("hello".to_string()).format(), "\"hello\""); assert_eq!(JValue::String("\"".to_string()).format(), r#""\"""#); } #[test] pub fn format_list() { assert_eq!( JValue::List(vec![ JValue::Number("1".to_string()), JValue::Number("2".to_string()), JValue::Number("3".to_string()) ]) .format(), "[1,2,3]" ); } #[test] pub fn test_format_special_characters() { let value = JValue::Object(vec![( "sp\"ecial\\key".to_string(), JValue::String("sp\nvalue\twith\x08control".to_string()), )]); assert_eq!( value.format(), r#"{"sp\"ecial\\key":"sp\nvalue\twith\bcontrol"}"# ); } #[test] pub fn format_object() { assert_eq!( JValue::Object(vec![ ("name".to_string(), JValue::String("Bob".to_string())), ("age".to_string(), JValue::Number("20".to_string())), ]) .format(), r#"{"name":"Bob","age":20}"# ); } } hurlfmt-6.1.1/src/format/text.rs000064400000000000000000000073351046102023000147330ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::ast::HurlFile; use hurl_core::text::{Format, Style, StyledString}; use crate::format::{Token, Tokenizable}; pub fn format(hurl_file: &HurlFile, color: bool) -> String { let mut buffer = String::new(); for token in &hurl_file.tokenize() { buffer.push_str(format_token(token, color).as_str()); } buffer } pub fn format_token(token: &Token, color: bool) -> String { let format = if color { Format::Ansi } else { Format::Plain }; match token { Token::Whitespace(value) => value.clone(), Token::Method(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().yellow()); s.to_string(format) } Token::Version(value) => value.clone(), Token::Status(value) => value.clone(), Token::SectionHeader(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().magenta()); s.to_string(format) } Token::Comment(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().bright_black()); s.to_string(format) } Token::Value(value) => value.clone(), Token::Colon(value) => value.clone(), Token::QueryType(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().cyan()); s.to_string(format) } Token::PredicateType(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().yellow()); s.to_string(format) } Token::Not(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().yellow()); s.to_string(format) } Token::Boolean(value) | Token::Number(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().cyan()); s.to_string(format) } Token::String(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().green()); s.to_string(format) } Token::StringDelimiter(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().green()); s.to_string(format) } Token::CodeDelimiter(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().green()); s.to_string(format) } Token::CodeVariable(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().green()); s.to_string(format) } Token::Keyword(value) => value.clone(), Token::FilterType(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().cyan()); s.to_string(format) } Token::Lang(value) => value.clone(), Token::Unit(value) => { let mut s = StyledString::new(); s.push_with(value, Style::new().cyan()); s.to_string(format) } } } hurlfmt-6.1.1/src/format/token.rs000064400000000000000000001131321046102023000150600ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::ast::{ Assert, Base64, Body, BooleanOption, Bytes, Capture, CertificateAttributeName, Comment, Cookie, CookieAttribute, CookiePath, CountOption, DurationOption, Entry, EntryOption, Expr, ExprKind, File, FileParam, FileValue, Filter, FilterValue, Function, GraphQl, GraphQlVariables, Hex, HurlFile, JsonListElement, JsonObjectElement, JsonValue, KeyValue, LineTerminator, Method, MultilineString, MultilineStringAttribute, MultilineStringKind, MultipartParam, NaturalOption, OptionKind, Placeholder, Predicate, PredicateFunc, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex, RegexValue, Request, Response, Section, SectionValue, Status, StatusValue, Template, TemplateElement, Variable, VariableDefinition, VariableValue, Version, Whitespace, I64, U64, }; use hurl_core::typing::{Count, Duration, ToSource}; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Token { Method(String), Version(String), Status(String), SectionHeader(String), QueryType(String), PredicateType(String), FilterType(String), Not(String), Keyword(String), // Primitives Whitespace(String), Comment(String), Value(String), Colon(String), StringDelimiter(String), Boolean(String), Number(String), String(String), CodeDelimiter(String), CodeVariable(String), Lang(String), Unit(String), } pub trait Tokenizable { fn tokenize(&self) -> Vec; } impl Tokenizable for HurlFile { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append(&mut self.entries.iter().flat_map(|e| e.tokenize()).collect()); tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens } } impl Tokenizable for Entry { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append(&mut self.request.tokenize()); if let Some(response) = &self.response { tokens.append(&mut response.tokenize()); } tokens } } impl Tokenizable for Request { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.method.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.append(&mut self.url.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens.append(&mut self.headers.iter().flat_map(|e| e.tokenize()).collect()); tokens.append(&mut self.sections.iter().flat_map(|e| e.tokenize()).collect()); if let Some(body) = &self.body { tokens.append(&mut body.tokenize()); } tokens } } impl Tokenizable for Method { fn tokenize(&self) -> Vec { vec![Token::Method(self.to_string())] } } impl Tokenizable for Response { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.version.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.append(&mut self.status.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens.append(&mut self.headers.iter().flat_map(|e| e.tokenize()).collect()); tokens.append(&mut self.sections.iter().flat_map(|e| e.tokenize()).collect()); if let Some(body) = self.clone().body { tokens.append(&mut body.tokenize()); } tokens } } impl Tokenizable for Status { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; match self.value.clone() { StatusValue::Any => tokens.push(Token::Status("*".to_string())), StatusValue::Specific(v) => tokens.push(Token::Status(v.to_string())), } tokens } } impl Tokenizable for Version { fn tokenize(&self) -> Vec { vec![Token::Version(self.value.to_string())] } } impl Tokenizable for Body { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.value.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for Bytes { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; match self { Bytes::Json(value) => tokens.append(&mut value.tokenize()), Bytes::Xml(value) => tokens.push(Token::String(value.to_string())), Bytes::MultilineString(value) => tokens.append(&mut value.tokenize()), Bytes::OnelineString(value) => tokens.append(&mut value.tokenize()), Bytes::Base64(value) => tokens.append(&mut value.tokenize()), Bytes::Hex(value) => tokens.append(&mut value.tokenize()), Bytes::File(value) => tokens.append(&mut value.tokenize()), } tokens } } impl Tokenizable for Section { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.push(Token::SectionHeader(format!("[{}]", self.identifier()))); tokens.append(&mut self.line_terminator0.tokenize()); tokens.append(&mut self.value.tokenize()); tokens } } impl Tokenizable for SectionValue { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; match self { SectionValue::Asserts(items) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } SectionValue::QueryParams(items, _) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } SectionValue::BasicAuth(item) => { if let Some(kv) = item { tokens.append(&mut kv.tokenize()); } } SectionValue::FormParams(items, _) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } SectionValue::MultipartFormData(items, _) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } SectionValue::Cookies(items) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } SectionValue::Captures(items) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } SectionValue::Options(items) => { tokens.append(&mut items.iter().flat_map(|e| e.tokenize()).collect()); } } tokens } } impl Tokenizable for Base64 { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::Keyword(String::from("base64,"))]; tokens.append(&mut self.space0.tokenize()); tokens.push(Token::String(self.source.to_string())); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Keyword(String::from(";"))); tokens } } impl Tokenizable for Hex { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::Keyword(String::from("hex,"))]; tokens.append(&mut self.space0.tokenize()); tokens.push(Token::String(self.source.to_string())); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Keyword(String::from(";"))); tokens } } impl Tokenizable for File { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::Keyword(String::from("file,"))]; tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.filename.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Keyword(String::from(";"))); tokens } } impl Tokenizable for KeyValue { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.key.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Colon(String::from(":"))); tokens.append(&mut self.space2.tokenize()); tokens.append(&mut self.value.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for MultipartParam { fn tokenize(&self) -> Vec { match self { MultipartParam::Param(key_value) => key_value.tokenize(), MultipartParam::FileParam(file_param) => file_param.tokenize(), } } } impl Tokenizable for FileParam { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.key.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Colon(String::from(":"))); tokens.append(&mut self.space2.tokenize()); tokens.append(&mut self.value.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for FileValue { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::Keyword("file,".to_string())]; tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.filename.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Keyword(";".to_string())); tokens.append(&mut self.space2.tokenize()); if let Some(content_type) = &self.content_type { tokens.push(Token::String(content_type.to_string())); } tokens } } impl Tokenizable for Cookie { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.name.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Colon(String::from(":"))); tokens.append(&mut self.space2.tokenize()); tokens.append(&mut self.value.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for Capture { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.name.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Colon(String::from(":"))); tokens.append(&mut self.space2.tokenize()); tokens.append(&mut self.query.tokenize()); for (space, filter) in &self.filters { tokens.append(&mut space.tokenize()); tokens.append(&mut filter.tokenize()); } tokens.append(&mut self.space3.tokenize()); if self.redact { tokens.push(Token::Keyword(String::from("redact"))); } tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for Assert { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.query.tokenize()); for (space, filter) in &self.filters { tokens.append(&mut space.tokenize()); tokens.append(&mut filter.tokenize()); } tokens.append(&mut self.space1.tokenize()); // TODO reconvert back your first predicate for jsonpath // so that you can use your firstX predicate for other query tokens.append(&mut self.predicate.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for Query { fn tokenize(&self) -> Vec { self.value.tokenize() } } impl Tokenizable for QueryValue { fn tokenize(&self) -> Vec { let mut tokens = vec![]; let token = Token::QueryType(self.identifier().to_string()); tokens.push(token); match self { QueryValue::Header { space0, name } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut name.tokenize()); } QueryValue::Cookie { space0, expr } => { tokens.append(&mut space0.tokenize()); tokens.push(Token::CodeDelimiter("\"".to_string())); tokens.append(&mut expr.tokenize()); tokens.push(Token::CodeDelimiter("\"".to_string())); } QueryValue::Xpath { space0, expr } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut expr.tokenize()); } QueryValue::Jsonpath { space0, expr } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut expr.tokenize()); } QueryValue::Regex { space0, value } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } QueryValue::Variable { space0, name } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut name.tokenize()); } QueryValue::Certificate { space0, attribute_name: field, } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut field.tokenize()); } _ => {} } tokens } } impl Tokenizable for RegexValue { fn tokenize(&self) -> Vec { match self { RegexValue::Template(template) => template.tokenize(), RegexValue::Regex(regex) => regex.tokenize(), } } } impl Tokenizable for CookiePath { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append(&mut self.name.tokenize()); if let Some(attribute) = self.attribute.clone() { tokens.append(&mut attribute.tokenize()); } tokens } } impl Tokenizable for CookieAttribute { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::CodeDelimiter("[".to_string())]; tokens.append(&mut self.space0.tokenize()); tokens.push(Token::String(self.name.value())); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::CodeDelimiter("]".to_string())); tokens } } impl Tokenizable for CertificateAttributeName { fn tokenize(&self) -> Vec { vec![ Token::StringDelimiter("\"".to_string()), Token::String(self.identifier().to_string()), Token::StringDelimiter("\"".to_string()), ] } } impl Tokenizable for Predicate { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; if self.not { tokens.push(Token::Not(String::from("not"))); tokens.append(&mut self.space0.tokenize()); } tokens.append(&mut self.predicate_func.tokenize()); tokens } } impl Tokenizable for PredicateFunc { fn tokenize(&self) -> Vec { self.value.tokenize() } } impl Tokenizable for PredicateFuncValue { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; let name = self.identifier().to_string(); match self { PredicateFuncValue::Equal { space0, value, .. } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::NotEqual { space0, value, .. } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::GreaterThan { space0, value, .. } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::GreaterThanOrEqual { space0, value, .. } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::LessThan { space0, value, .. } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::LessThanOrEqual { space0, value, .. } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::StartWith { space0, value } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::EndWith { space0, value } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::Contain { space0, value } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::Include { space0, value } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::Match { space0, value } => { tokens.push(Token::PredicateType(name)); tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } PredicateFuncValue::IsInteger => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsFloat => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsBoolean => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsString => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsCollection => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsDate => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsIsoDate => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::Exist => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsEmpty => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsNumber => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsIpv4 => { tokens.push(Token::PredicateType(name)); } PredicateFuncValue::IsIpv6 => { tokens.push(Token::PredicateType(name)); } } tokens } } impl Tokenizable for PredicateValue { fn tokenize(&self) -> Vec { match self { PredicateValue::String(value) => value.tokenize(), PredicateValue::MultilineString(value) => value.tokenize(), PredicateValue::Bool(value) => vec![Token::Boolean(value.to_string())], PredicateValue::Null => vec![Token::Keyword("null".to_string())], PredicateValue::Number(value) => vec![Token::Number(value.to_source().to_string())], PredicateValue::File(value) => value.tokenize(), PredicateValue::Hex(value) => vec![Token::String(value.to_string())], PredicateValue::Base64(value) => value.tokenize(), PredicateValue::Placeholder(value) => value.tokenize(), PredicateValue::Regex(value) => value.tokenize(), } } } impl Tokenizable for MultilineString { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::StringDelimiter("```".to_string())]; tokens.push(Token::Lang(self.lang().to_string())); for (i, attribute) in self.attributes.iter().enumerate() { if i > 0 || !self.lang().is_empty() { tokens.push(Token::StringDelimiter(",".to_string())); } tokens.append(&mut attribute.tokenize()); } tokens.append(&mut self.space.tokenize()); tokens.append(&mut self.newline.tokenize()); match &self.kind { MultilineStringKind::Text(value) | MultilineStringKind::Json(value) | MultilineStringKind::Xml(value) => tokens.append(&mut value.tokenize()), MultilineStringKind::GraphQl(graphql) => tokens.append(&mut graphql.tokenize()), } tokens.push(Token::StringDelimiter("```".to_string())); tokens } } impl Tokenizable for MultilineStringAttribute { fn tokenize(&self) -> Vec { match self { MultilineStringAttribute::Escape => vec![Token::String("escape".to_string())], MultilineStringAttribute::NoVariable => vec![Token::String("novariable".to_string())], } } } impl Tokenizable for GraphQl { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append(&mut self.value.tokenize()); if let Some(vars) = &self.variables { tokens.append(&mut vars.tokenize()); } tokens } } impl Tokenizable for GraphQlVariables { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.push(Token::String("variables".to_string())); tokens.append(&mut self.space.tokenize()); tokens.append(&mut self.value.tokenize()); tokens.append(&mut self.whitespace.tokenize()); tokens } } impl Tokenizable for Template { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; if let Some(d) = self.delimiter { tokens.push(Token::StringDelimiter(d.to_string())); } for element in &self.elements { tokens.append(&mut element.tokenize()); } if let Some(d) = self.delimiter { tokens.push(Token::StringDelimiter(d.to_string())); } tokens } } impl Tokenizable for TemplateElement { fn tokenize(&self) -> Vec { match self { TemplateElement::String { source, .. } => { vec![Token::String(source.to_string())] } TemplateElement::Placeholder(value) => { let mut tokens: Vec = vec![]; tokens.append(&mut value.tokenize()); tokens } } } } impl Tokenizable for Placeholder { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::CodeDelimiter(String::from("{{"))]; tokens.append(&mut self.space0.tokenize()); tokens.append(&mut self.expr.tokenize()); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::CodeDelimiter(String::from("}}"))); tokens } } impl Tokenizable for Expr { fn tokenize(&self) -> Vec { self.kind.tokenize() } } impl Tokenizable for ExprKind { fn tokenize(&self) -> Vec { match self { ExprKind::Variable(variable) => variable.tokenize(), ExprKind::Function(function) => function.tokenize(), } } } impl Tokenizable for Variable { fn tokenize(&self) -> Vec { vec![Token::CodeVariable(self.name.clone())] } } impl Tokenizable for Function { fn tokenize(&self) -> Vec { match self { Function::NewDate => vec![Token::CodeVariable("newDate".to_string())], Function::NewUuid => vec![Token::CodeVariable("newUuid".to_string())], } } } impl Tokenizable for Regex { fn tokenize(&self) -> Vec { let s = str::replace(self.inner.as_str(), "/", "\\/"); vec![Token::String(format!("/{s}/"))] } } impl Tokenizable for LineTerminator { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append(&mut self.space0.tokenize()); if let Some(comment) = &self.comment { tokens.append(&mut comment.tokenize()); } tokens.append(&mut self.newline.tokenize()); tokens } } impl Tokenizable for Whitespace { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; if !self.value.is_empty() { tokens.push(Token::Whitespace(self.value.clone())); } tokens } } impl Tokenizable for Comment { fn tokenize(&self) -> Vec { vec![Token::Comment(format!("#{}", self.value.clone()))] } } impl Tokenizable for JsonValue { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; match self { JsonValue::String(s) => { //tokens.push(Token::CodeDelimiter("\"".to_string())); tokens.append(&mut s.tokenize()); //tokens.push(Token::CodeDelimiter("\"".to_string())); } JsonValue::Number(value) => { tokens.push(Token::Number(value.to_string())); } JsonValue::Boolean(value) => { tokens.push(Token::Boolean(value.to_string())); } JsonValue::List { space0, elements } => { tokens.push(Token::CodeDelimiter("[".to_string())); tokens.push(Token::Whitespace(space0.clone())); for (i, element) in elements.iter().enumerate() { if i > 0 { tokens.push(Token::CodeDelimiter(",".to_string())); } tokens.append(&mut element.tokenize()); } tokens.push(Token::CodeDelimiter("]".to_string())); } JsonValue::Object { space0, elements } => { tokens.push(Token::CodeDelimiter("{".to_string())); tokens.push(Token::Whitespace(space0.clone())); for (i, element) in elements.iter().enumerate() { if i > 0 { tokens.push(Token::CodeDelimiter(",".to_string())); } tokens.append(&mut element.tokenize()); } tokens.push(Token::CodeDelimiter("}".to_string())); } JsonValue::Null => { tokens.push(Token::Keyword("null".to_string())); } JsonValue::Placeholder(exp) => { tokens.append(&mut exp.tokenize()); } } tokens } } impl Tokenizable for JsonListElement { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::Whitespace(self.space0.clone())]; tokens.append(&mut self.value.tokenize()); tokens.push(Token::Whitespace(self.space1.clone())); tokens } } impl Tokenizable for JsonObjectElement { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::Whitespace(self.space0.clone())]; tokens.push(Token::StringDelimiter("\"".to_string())); tokens.push(Token::String(self.name.to_string())); tokens.push(Token::StringDelimiter("\"".to_string())); tokens.push(Token::Whitespace(self.space1.clone())); tokens.push(Token::CodeDelimiter(":".to_string())); tokens.push(Token::Whitespace(self.space2.clone())); tokens.append(&mut self.value.tokenize()); tokens.push(Token::Whitespace(self.space3.clone())); tokens } } impl Tokenizable for EntryOption { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![]; tokens.append( &mut self .line_terminators .iter() .flat_map(|e| e.tokenize()) .collect(), ); tokens.append(&mut self.space0.tokenize()); tokens.push(Token::String(self.kind.identifier().to_string())); tokens.append(&mut self.space1.tokenize()); tokens.push(Token::Colon(String::from(":"))); tokens.append(&mut self.space2.tokenize()); tokens.append(&mut self.kind.tokenize()); tokens.append(&mut self.line_terminator0.tokenize()); tokens } } impl Tokenizable for OptionKind { fn tokenize(&self) -> Vec { match self { OptionKind::AwsSigV4(value) => value.tokenize(), OptionKind::CaCertificate(filename) => filename.tokenize(), OptionKind::ClientCert(filename) => filename.tokenize(), OptionKind::ClientKey(filename) => filename.tokenize(), OptionKind::Compressed(value) => value.tokenize(), OptionKind::ConnectTo(value) => value.tokenize(), OptionKind::ConnectTimeout(value) => value.tokenize(), OptionKind::Delay(value) => value.tokenize(), OptionKind::FollowLocation(value) => value.tokenize(), OptionKind::FollowLocationTrusted(value) => value.tokenize(), OptionKind::Header(value) => value.tokenize(), OptionKind::Http10(value) => value.tokenize(), OptionKind::Http11(value) => value.tokenize(), OptionKind::Http2(value) => value.tokenize(), OptionKind::Http3(value) => value.tokenize(), OptionKind::Insecure(value) => value.tokenize(), OptionKind::IpV4(value) => value.tokenize(), OptionKind::IpV6(value) => value.tokenize(), OptionKind::LimitRate(value) => value.tokenize(), OptionKind::MaxRedirect(value) => value.tokenize(), OptionKind::NetRc(value) => value.tokenize(), OptionKind::NetRcFile(filename) => filename.tokenize(), OptionKind::NetRcOptional(value) => value.tokenize(), OptionKind::Output(filename) => filename.tokenize(), OptionKind::PathAsIs(value) => value.tokenize(), OptionKind::Proxy(value) => value.tokenize(), OptionKind::Repeat(value) => value.tokenize(), OptionKind::Resolve(value) => value.tokenize(), OptionKind::Retry(value) => value.tokenize(), OptionKind::RetryInterval(value) => value.tokenize(), OptionKind::Skip(value) => value.tokenize(), OptionKind::UnixSocket(value) => value.tokenize(), OptionKind::User(value) => value.tokenize(), OptionKind::Variable(value) => value.tokenize(), OptionKind::Verbose(value) => value.tokenize(), OptionKind::VeryVerbose(value) => value.tokenize(), } } } impl Tokenizable for BooleanOption { fn tokenize(&self) -> Vec { match self { BooleanOption::Literal(value) => vec![Token::Boolean(value.to_string())], BooleanOption::Placeholder(expr) => expr.tokenize(), } } } impl Tokenizable for NaturalOption { fn tokenize(&self) -> Vec { match self { NaturalOption::Literal(value) => value.tokenize(), NaturalOption::Placeholder(expr) => expr.tokenize(), } } } impl Tokenizable for U64 { fn tokenize(&self) -> Vec { vec![Token::Number(self.to_source().to_string())] } } impl Tokenizable for I64 { fn tokenize(&self) -> Vec { vec![Token::Number(self.to_source().to_string())] } } impl Tokenizable for CountOption { fn tokenize(&self) -> Vec { match self { CountOption::Literal(retry) => retry.tokenize(), CountOption::Placeholder(expr) => expr.tokenize(), } } } impl Tokenizable for Count { fn tokenize(&self) -> Vec { match self { Count::Finite(n) => vec![Token::Number(n.to_string())], Count::Infinite => vec![Token::Number("-1".to_string())], } } } impl Tokenizable for DurationOption { fn tokenize(&self) -> Vec { match self { DurationOption::Literal(value) => value.tokenize(), DurationOption::Placeholder(expr) => expr.tokenize(), } } } impl Tokenizable for Duration { fn tokenize(&self) -> Vec { let mut tokens = vec![Token::Number(self.value.to_source().to_string())]; if let Some(unit) = self.unit { tokens.push(Token::Unit(unit.to_string())); } tokens } } impl Tokenizable for VariableDefinition { fn tokenize(&self) -> Vec { let mut tokens: Vec = vec![Token::String(self.name.clone())]; tokens.append(&mut self.space0.tokenize()); tokens.push(Token::Keyword("=".to_string())); tokens.append(&mut self.space1.tokenize()); tokens.append(&mut self.value.tokenize()); tokens } } impl Tokenizable for VariableValue { fn tokenize(&self) -> Vec { match self { VariableValue::Null => vec![Token::Keyword("null".to_string())], VariableValue::Bool(v) => vec![Token::Boolean(v.to_string())], VariableValue::Number(v) => vec![Token::Number(v.to_source().to_string())], VariableValue::String(v) => v.tokenize(), } } } impl Tokenizable for Filter { fn tokenize(&self) -> Vec { let mut tokens = vec![Token::FilterType(self.value.identifier().to_string())]; match &self.value { FilterValue::Decode { space0, encoding } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut encoding.tokenize()); } FilterValue::Format { space0, fmt } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut fmt.tokenize()); } FilterValue::JsonPath { space0, expr } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut expr.tokenize()); } FilterValue::Nth { space0, n } => { tokens.append(&mut space0.tokenize()); tokens.push(Token::Number(n.to_source().to_string())); } FilterValue::Regex { space0, value } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut value.tokenize()); } FilterValue::Replace { space0, old_value, space1, new_value, } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut old_value.tokenize()); tokens.append(&mut space1.tokenize()); tokens.append(&mut new_value.tokenize()); } FilterValue::Split { space0, sep } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut sep.tokenize()); } FilterValue::ToDate { space0, fmt } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut fmt.tokenize()); } FilterValue::XPath { space0, expr } => { tokens.append(&mut space0.tokenize()); tokens.append(&mut expr.tokenize()); } _ => {} } tokens } } hurlfmt-6.1.1/src/lib.rs000064400000000000000000000013011046102023000132100ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ pub mod cli; pub mod command; pub mod curl; pub mod format; pub mod linter; hurlfmt-6.1.1/src/linter/mod.rs000064400000000000000000000012401046102023000145200ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ pub use rules::lint_hurl_file; mod rules; hurlfmt-6.1.1/src/linter/rules.rs000064400000000000000000000567161046102023000151150ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use hurl_core::ast::{ Assert, Base64, Body, Bytes, Capture, Comment, Cookie, CookieAttribute, CookieAttributeName, CookiePath, DurationOption, Entry, EntryOption, File, FileParam, Filter, FilterValue, GraphQl, Hex, HurlFile, KeyValue, LineTerminator, MultilineString, MultilineStringAttribute, MultilineStringKind, MultipartParam, OptionKind, Predicate, PredicateFunc, PredicateFuncValue, PredicateValue, Query, QueryValue, RegexValue, Request, Response, Section, SectionValue, SourceInfo, Template, VariableDefinition, Whitespace, }; use hurl_core::reader::Pos; use hurl_core::typing::{Duration, DurationUnit}; /// Returns a new linted instance from this `hurl_file`. pub fn lint_hurl_file(hurl_file: &HurlFile) -> HurlFile { HurlFile { entries: hurl_file.entries.iter().map(lint_entry).collect(), line_terminators: hurl_file.line_terminators.clone(), } } fn lint_entry(entry: &Entry) -> Entry { let request = lint_request(&entry.request); let response = entry.response.as_ref().map(lint_response); Entry { request, response } } fn lint_request(request: &Request) -> Request { let line_terminators = request.line_terminators.clone(); let space0 = empty_whitespace(); let method = request.method.clone(); let space1 = one_whitespace(); let url = request.url.clone(); let line_terminator0 = lint_line_terminator(&request.line_terminator0); let headers = request.headers.iter().map(lint_key_value).collect(); let body = request.body.as_ref().map(lint_body); let mut sections: Vec
= request.sections.iter().map(lint_section).collect(); sections.sort_by_key(|k| section_value_index(k.value.clone())); let source_info = SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)); Request { line_terminators, space0, method, space1, url, line_terminator0, headers, sections, body, source_info, } } fn lint_response(response: &Response) -> Response { let line_terminators = response.line_terminators.clone(); let space0 = empty_whitespace(); let version = response.version.clone(); let space1 = response.space1.clone(); let status = response.status.clone(); let line_terminator0 = response.line_terminator0.clone(); let headers = response.headers.iter().map(lint_key_value).collect(); let mut sections: Vec
= response.sections.iter().map(lint_section).collect(); sections.sort_by_key(|k| section_value_index(k.value.clone())); let body = response.body.clone(); Response { line_terminators, space0, version, space1, status, line_terminator0, headers, sections, body, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } } fn lint_section(section: &Section) -> Section { let line_terminators = section.line_terminators.clone(); let line_terminator0 = section.line_terminator0.clone(); let value = lint_section_value(§ion.value); Section { line_terminators, space0: empty_whitespace(), value, line_terminator0, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } } fn lint_section_value(section_value: &SectionValue) -> SectionValue { match section_value { SectionValue::QueryParams(params, short) => { SectionValue::QueryParams(params.iter().map(lint_key_value).collect(), *short) } SectionValue::BasicAuth(param) => { SectionValue::BasicAuth(param.as_ref().map(lint_key_value)) } SectionValue::Captures(captures) => { SectionValue::Captures(captures.iter().map(lint_capture).collect()) } SectionValue::Asserts(asserts) => { SectionValue::Asserts(asserts.iter().map(lint_assert).collect()) } SectionValue::FormParams(params, short) => { SectionValue::FormParams(params.iter().map(lint_key_value).collect(), *short) } SectionValue::MultipartFormData(params, short) => SectionValue::MultipartFormData( params.iter().map(lint_multipart_param).collect(), *short, ), SectionValue::Cookies(cookies) => { SectionValue::Cookies(cookies.iter().map(lint_cookie).collect()) } SectionValue::Options(options) => { SectionValue::Options(options.iter().map(lint_entry_option).collect()) } } } fn section_value_index(section_value: SectionValue) -> u32 { match section_value { // Request sections SectionValue::Options(_) => 0, SectionValue::QueryParams(_, _) => 1, SectionValue::BasicAuth(_) => 2, SectionValue::FormParams(_, _) => 3, SectionValue::MultipartFormData(_, _) => 4, SectionValue::Cookies(_) => 5, // Response sections SectionValue::Captures(_) => 0, SectionValue::Asserts(_) => 1, } } fn lint_assert(assert: &Assert) -> Assert { let filters = assert .filters .iter() .map(|(_, f)| (one_whitespace(), lint_filter(f))) .collect(); Assert { line_terminators: assert.line_terminators.clone(), space0: empty_whitespace(), query: lint_query(&assert.query), filters, space1: one_whitespace(), predicate: lint_predicate(&assert.predicate), line_terminator0: assert.line_terminator0.clone(), } } fn lint_capture(capture: &Capture) -> Capture { let filters = capture .filters .iter() .map(|(_, f)| (one_whitespace(), lint_filter(f))) .collect(); let space3 = if capture.redact { one_whitespace() } else { capture.space3.clone() }; Capture { line_terminators: capture.line_terminators.clone(), space0: empty_whitespace(), name: capture.name.clone(), space1: empty_whitespace(), space2: one_whitespace(), query: lint_query(&capture.query), filters, space3, redact: capture.redact, line_terminator0: lint_line_terminator(&capture.line_terminator0), } } fn lint_query(query: &Query) -> Query { Query { source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), value: lint_query_value(&query.value), } } fn lint_query_value(query_value: &QueryValue) -> QueryValue { match query_value { QueryValue::Status => QueryValue::Status, QueryValue::Version => QueryValue::Version, QueryValue::Url => QueryValue::Url, QueryValue::Header { name, .. } => QueryValue::Header { name: name.clone(), space0: one_whitespace(), }, QueryValue::Cookie { expr: CookiePath { name, attribute }, .. } => { let attribute = attribute.as_ref().map(lint_cookie_attribute); QueryValue::Cookie { space0: one_whitespace(), expr: CookiePath { name: name.clone(), attribute, }, } } QueryValue::Body => QueryValue::Body, QueryValue::Xpath { expr, .. } => QueryValue::Xpath { expr: expr.clone(), space0: one_whitespace(), }, QueryValue::Jsonpath { expr, .. } => QueryValue::Jsonpath { expr: expr.clone(), space0: one_whitespace(), }, QueryValue::Regex { value, .. } => QueryValue::Regex { value: lint_regex_value(value), space0: one_whitespace(), }, QueryValue::Variable { name, .. } => QueryValue::Variable { name: name.clone(), space0: one_whitespace(), }, QueryValue::Duration => QueryValue::Duration, QueryValue::Bytes => QueryValue::Bytes, QueryValue::Sha256 => QueryValue::Sha256, QueryValue::Md5 => QueryValue::Md5, QueryValue::Certificate { attribute_name: field, .. } => QueryValue::Certificate { attribute_name: *field, space0: one_whitespace(), }, QueryValue::Ip => QueryValue::Ip, } } fn lint_regex_value(regex_value: &RegexValue) -> RegexValue { match regex_value { RegexValue::Template(template) => RegexValue::Template(lint_template(template)), RegexValue::Regex(regex) => RegexValue::Regex(regex.clone()), } } fn lint_cookie_attribute(cookie_attribute: &CookieAttribute) -> CookieAttribute { let space0 = empty_whitespace(); let name = lint_cookie_attribute_name(&cookie_attribute.name); let space1 = empty_whitespace(); CookieAttribute { space0, name, space1, } } fn lint_cookie_attribute_name(cookie_attribute_name: &CookieAttributeName) -> CookieAttributeName { match cookie_attribute_name { CookieAttributeName::Value(_) => CookieAttributeName::Value("Value".to_string()), CookieAttributeName::Expires(_) => CookieAttributeName::Expires("Expires".to_string()), CookieAttributeName::MaxAge(_) => CookieAttributeName::MaxAge("Max-Age".to_string()), CookieAttributeName::Domain(_) => CookieAttributeName::Domain("Domain".to_string()), CookieAttributeName::Path(_) => CookieAttributeName::Path("Path".to_string()), CookieAttributeName::Secure(_) => CookieAttributeName::Secure("Secure".to_string()), CookieAttributeName::HttpOnly(_) => CookieAttributeName::HttpOnly("HttpOnly".to_string()), CookieAttributeName::SameSite(_) => CookieAttributeName::SameSite("SameSite".to_string()), } } fn lint_predicate(predicate: &Predicate) -> Predicate { Predicate { not: predicate.not, space0: if predicate.not { one_whitespace() } else { empty_whitespace() }, predicate_func: lint_predicate_func(&predicate.predicate_func), } } fn lint_predicate_func(predicate_func: &PredicateFunc) -> PredicateFunc { PredicateFunc { source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), value: lint_predicate_func_value(&predicate_func.value), } } fn lint_predicate_func_value(predicate_func_value: &PredicateFuncValue) -> PredicateFuncValue { match predicate_func_value { PredicateFuncValue::Equal { value, .. } => PredicateFuncValue::Equal { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::NotEqual { value, .. } => PredicateFuncValue::NotEqual { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::GreaterThan { value, .. } => PredicateFuncValue::GreaterThan { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::GreaterThanOrEqual { value, .. } => { PredicateFuncValue::GreaterThanOrEqual { space0: one_whitespace(), value: lint_predicate_value(value), } } PredicateFuncValue::LessThan { value, .. } => PredicateFuncValue::LessThan { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::LessThanOrEqual { value, .. } => PredicateFuncValue::LessThanOrEqual { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::Contain { value, .. } => PredicateFuncValue::Contain { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::Include { value, .. } => PredicateFuncValue::Include { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::Match { value, .. } => PredicateFuncValue::Match { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::StartWith { value, .. } => PredicateFuncValue::StartWith { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::EndWith { value, .. } => PredicateFuncValue::EndWith { space0: one_whitespace(), value: lint_predicate_value(value), }, PredicateFuncValue::IsInteger => PredicateFuncValue::IsInteger, PredicateFuncValue::IsFloat => PredicateFuncValue::IsFloat, PredicateFuncValue::IsBoolean => PredicateFuncValue::IsBoolean, PredicateFuncValue::IsString => PredicateFuncValue::IsString, PredicateFuncValue::IsCollection => PredicateFuncValue::IsCollection, PredicateFuncValue::IsDate => PredicateFuncValue::IsDate, PredicateFuncValue::IsIsoDate => PredicateFuncValue::IsIsoDate, PredicateFuncValue::Exist => PredicateFuncValue::Exist, PredicateFuncValue::IsEmpty => PredicateFuncValue::IsEmpty, PredicateFuncValue::IsNumber => PredicateFuncValue::IsNumber, PredicateFuncValue::IsIpv4 => PredicateFuncValue::IsIpv4, PredicateFuncValue::IsIpv6 => PredicateFuncValue::IsIpv6, } } fn lint_predicate_value(predicate_value: &PredicateValue) -> PredicateValue { match predicate_value { PredicateValue::String(value) => PredicateValue::String(lint_template(value)), PredicateValue::MultilineString(value) => { PredicateValue::MultilineString(lint_multiline_string(value)) } PredicateValue::Bool(value) => PredicateValue::Bool(*value), PredicateValue::Null => PredicateValue::Null, PredicateValue::Number(value) => PredicateValue::Number(value.clone()), PredicateValue::File(value) => PredicateValue::File(lint_file(value)), PredicateValue::Hex(value) => PredicateValue::Hex(lint_hex(value)), PredicateValue::Base64(value) => PredicateValue::Base64(lint_base64(value)), PredicateValue::Placeholder(value) => PredicateValue::Placeholder(value.clone()), PredicateValue::Regex(value) => PredicateValue::Regex(value.clone()), } } fn lint_multiline_string(multiline_string: &MultilineString) -> MultilineString { let space = empty_whitespace(); let newline = multiline_string.newline.clone(); match multiline_string { MultilineString { attributes, kind: MultilineStringKind::Text(value), .. } => MultilineString { attributes: lint_multiline_string_attributes(attributes), space, newline, kind: MultilineStringKind::Text(lint_template(value)), }, MultilineString { attributes, kind: MultilineStringKind::Json(value), .. } => MultilineString { attributes: lint_multiline_string_attributes(attributes), space, newline, kind: MultilineStringKind::Json(lint_template(value)), }, MultilineString { attributes, kind: MultilineStringKind::Xml(value), .. } => MultilineString { attributes: lint_multiline_string_attributes(attributes), space, newline, kind: MultilineStringKind::Xml(lint_template(value)), }, MultilineString { attributes, kind: MultilineStringKind::GraphQl(value), .. } => MultilineString { attributes: lint_multiline_string_attributes(attributes), space, newline, kind: MultilineStringKind::GraphQl(lint_graphql(value)), }, } } fn lint_multiline_string_attributes( attributes: &[MultilineStringAttribute], ) -> Vec { attributes.to_vec() } fn lint_graphql(graphql: &GraphQl) -> GraphQl { let value = lint_template(&graphql.value); let variables = graphql.variables.clone(); GraphQl { value, variables } } fn lint_cookie(cookie: &Cookie) -> Cookie { cookie.clone() } fn lint_body(body: &Body) -> Body { let line_terminators = body.line_terminators.clone(); let space0 = empty_whitespace(); let value = lint_bytes(&body.value); let line_terminator0 = body.line_terminator0.clone(); Body { line_terminators, space0, value, line_terminator0, } } fn lint_bytes(bytes: &Bytes) -> Bytes { match bytes { Bytes::File(value) => Bytes::File(lint_file(value)), Bytes::Base64(value) => Bytes::Base64(lint_base64(value)), Bytes::Hex(value) => Bytes::Hex(lint_hex(value)), Bytes::Json(value) => Bytes::Json(value.clone()), Bytes::OnelineString(value) => Bytes::OnelineString(lint_template(value)), Bytes::MultilineString(value) => Bytes::MultilineString(lint_multiline_string(value)), Bytes::Xml(value) => Bytes::Xml(value.clone()), } } fn lint_base64(base64: &Base64) -> Base64 { Base64 { space0: empty_whitespace(), value: base64.value.clone(), source: base64.source.clone(), space1: empty_whitespace(), } } fn lint_hex(hex: &Hex) -> Hex { Hex { space0: empty_whitespace(), value: hex.value.clone(), source: hex.source.clone(), space1: empty_whitespace(), } } fn lint_file(file: &File) -> File { File { space0: empty_whitespace(), filename: lint_template(&file.filename), space1: empty_whitespace(), } } fn lint_key_value(key_value: &KeyValue) -> KeyValue { KeyValue { line_terminators: key_value.line_terminators.clone(), space0: empty_whitespace(), key: key_value.key.clone(), space1: empty_whitespace(), space2: if key_value.value.elements.is_empty() { empty_whitespace() } else { one_whitespace() }, value: key_value.value.clone(), line_terminator0: key_value.line_terminator0.clone(), } } fn lint_multipart_param(multipart_param: &MultipartParam) -> MultipartParam { match multipart_param { MultipartParam::Param(param) => MultipartParam::Param(lint_key_value(param)), MultipartParam::FileParam(file_param) => { MultipartParam::FileParam(lint_file_param(file_param)) } } } fn lint_file_param(file_param: &FileParam) -> FileParam { let line_terminators = file_param.line_terminators.clone(); let space0 = file_param.space0.clone(); let key = file_param.key.clone(); let space1 = file_param.space1.clone(); let space2 = file_param.space2.clone(); let value = file_param.value.clone(); let line_terminator0 = file_param.line_terminator0.clone(); FileParam { line_terminators, space0, key, space1, space2, value, line_terminator0, } } fn empty_whitespace() -> Whitespace { Whitespace { value: String::new(), source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } } fn one_whitespace() -> Whitespace { Whitespace { value: " ".to_string(), source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } } fn lint_line_terminator(line_terminator: &LineTerminator) -> LineTerminator { let space0 = match line_terminator.comment { None => empty_whitespace(), Some(_) => Whitespace { value: line_terminator.space0.value.clone(), source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }, }; let comment = line_terminator.comment.as_ref().map(lint_comment); let newline = Whitespace { value: if line_terminator.newline.value.is_empty() { String::new() } else { "\n".to_string() }, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }; LineTerminator { space0, comment, newline, } } fn lint_comment(comment: &Comment) -> Comment { Comment { value: if comment.value.starts_with(' ') { comment.value.clone() } else { format!(" {}", comment.value) }, source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), } } fn lint_template(template: &Template) -> Template { template.clone() } fn lint_entry_option(entry_option: &EntryOption) -> EntryOption { EntryOption { line_terminators: entry_option.line_terminators.clone(), space0: empty_whitespace(), space1: empty_whitespace(), space2: one_whitespace(), kind: lint_option_kind(&entry_option.kind), line_terminator0: entry_option.line_terminator0.clone(), } } fn lint_option_kind(option_kind: &OptionKind) -> OptionKind { match option_kind { OptionKind::Delay(duration) => { OptionKind::Delay(lint_duration_option(duration, DurationUnit::MilliSecond)) } OptionKind::RetryInterval(duration) => { OptionKind::RetryInterval(lint_duration_option(duration, DurationUnit::MilliSecond)) } OptionKind::Variable(var_def) => OptionKind::Variable(lint_variable_definition(var_def)), _ => option_kind.clone(), } } fn lint_duration_option( duration_option: &DurationOption, default_unit: DurationUnit, ) -> DurationOption { match duration_option { DurationOption::Literal(duration) => { DurationOption::Literal(lint_duration(duration, default_unit)) } DurationOption::Placeholder(expr) => DurationOption::Placeholder(expr.clone()), } } fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> Duration { let value = duration.value.clone(); let unit = Some(duration.unit.unwrap_or(default_unit)); Duration { value, unit } } fn lint_filter(filter: &Filter) -> Filter { Filter { source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), value: lint_filter_value(&filter.value), } } fn lint_filter_value(filter_value: &FilterValue) -> FilterValue { match filter_value { FilterValue::Regex { value, .. } => FilterValue::Regex { space0: one_whitespace(), value: lint_regex_value(value), }, f => f.clone(), } } fn lint_variable_definition(var_def: &VariableDefinition) -> VariableDefinition { VariableDefinition { space0: empty_whitespace(), space1: empty_whitespace(), ..var_def.clone() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_hurl_file() { let hurl_file = HurlFile { entries: vec![], line_terminators: vec![], }; let hurl_file_linted = HurlFile { entries: vec![], line_terminators: vec![], }; assert_eq!(lint_hurl_file(&hurl_file), hurl_file_linted); } #[test] fn test_entry() { let entry = HurlFile { entries: vec![], line_terminators: vec![], }; let entry_linted = HurlFile { entries: vec![], line_terminators: vec![], }; assert_eq!(lint_hurl_file(&entry), entry_linted); } } hurlfmt-6.1.1/src/main.rs000064400000000000000000000160021046102023000133720ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use std::io::{self, Write}; use std::path::PathBuf; use std::process; use hurl_core::input::{Input, InputKind}; use hurl_core::text; use hurlfmt::cli::options::{InputFormat, OptionsError, OutputFormat}; use hurlfmt::cli::Logger; use hurlfmt::command::check::CheckError; use hurlfmt::command::export::ExportError; use hurlfmt::command::format::FormatError; use hurlfmt::{cli, command}; const EXIT_OK: i32 = 0; const EXIT_ERROR: i32 = 1; const EXIT_INVALID_INPUT: i32 = 2; const EXIT_LINT_ISSUE: i32 = 3; /// Executes `hurlfmt` entry point. fn main() { text::init_crate_colored(); let opts = match cli::options::parse() { Ok(v) => v, Err(e) => match e { OptionsError::Info(message) => { print!("{message}"); process::exit(EXIT_OK); } OptionsError::Error(message) => { eprintln!("{message}"); process::exit(EXIT_ERROR); } }, }; let logger = Logger::new(opts.color); if opts.check { process_check_command(&opts.input_files, opts.output_file, &logger); } else if opts.in_place { process_format_command(&opts.input_files, &logger); } else { process_export_command( &opts.input_files, opts.output_file, &logger, &opts.input_format, &opts.output_format, opts.standalone, opts.color, ); } } fn process_check_command(input_files: &[Input], output_file: Option, logger: &Logger) { let errors = command::check::run(input_files); if errors.is_empty() { process::exit(EXIT_OK); } else { let mut count = 0; let mut invalid_input = false; let mut output_all = String::new(); for e in &errors { match e { CheckError::IO { filename, message } => { logger.error(&format!( "Input file {filename} can not be read - {message}" )); invalid_input = true; } CheckError::Parse { content, input_file, error, } => { logger.error_parsing(content, input_file, error); invalid_input = true; } CheckError::Unformatted(filename) => { output_all.push_str(&format!("would reformat: {}\n", filename)); count += 1; } } } if count > 0 { output_all.push_str(&format!( "{count} file{} would be reformatted", if count > 1 { "s" } else { "" } )); } write_output(&output_all, output_file, logger); if invalid_input { process::exit(EXIT_INVALID_INPUT); } else { process::exit(EXIT_LINT_ISSUE); } } } fn process_format_command(input_files: &[Input], logger: &Logger) { let mut input_files2 = vec![]; for input_file in input_files { if let InputKind::File(path) = input_file.kind() { input_files2.push(path.clone()); } else { logger.error("Standard input can be formatted in place!"); process::exit(EXIT_INVALID_INPUT); } } let errors = command::format::run(&input_files2); if errors.is_empty() { process::exit(EXIT_OK); } else { for e in &errors { match e { FormatError::IO { filename, message } => { logger.error(&format!( "Input file {filename} can not be read - {message}" )); } FormatError::Parse { content, input_file, error, } => { logger.error_parsing(content, input_file, error); } } } process::exit(EXIT_INVALID_INPUT); } } fn process_export_command( input_files: &[Input], output_file: Option, logger: &Logger, input_format: &InputFormat, output_format: &OutputFormat, standalone: bool, color: bool, ) { let mut error = false; let mut output_all = String::new(); let results = command::export::run(input_files, input_format, output_format, standalone, color); for result in &results { match result { Ok(output) => output_all.push_str(output), Err(e) => { error = true; match e { ExportError::IO { filename, message } => { logger.error(&format!( "Input file {filename} can not be read - {message}" )); error = true; } ExportError::Parse { content, input_file, error, } => { logger.error_parsing(content, input_file, error); } ExportError::Curl(s) => logger.error(&format!("error curl {s} d")), } } } } write_output(&output_all, output_file, logger); if error { process::exit(EXIT_INVALID_INPUT); } else { process::exit(EXIT_OK); } } fn write_output(content: &str, filename: Option, logger: &Logger) { let content = if !content.ends_with('\n') { format!("{content}\n") } else { content.to_string() }; let bytes = content.into_bytes(); match filename { None => { let stdout = io::stdout(); let mut handle = stdout.lock(); if let Err(why) = handle.write_all(bytes.as_slice()) { logger.error(&format!("Issue writing to stdout: {why}")); process::exit(EXIT_ERROR); } } Some(path_buf) => { let mut file = match std::fs::File::create(&path_buf) { Err(why) => { eprintln!("Issue writing to {}: {:?}", path_buf.display(), why); process::exit(EXIT_ERROR); } Ok(file) => file, }; file.write_all(bytes.as_slice()) .expect("writing bytes to file"); } } } hurlfmt-6.1.1/tests/json/elasticsearch.json000064400000000000000000000010511046102023000171270ustar 00000000000000{ "from": 0, "size": 2000, "sort": [ { "@timestamp": "desc" } ], "query": { "bool": { "must": [ { "match": { "tag.env": "prod" } }, { "match": { "data.r_code": "500" } }, { "match": { "data.q_client-dns": "m.shop.sosh.fr" } }, { "match": { "@timestamp": { "query": "{{date}}" } } } ] } } }hurlfmt-6.1.1/tests/json/person.json000064400000000000000000000006261046102023000156320ustar 00000000000000{ "firstName": "John", "lastName": "Smith", "isAlive": true, "age": 27, "address": { "streetAddress": "21 2nd Street", "city": "New York", "state": "NY", "postalCode": "10021-3100" }, "phoneNumbers": [ { "type": "home", "number": "212 555-1234" }, { "type": "office", "number": "646 555-4567" } ], "children": [], "spouse": null }hurlfmt-6.1.1/tests/json.rs000064400000000000000000000240461046102023000140010ustar 00000000000000/* * Hurl (https://hurl.dev) * Copyright (C) 2025 Orange * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ use std::fs; use hurl_core::ast::*; use hurl_core::parser::parse_json; use hurl_core::reader::{Pos, Reader}; use hurl_core::typing::ToSource; use hurlfmt::format::{Token, Tokenizable}; use proptest::prelude::prop::test_runner::TestRunner; use proptest::prelude::*; fn whitespace() -> BoxedStrategy { prop_oneof![ Just(String::new()), Just(" ".to_string()), Just(" ".to_string()), ] .boxed() } // region strategy scalar/leaves fn value_number() -> BoxedStrategy { prop_oneof![ Just(JsonValue::Number("0".to_string())), Just(JsonValue::Number("1".to_string())), Just(JsonValue::Number("1.33".to_string())), Just(JsonValue::Number("-100".to_string())) ] .boxed() } fn value_boolean() -> BoxedStrategy { prop_oneof![ Just(JsonValue::Boolean(true)), Just(JsonValue::Boolean(false)), ] .boxed() } fn value_string() -> BoxedStrategy { let source_info = SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)); let variable = Variable { name: "name".to_string(), source_info, }; prop_oneof![ Just(JsonValue::String(Template { elements: vec![], delimiter: Some('"'), source_info })), Just(JsonValue::String(Template { elements: vec![TemplateElement::String { value: "Hello".to_string(), source: "Hello".to_source(), }], delimiter: Some('"'), source_info })), Just(JsonValue::String(Template { elements: vec![ TemplateElement::String { value: "Hello ".to_string(), source: "Hello\\u0020 ".to_source(), }, TemplateElement::Placeholder(Placeholder { space0: Whitespace { value: String::new(), source_info }, expr: Expr { kind: ExprKind::Variable(variable), source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)), }, space1: Whitespace { value: String::new(), source_info }, }) ], delimiter: Some('"'), source_info })), ] .boxed() } // endregion // region strategy value fn value() -> BoxedStrategy { let leaf = prop_oneof![value_boolean(), value_string(), value_number(),]; leaf.prop_recursive( 8, // 8 levels deep 256, // Shoot for maximum size of 256 nodes 10, // We put up to 10 items per collection |value| { prop_oneof![ // Lists (whitespace()).prop_map(|space0| JsonValue::List { space0, elements: vec![] }), (whitespace(), whitespace(), value.clone()).prop_map(|(space0, space1, value)| { JsonValue::List { space0, elements: vec![JsonListElement { space0: String::new(), value, space1, }], } }), ( whitespace(), whitespace(), value_number(), whitespace(), whitespace(), value_number() ) .prop_map( |(space00, space01, value0, space10, space11, value1)| JsonValue::List { space0: space00, elements: vec![ JsonListElement { space0: String::new(), value: value0, space1: space01 }, JsonListElement { space0: space10, value: value1, space1: space11 }, ] } ), ( whitespace(), whitespace(), value_boolean(), whitespace(), whitespace(), value_boolean() ) .prop_map( |(space00, space01, value0, space10, space11, value1)| JsonValue::List { space0: space00, elements: vec![ JsonListElement { space0: String::new(), value: value0, space1: space01 }, JsonListElement { space0: space10, value: value1, space1: space11 }, ] } ), ( whitespace(), whitespace(), value_string(), whitespace(), whitespace(), value_string() ) .prop_map( |(space00, space01, value0, space10, space11, value1)| JsonValue::List { space0: space00, elements: vec![ JsonListElement { space0: String::new(), value: value0, space1: space01 }, JsonListElement { space0: space10, value: value1, space1: space11 }, ] } ), // Object (whitespace()).prop_map(|space0| JsonValue::Object { space0, elements: vec![] }), ( whitespace(), whitespace(), whitespace(), value, whitespace() ) .prop_map(|(space0, space1, space2, value, space3)| { JsonValue::Object { space0, elements: vec![JsonObjectElement { space0: String::new(), name: Template { delimiter: None, elements: vec![TemplateElement::String { value: "key1".to_string(), source: "key1".to_source(), }], source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 1)), }, space1, space2, value, space3, }], } }), ] }, ) .boxed() } // endregion // region test-echo fn format_token(token: Token) -> String { match token { Token::Whitespace(s) | Token::Number(s) | Token::Boolean(s) | Token::String(s) | Token::Keyword(s) | Token::StringDelimiter(s) | Token::QueryType(s) | Token::CodeVariable(s) | Token::CodeDelimiter(s) => s, _ => panic!("invalid token {token:?}"), } } fn format_value(value: JsonValue) -> String { let tokens = value.tokenize(); //eprintln!("{:?}", tokens); tokens .iter() .map(|t| format_token(t.clone())) .collect::>() .join("") } #[test] fn test_echo() { let mut runner = TestRunner::default(); //let mut runner = TestRunner::new(ProptestConfig::with_cases(10000)); runner .run(&value(), |value| { //eprintln!("value={:#?}", value); let s = format_value(value); eprintln!("s={s}"); let mut reader = Reader::new(s.as_str()); let parsed_value = parse_json(&mut reader).unwrap(); assert_eq!(format_value(parsed_value), s); Ok(()) }) .unwrap(); //assert_eq!(1,2); } #[test] fn test_parse_files() { let paths = fs::read_dir("tests/json").unwrap(); for p in paths { let path = p.unwrap().path(); println!("parsing json file {}", path.display()); let s = fs::read_to_string(path).expect("Something went wrong reading the file"); let mut reader = Reader::new(s.as_str()); let parsed_value = parse_json(&mut reader).unwrap(); assert_eq!(format_value(parsed_value), s); } }