hurlfmt-7.1.0/.cargo_vcs_info.json0000644000000001560000000000100125240ustar { "git": { "sha1": "77798424906a431e5b1c136f540d6cb5ed79b788" }, "path_in_vcs": "packages/hurlfmt" }hurlfmt-7.1.0/Cargo.lock0000644000000364740000000000100105130ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "aho-corasick" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] [[package]] name = "anstream" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", "windows-sys 0.61.2", ] [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ "bitflags", "cexpr", "clang-sys", "itertools", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", "syn", ] [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cexpr" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" dependencies = [ "nom", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clang-sys" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", "libloading", ] [[package]] name = "clap" version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" version = "4.5.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" dependencies = [ "anstream", "anstyle", "clap_lex", "strsim", "terminal_size", ] [[package]] name = "clap_lex" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "colored" version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "glob" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "hurl_core" version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd2d82d2ed5224626e5a7c309efd6cdaffae1356ec1054f83dd3cfcd1dbc1ed8" dependencies = [ "colored", "libxml", "regex", ] [[package]] name = "hurlfmt" version = "7.1.0" dependencies = [ "base64", "clap", "hurl_core", "regex", ] [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] [[package]] name = "libc" version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libloading" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ "cfg-if", "windows-link", ] [[package]] name = "libxml" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74a5e46b8bcd6b70cb485ca086e43aa020af841e29fb0aba88ce02cd1cb52cc7" dependencies = [ "bindgen", "libc", "pkg-config", "vcpkg", ] [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", ] [[package]] name = "once_cell_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rustc-hash" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[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.110" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "terminal_size" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" dependencies = [ "rustix", "windows-sys 0.60.2", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[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 = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets 0.52.6", ] [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets 0.53.5", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", "windows_i686_gnullvm 0.53.1", "windows_i686_msvc 0.53.1", "windows_x86_64_gnu 0.53.1", "windows_x86_64_gnullvm 0.53.1", "windows_x86_64_msvc 0.53.1", ] [[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_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[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_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[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_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[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_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[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_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" hurlfmt-7.1.0/Cargo.toml0000644000000027440000000000100105270ustar # 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.91.1" name = "hurlfmt" version = "7.1.0" 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" [dependencies.base64] version = "0.22.1" [dependencies.clap] version = "4.5.51" features = [ "cargo", "wrap_help", ] [dependencies.hurl_core] version = "7.1.0" [dependencies.regex] version = "1.12.2" [lints.clippy] empty_structs_with_brackets = "deny" manual_string_new = "deny" semicolon_if_nothing_returned = "deny" wildcard-imports = "deny" [lints.rust] warnings = "deny" hurlfmt-7.1.0/Cargo.toml.orig000064400000000000000000000011531046102023000142010ustar 00000000000000[package] name = "hurlfmt" version = "7.1.0" 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.91.1" [dependencies] base64 = "0.22.1" clap = { version = "4.5.51", features = ["cargo", "wrap_help"] } hurl_core = { version = "7.1.0", path = "../hurl_core" } regex = "1.12.2" [lints] workspace = true hurlfmt-7.1.0/README.md000064400000000000000000000002321046102023000125660ustar 00000000000000hurlfmt ===================================== The hurlfmt crate provides the `hurlfmt` binary. It can format hurl files and to export them to JSON/HTML. hurlfmt-7.1.0/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-7.1.0/src/cli/logger.rs000064400000000000000000000042101046102023000144720ustar 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.render( &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)); } } hurlfmt-7.1.0/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-7.1.0/src/cli/options/commands.rs000064400000000000000000000051551046102023000165200ustar 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") .help("Specify input format: hurl or curl [default: hurl]") .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") .help("Specify output format: hurl, json or html [default: hurl]") .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-7.1.0/src/cli/options/matches.rs000064400000000000000000000107461046102023000163450ustar 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) -> Option { if has_flag(arg_matches, "color") { Some(true) } else if has_flag(arg_matches, "no_color") || has_flag(arg_matches, "in_place") { Some(false) } else { None } } pub fn input_format(arg_matches: &ArgMatches) -> Result { match get_string(arg_matches, "input_format") .unwrap_or("hurl".to_string()) .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_or("hurl".to_string()) .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 input_format(arg_matches)? != InputFormat::Hurl { 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-7.1.0/src/cli/options/mod.rs000064400000000000000000000062251046102023000154750ustar 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: Option, 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-7.1.0/src/command/check.rs000064400000000000000000000035311046102023000151440ustar 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::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 formatted = linter::lint_hurl_file(&hurl_file); if formatted == content { Ok(()) } else { Err(CheckError::Unformatted(input_file.to_string())) } } hurlfmt-7.1.0/src/command/export.rs000064400000000000000000000051301046102023000154050ustar 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 formatted = linter::lint_hurl_file(&hurl_file); let hurl_file = parser::parse_hurl_file(&formatted).unwrap(); 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-7.1.0/src/command/format.rs000064400000000000000000000045061046102023000153620ustar 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::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 formatted = linter::lint_hurl_file(&hurl_file); 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-7.1.0/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-7.1.0/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-7.1.0/src/curl/commands.rs000064400000000000000000000103761046102023000152240ustar 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 cookies() -> clap::Arg { clap::Arg::new("cookies") .long("cookie") .short('b') .value_name("NAME1=VALUE1; NAME2=VALUE2") .value_parser(|value: &str| { if value.trim().is_empty() { return Err("empty value provided".to_string()); } let (valid_cookies, invalid_cookies): (Vec<_>, Vec<_>) = value .split(';') .map(str::trim) .filter(|c| !c.is_empty()) .partition(|c| c.contains('=')); if invalid_cookies.is_empty() { Ok(valid_cookies.join("; ")) } else { match invalid_cookies.as_slice() { [_] => Err("invalid cookie pair provided".to_string()), _ => Err(format!( "invalid cookie pairs provided: [{}]", invalid_cookies.join(", ") )), } } }) .action(ArgAction::Append) .num_args(1) } 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") .value_parser(|value: &str| { // We add a basic format check on headers, accepting either "NAME: VALUE" or "NAME;" for an empty header. // See curl manual // > If you send the custom header with no-value then its header must be terminated with a semicolon, // > such as -H "X-Custom-Header;" to send "X-Custom-Header:". if value.contains(":") || value.ends_with(";") { Ok(String::from(value)) } else { Err("headers must be formatted as '' or ';'") } }) .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 negotiate() -> clap::Arg { clap::Arg::new("negotiate").long("negotiate").num_args(0) } pub fn ntlm() -> clap::Arg { clap::Arg::new("ntlm").long("ntlm").num_args(0) } 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) } pub fn user() -> clap::Arg { clap::Arg::new("user").long("user").short('u').num_args(1) } pub fn verbose() -> clap::Arg { clap::Arg::new("verbose") .long("verbose") .short('v') .num_args(0) } hurlfmt-7.1.0/src/curl/matches.rs000064400000000000000000000103041046102023000150360ustar 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 cookies(arg_matches: &ArgMatches) -> Vec { get_strings(arg_matches, "cookies").unwrap_or_default() } 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 has_flag(arg_matches, "negotiate") { options.push(HurlOption::new("negotiate", "true")); } if has_flag(arg_matches, "ntlm") { options.push(HurlOption::new("ntlm", "true")); } if let Some(value) = get::(arg_matches, "retry") { options.push(HurlOption::new("retry", value.to_string().as_str())); } if let Some(value) = get::(arg_matches, "user") { options.push(HurlOption::new("user", value.to_string().as_str())); } if has_flag(arg_matches, "verbose") { options.push(HurlOption::new("verbose", "true")); } 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-7.1.0/src/curl/mod.rs000064400000000000000000000272301046102023000141770ustar 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 regex; 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::cookies()) .arg(commands::insecure()) .arg(commands::verbose()) .arg(commands::negotiate()) .arg(commands::ntlm()) .arg(commands::location()) .arg(commands::max_redirects()) .arg(commands::method()) .arg(commands::retry()) .arg(commands::user()) .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 cookies = matches::cookies(&arg_matches); let options = matches::options(&arg_matches); let body = matches::body(&arg_matches); let s = format(&method, &url, &headers, &cookies, &options, body); Ok(s) } fn format( method: &str, url: &str, headers: &[String], cookies: &[String], 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 !cookies.is_empty() { s.push_str(format!("\ncookie: {}", cookies.join("; ")).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_illegal_header() { assert!( parse_line("curl http://localhost:8000/illegal-header -H 'Illegal-Header'") .unwrap_err() .contains("headers must be formatted as '' or ';'") ); } #[test] fn test_valid_cookies() { let hurl_str = r#"GET http://localhost:8000/custom-cookies cookie: name1=value1; name2=value2; name3=value3 "#; assert_eq!( parse_line("curl http://localhost:8000/custom-cookies -b 'name1=value1' -b 'name2=value2;name3=value3;;'").unwrap(), hurl_str ); assert_eq!( parse_line("curl http://localhost:8000/custom-cookies --cookie 'name1=value1' --cookie 'name2=value2;name3=value3;;'").unwrap(), hurl_str ); } #[test] fn test_empty_cookie() { assert!( parse_line("curl http://localhost:8000/empty-cookie -b 'valid=pair' -b ''") .unwrap_err() .contains("empty value provided") ); } #[test] fn test_single_illegal_cookie_pair() { assert!( parse_line("curl http://localhost:8000/empty-cookie -b 'valid=pair' -b 'invalid'") .unwrap_err() .contains("invalid cookie pair provided") ); } #[test] fn test_multiple_illegal_cookie_pairs() { assert!(parse_line( "curl http://localhost:8000/empty-cookie -b 'name=value' -b 'valid=pair; invalid-1; invalid-2'" ) .unwrap_err() .contains("invalid cookie pairs provided: [invalid-1, invalid-2]")); } #[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 ); } #[test] fn test_verbose_flag() { let hurl_str = r#"GET http://localhost:8000/hello [Options] verbose: true "#; let flags = vec!["-v", "--verbose"]; for flag in flags { assert_eq!( parse_line(format!("curl {flag} http://localhost:8000/hello").as_str()).unwrap(), hurl_str ); } } #[test] fn test_user_option() { let user = "test_user:test_pass"; let hurl_str = format!("GET http://localhost:8000/hello\n[Options]\nuser: {user}\n"); let flags = vec!["-u", "--user"]; for flag in flags { assert_eq!( parse_line(&format!("curl {flag} '{user}' http://localhost:8000/hello")).unwrap(), hurl_str ); } } #[test] fn test_ntlm_flag() { let hurl_str = r#"GET http://localhost:8000/hello [Options] ntlm: true "#; assert_eq!( parse_line("curl --ntlm http://localhost:8000/hello").unwrap(), hurl_str ); } #[test] fn test_negotiate_flag() { let hurl_str = r#"GET http://localhost:8000/hello [Options] negotiate: true "#; assert_eq!( parse_line("curl --negotiate http://localhost:8000/hello").unwrap(), hurl_str ); } } hurlfmt-7.1.0/src/format/json.rs000064400000000000000000001075311046102023000147170ustar 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, FilenameParam, 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::types::{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::FilenameParam(param) => param.to_json(), } } } impl ToJson for FilenameParam { 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::MaxTime(value) => value.to_json(), OptionKind::Negotiate(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::Ntlm(value) => value.to_json(), OptionKind::Output(filename) => JValue::String(filename.to_string()), OptionKind::PathAsIs(value) => value.to_json(), OptionKind::PinnedPublicKey(value) => JValue::String(value.to_string()), 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.redacted { 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::Exist | PredicateFuncValue::IsBoolean | PredicateFuncValue::IsCollection | PredicateFuncValue::IsDate | PredicateFuncValue::IsEmpty | PredicateFuncValue::IsFloat | PredicateFuncValue::IsInteger | PredicateFuncValue::IsIpv4 | PredicateFuncValue::IsIpv6 | PredicateFuncValue::IsIsoDate | PredicateFuncValue::IsList | PredicateFuncValue::IsNumber | PredicateFuncValue::IsObject | PredicateFuncValue::IsString | PredicateFuncValue::IsUuid => {} } 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::DateFormat { 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(), JValue::String(old_value.to_string()), )); attributes.push(( "new_value".to_string(), JValue::String(new_value.to_string()), )); } FilterValue::ReplaceRegex { pattern, new_value, .. } => { attributes.push(("pattern".to_string(), pattern.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::UrlQueryParam { param, .. } => { attributes.push(("param".to_string(), JValue::String(param.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::types::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(), redacted: 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-7.1.0/src/format/mod.rs000064400000000000000000000013641046102023000145220ustar 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; mod json; mod serialize_json; mod text; hurlfmt-7.1.0/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-7.1.0/src/format/text.rs000064400000000000000000000164451046102023000147350ustar 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::visit::Visitor; use hurl_core::ast::{ Comment, CookiePath, FilterValue, HurlFile, JsonValue, Method, MultilineString, Number, Placeholder, PredicateFuncValue, QueryValue, Regex, StatusValue, Template, VersionValue, Whitespace, U64, }; use hurl_core::text::{Format, Style, StyledString}; use hurl_core::types::{DurationUnit, SourceString, ToSource}; /// Format a Hurl `file` to text, using ANSI escape code if `color` is true. pub fn format(file: &HurlFile, color: bool) -> String { let format = if color { Format::Ansi } else { Format::Plain }; let mut fmt = TextFormatter::new(); fmt.format(file, format) } /// A formatter for text and terminal export. struct TextFormatter { buffer: StyledString, } impl TextFormatter { /// Creates a new text formatter supporting ANSI escape code coloring. fn new() -> Self { TextFormatter { buffer: StyledString::new(), } } fn format(&mut self, file: &HurlFile, format: Format) -> String { self.buffer.clear(); self.visit_hurl_file(file); self.buffer.to_string(format) } } impl Visitor for TextFormatter { fn visit_base64_value(&mut self, _value: &[u8], source: &SourceString) { self.buffer.push_with(source.as_str(), Style::new().green()); } fn visit_bool(&mut self, value: bool) { let value = value.to_string(); self.buffer.push_with(value.as_str(), Style::new().cyan()); } fn visit_cookie_path(&mut self, path: &CookiePath) { let value = path.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_comment(&mut self, comment: &Comment) { let value = comment.to_source(); self.buffer .push_with(value.as_str(), Style::new().bright_black()); } fn visit_duration_unit(&mut self, unit: DurationUnit) { let value = unit.to_string(); self.buffer.push_with(&value, Style::new().cyan()); } fn visit_filename(&mut self, filename: &Template) { let value = filename.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_filter_kind(&mut self, kind: &FilterValue) { let value = kind.identifier(); self.buffer.push_with(value, Style::new().yellow()); } fn visit_hex_value(&mut self, _value: &[u8], source: &SourceString) { self.buffer.push_with(source.as_str(), Style::new().green()); } fn visit_i64(&mut self, n: i64) { let value = n.to_string(); self.buffer.push_with(&value, Style::new().cyan()); } fn visit_json_body(&mut self, json: &JsonValue) { let value = json.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_literal(&mut self, lit: &'static str) { self.buffer.push(lit); } fn visit_method(&mut self, method: &Method) { let value = method.to_source(); self.buffer.push_with(value.as_str(), Style::new().yellow()); } fn visit_multiline_string(&mut self, string: &MultilineString) { let value = string.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_not(&mut self, identifier: &'static str) { self.buffer.push_with(identifier, Style::new().yellow()); } fn visit_null(&mut self, identifier: &'static str) { self.buffer.push_with(identifier, Style::new().cyan()); } fn visit_number(&mut self, number: &Number) { let value = number.to_source(); self.buffer.push_with(value.as_str(), Style::new().cyan()); } fn visit_placeholder(&mut self, placeholder: &Placeholder) { let value = placeholder.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_predicate_kind(&mut self, kind: &PredicateFuncValue) { let value = kind.identifier(); self.buffer.push_with(value, Style::new().yellow()); } fn visit_query_kind(&mut self, kind: &QueryValue) { let value = kind.identifier(); self.buffer.push_with(value, Style::new().cyan()); } fn visit_regex(&mut self, regex: &Regex) { let value = regex.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_status(&mut self, value: &StatusValue) { let value = value.to_string(); self.buffer.push(&value); } fn visit_string(&mut self, value: &str) { self.buffer.push_with(value, Style::new().green()); } fn visit_section_header(&mut self, name: &str) { self.buffer.push_with(name, Style::new().magenta()); } fn visit_template(&mut self, template: &Template) { let value = template.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_url(&mut self, url: &Template) { let value = url.to_source(); self.buffer.push_with(value.as_str(), Style::new().green()); } fn visit_u64(&mut self, n: &U64) { let value = n.to_source(); self.buffer.push_with(value.as_str(), Style::new().cyan()); } fn visit_usize(&mut self, n: usize) { let value = n.to_string(); self.buffer.push_with(value.as_str(), Style::new().cyan()); } fn visit_variable_name(&mut self, name: &str) { self.buffer.push(name); } fn visit_version(&mut self, value: &VersionValue) { let value = value.to_string(); self.buffer.push(&value); } fn visit_xml_body(&mut self, xml: &str) { self.buffer.push_with(xml, Style::new().green()); } fn visit_whitespace(&mut self, ws: &Whitespace) { self.buffer.push(ws.as_str()); } } #[cfg(test)] mod tests { use crate::format::text::TextFormatter; use hurl_core::parser::parse_hurl_file; use hurl_core::text::Format; #[test] fn format_hurl_file() { // For the crate colored to output ANSI escape code in test environment. hurl_core::text::init_crate_colored(); let src = r#" GET https://foo.com header1: value1 header2: value2 [Form] foo: bar baz: 123 HTTP 200 [Asserts] jsonpath "$.name" == "toto" "#; let file = parse_hurl_file(src).unwrap(); let mut fmt = TextFormatter::new(); let dst = fmt.format(&file, Format::Plain); assert_eq!(src, dst); let dst = fmt.format(&file, Format::Ansi); assert_eq!( dst, r#" GET https://foo.com header1: value1 header2: value2 [Form] foo: bar baz: 123 HTTP 200 [Asserts] jsonpath "$.name" == "toto" "# ); } } hurlfmt-7.1.0/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-7.1.0/src/linter/mod.rs000064400000000000000000000012441046102023000145240ustar 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 rewrite::lint_hurl_file; mod rewrite; hurlfmt-7.1.0/src/linter/rewrite.rs000064400000000000000000000660551046102023000154410ustar 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, CookiePath, CountOption, DurationOption, Entry, EntryOption, File, FilenameParam, FilenameValue, FilterValue, Hex, HurlFile, IntegerValue, JsonValue, KeyValue, LineTerminator, Method, MultilineString, MultipartParam, NaturalOption, Number, OptionKind, Placeholder, Predicate, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex, RegexValue, Request, Response, Section, SectionValue, StatusValue, Template, VariableDefinition, VariableValue, VersionValue, I64, U64, }; use hurl_core::types::{Count, Duration, DurationUnit, ToSource}; /// Lint a parsed `HurlFile` to a string. pub fn lint_hurl_file(file: &HurlFile) -> String { file.lint() } /// Lint something (usually a Hurl AST node) to a string. trait Lint { fn lint(&self) -> String; } impl Lint for Assert { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.query.lint()); if !self.filters.is_empty() { s.push(' '); let filters = self .filters .iter() .map(|(_, f)| f.value.lint()) .collect::>() .join(" "); s.push_str(&filters); } s.push(' '); s.push_str(&self.predicate.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); s } } impl Lint for Base64 { fn lint(&self) -> String { let mut s = String::new(); s.push_str("base64,"); s.push_str(self.source.as_str()); s.push(';'); s } } impl Lint for Body { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.value.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); s } } impl Lint for BooleanOption { fn lint(&self) -> String { match self { BooleanOption::Literal(value) => value.to_string(), BooleanOption::Placeholder(value) => value.lint(), } } } impl Lint for Bytes { fn lint(&self) -> String { match self { Bytes::Json(value) => value.lint(), Bytes::Xml(value) => value.clone(), Bytes::MultilineString(value) => value.lint(), Bytes::OnelineString(value) => value.lint(), Bytes::Base64(value) => value.lint(), Bytes::File(value) => value.lint(), Bytes::Hex(value) => value.lint(), } } } impl Lint for Capture { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.name.lint()); s.push(':'); s.push(' '); s.push_str(&self.query.lint()); if !self.filters.is_empty() { s.push(' '); let filters = self .filters .iter() .map(|(_, f)| f.value.lint()) .collect::>() .join(" "); s.push_str(&filters); } if self.redacted { s.push(' '); s.push_str("redact"); } s.push_str(&lint_lt(&self.line_terminator0, true)); s } } impl Lint for CertificateAttributeName { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for Cookie { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.name.lint()); s.push(':'); s.push(' '); s.push_str(&self.value.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); s } } impl Lint for CookiePath { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for Comment { fn lint(&self) -> String { format!("#{}", self.value.trim_end()) } } impl Lint for Count { fn lint(&self) -> String { self.to_string() } } impl Lint for CountOption { fn lint(&self) -> String { match self { CountOption::Literal(value) => value.lint(), CountOption::Placeholder(value) => value.lint(), } } } impl Lint for Entry { fn lint(&self) -> String { let mut s = String::new(); s.push_str(&self.request.lint()); if let Some(response) = &self.response { s.push_str(&response.lint()); } s } } impl Lint for EntryOption { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.kind.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); s } } impl Lint for File { fn lint(&self) -> String { let mut s = String::new(); s.push_str("file,"); s.push_str(&self.filename.lint()); s.push(';'); s } } impl Lint for FilenameParam { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.key.lint()); s.push(':'); s.push(' '); s.push_str(&self.value.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); s } } impl Lint for FilenameValue { fn lint(&self) -> String { let mut s = String::new(); s.push_str("file,"); s.push_str(&self.filename.lint()); s.push(';'); if let Some(content_type) = &self.content_type { s.push(' '); s.push_str(&content_type.lint()); } s } } impl Lint for FilterValue { fn lint(&self) -> String { let mut s = String::new(); s.push_str(self.identifier()); match self { FilterValue::Decode { encoding, .. } => { s.push(' '); s.push_str(&encoding.lint()); } FilterValue::Format { fmt, .. } => { s.push(' '); s.push_str(&fmt.lint()); } FilterValue::DateFormat { fmt, .. } => { s.push(' '); s.push_str(&fmt.lint()); } FilterValue::JsonPath { expr, .. } => { s.push(' '); s.push_str(&expr.lint()); } FilterValue::Nth { n, .. } => { s.push(' '); s.push_str(&n.lint()); } FilterValue::Regex { value, .. } => { s.push(' '); s.push_str(&value.lint()); } FilterValue::Replace { old_value, new_value, .. } => { s.push(' '); s.push_str(&old_value.lint()); s.push(' '); s.push_str(&new_value.lint()); } FilterValue::Split { sep, .. } => { s.push(' '); s.push_str(&sep.lint()); } FilterValue::ReplaceRegex { pattern, new_value, .. } => { s.push(' '); s.push_str(&pattern.lint()); s.push(' '); s.push_str(&new_value.lint()); } FilterValue::ToDate { fmt, .. } => { s.push(' '); s.push_str(&fmt.lint()); } FilterValue::UrlQueryParam { param, .. } => { s.push(' '); s.push_str(¶m.lint()); } FilterValue::XPath { expr, .. } => { s.push(' '); s.push_str(&expr.lint()); } FilterValue::Base64Decode | FilterValue::Base64Encode | FilterValue::Base64UrlSafeDecode | FilterValue::Base64UrlSafeEncode | FilterValue::Count | FilterValue::DaysAfterNow | FilterValue::DaysBeforeNow | FilterValue::First | FilterValue::HtmlEscape | FilterValue::HtmlUnescape | FilterValue::Last | FilterValue::Location | FilterValue::ToFloat | FilterValue::ToHex | FilterValue::ToInt | FilterValue::ToString | FilterValue::UrlDecode | FilterValue::UrlEncode | FilterValue::Utf8Decode | FilterValue::Utf8Encode => {} } s } } impl Lint for Hex { fn lint(&self) -> String { let mut s = String::new(); s.push_str("hex,"); s.push_str(self.source.as_str()); s.push(';'); s } } impl Lint for HurlFile { fn lint(&self) -> String { let mut s = String::new(); self.entries.iter().for_each(|e| s.push_str(&e.lint())); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s } } impl Lint for IntegerValue { fn lint(&self) -> String { match self { IntegerValue::Literal(value) => value.lint(), IntegerValue::Placeholder(value) => value.lint(), } } } impl Lint for I64 { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for JsonValue { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for KeyValue { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.key.lint()); s.push(':'); if !self.value.is_empty() { s.push(' '); s.push_str(&self.value.lint()); } s.push_str(&lint_lt(&self.line_terminator0, true)); s } } fn lint_lt(lt: &LineTerminator, is_trailing: bool) -> String { let mut s = String::new(); if let Some(comment) = <.comment { if is_trailing { // if line terminator is a trailing terminator, we keep the leading whitespaces // to keep user alignment. s.push_str(lt.space0.as_str()); } s.push_str(&comment.lint()); }; // We always terminate a file by a newline if lt.newline.value.is_empty() { s.push('\n'); } else { s.push_str(<.newline.value); } s } impl Lint for Method { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for MultipartParam { fn lint(&self) -> String { let mut s = String::new(); match self { MultipartParam::Param(param) => s.push_str(¶m.lint()), MultipartParam::FilenameParam(param) => s.push_str(¶m.lint()), } s } } impl Lint for MultilineString { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for NaturalOption { fn lint(&self) -> String { match self { NaturalOption::Literal(value) => value.lint(), NaturalOption::Placeholder(value) => value.lint(), } } } impl Lint for Number { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for OptionKind { fn lint(&self) -> String { let mut s = String::new(); s.push_str(self.identifier()); s.push(':'); s.push(' '); let value = match self { OptionKind::AwsSigV4(value) => value.lint(), OptionKind::CaCertificate(value) => value.lint(), OptionKind::ClientCert(value) => value.lint(), OptionKind::ClientKey(value) => value.lint(), OptionKind::Compressed(value) => value.lint(), OptionKind::ConnectTo(value) => value.lint(), OptionKind::ConnectTimeout(value) => { lint_duration_option(value, DurationUnit::MilliSecond) } OptionKind::Delay(value) => lint_duration_option(value, DurationUnit::MilliSecond), OptionKind::Header(value) => value.lint(), OptionKind::Http10(value) => value.lint(), OptionKind::Http11(value) => value.lint(), OptionKind::Http2(value) => value.lint(), OptionKind::Http3(value) => value.lint(), OptionKind::Insecure(value) => value.lint(), OptionKind::IpV4(value) => value.lint(), OptionKind::IpV6(value) => value.lint(), OptionKind::FollowLocation(value) => value.lint(), OptionKind::FollowLocationTrusted(value) => value.lint(), OptionKind::LimitRate(value) => value.lint(), OptionKind::MaxRedirect(value) => value.lint(), OptionKind::MaxTime(value) => lint_duration_option(value, DurationUnit::MilliSecond), OptionKind::Negotiate(value) => value.lint(), OptionKind::NetRc(value) => value.lint(), OptionKind::NetRcFile(value) => value.lint(), OptionKind::NetRcOptional(value) => value.lint(), OptionKind::Ntlm(value) => value.lint(), OptionKind::Output(value) => value.lint(), OptionKind::PathAsIs(value) => value.lint(), OptionKind::PinnedPublicKey(value) => value.lint(), OptionKind::Proxy(value) => value.lint(), OptionKind::Repeat(value) => value.lint(), OptionKind::Resolve(value) => value.lint(), OptionKind::Retry(value) => value.lint(), OptionKind::RetryInterval(value) => { lint_duration_option(value, DurationUnit::MilliSecond) } OptionKind::Skip(value) => value.lint(), OptionKind::UnixSocket(value) => value.lint(), OptionKind::User(value) => value.lint(), OptionKind::Variable(value) => value.lint(), OptionKind::Verbose(value) => value.lint(), OptionKind::VeryVerbose(value) => value.lint(), }; s.push_str(&value); s } } impl Lint for Query { fn lint(&self) -> String { let mut s = String::new(); s.push_str(self.value.identifier()); match &self.value { QueryValue::Status => {} QueryValue::Version => {} QueryValue::Url => {} QueryValue::Header { name, .. } => { s.push(' '); s.push_str(&name.lint()); } QueryValue::Cookie { expr, .. } => { s.push(' '); s.push_str(&expr.lint()); } QueryValue::Body => {} QueryValue::Xpath { expr, .. } => { s.push(' '); s.push_str(&expr.lint()); } QueryValue::Jsonpath { expr, .. } => { s.push(' '); s.push_str(&expr.lint()); } QueryValue::Regex { value, .. } => { s.push(' '); s.push_str(&value.lint()); } QueryValue::Variable { name, .. } => { s.push(' '); s.push_str(&name.lint()); } QueryValue::Duration => {} QueryValue::Bytes => {} QueryValue::Sha256 => {} QueryValue::Md5 => {} QueryValue::Certificate { attribute_name, .. } => { s.push(' '); s.push_str(&attribute_name.lint()); } QueryValue::Ip => {} QueryValue::Redirects => {} } s } } impl Lint for Placeholder { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for Predicate { fn lint(&self) -> String { let mut s = String::new(); if self.not { s.push_str("not"); s.push(' '); } s.push_str(&self.predicate_func.value.lint()); s } } impl Lint for PredicateFuncValue { fn lint(&self) -> String { let mut s = String::new(); s.push_str(self.identifier()); match self { PredicateFuncValue::Equal { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::NotEqual { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::GreaterThan { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::GreaterThanOrEqual { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::LessThan { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::LessThanOrEqual { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::StartWith { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::EndWith { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::Contain { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::Include { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::Match { value, .. } => { s.push(' '); s.push_str(&value.lint()); } PredicateFuncValue::Exist | PredicateFuncValue::IsBoolean | PredicateFuncValue::IsCollection | PredicateFuncValue::IsDate | PredicateFuncValue::IsEmpty | PredicateFuncValue::IsFloat | PredicateFuncValue::IsInteger | PredicateFuncValue::IsIpv4 | PredicateFuncValue::IsIpv6 | PredicateFuncValue::IsIsoDate | PredicateFuncValue::IsList | PredicateFuncValue::IsNumber | PredicateFuncValue::IsObject | PredicateFuncValue::IsString | PredicateFuncValue::IsUuid => {} } s } } impl Lint for PredicateValue { fn lint(&self) -> String { match self { PredicateValue::Base64(value) => value.lint(), PredicateValue::Bool(value) => value.to_string(), PredicateValue::File(value) => value.lint(), PredicateValue::Hex(value) => value.lint(), PredicateValue::MultilineString(value) => value.lint(), PredicateValue::Null => "null".to_string(), PredicateValue::Number(value) => value.lint(), PredicateValue::Placeholder(value) => value.lint(), PredicateValue::Regex(value) => value.lint(), PredicateValue::String(value) => value.lint(), } } } impl Lint for Regex { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for RegexValue { fn lint(&self) -> String { match self { RegexValue::Template(value) => value.lint(), RegexValue::Regex(value) => value.lint(), } } } impl Lint for Request { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.method.lint()); s.push(' '); s.push_str(&self.url.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); self.headers.iter().for_each(|h| s.push_str(&h.lint())); // We rewrite our file and reorder the various section. if let Some(section) = get_option_section(self) { s.push_str(§ion.lint()); } if let Some(section) = get_query_params_section(self) { s.push_str(§ion.lint()); } if let Some(section) = get_basic_auth_section(self) { s.push_str(§ion.lint()); } if let Some(section) = get_form_params_section(self) { s.push_str(§ion.lint()); } if let Some(section) = get_multipart_section(self) { s.push_str(§ion.lint()); } if let Some(section) = get_cookies_section(self) { s.push_str(§ion.lint()); } if let Some(body) = &self.body { s.push_str(&body.lint()); } s } } impl Lint for Response { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push_str(&self.version.value.lint()); s.push(' '); s.push_str(&self.status.value.lint()); s.push_str(&lint_lt(&self.line_terminator0, true)); self.headers.iter().for_each(|h| s.push_str(&h.lint())); if let Some(section) = get_captures_section(self) { s.push_str(§ion.lint()); } if let Some(section) = get_asserts_section(self) { s.push_str(§ion.lint()); } if let Some(body) = &self.body { s.push_str(&body.lint()); } s } } impl Lint for Section { fn lint(&self) -> String { let mut s = String::new(); self.line_terminators .iter() .for_each(|lt| s.push_str(&lint_lt(lt, false))); s.push('['); s.push_str(self.identifier()); s.push(']'); s.push_str(&lint_lt(&self.line_terminator0, true)); s.push_str(&self.value.lint()); s } } impl Lint for SectionValue { fn lint(&self) -> String { let mut s = String::new(); match self { SectionValue::QueryParams(params, _) => { params.iter().for_each(|p| s.push_str(&p.lint())); } SectionValue::BasicAuth(Some(auth)) => { s.push_str(&auth.lint()); } SectionValue::BasicAuth(_) => {} SectionValue::FormParams(params, _) => { params.iter().for_each(|p| s.push_str(&p.lint())); } SectionValue::MultipartFormData(params, _) => { params.iter().for_each(|p| s.push_str(&p.lint())); } SectionValue::Cookies(cookies) => { cookies.iter().for_each(|c| s.push_str(&c.lint())); } SectionValue::Captures(captures) => { captures.iter().for_each(|c| s.push_str(&c.lint())); } SectionValue::Asserts(asserts) => { asserts.iter().for_each(|a| s.push_str(&a.lint())); } SectionValue::Options(options) => { options.iter().for_each(|o| s.push_str(&o.lint())); } } s } } impl Lint for StatusValue { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for Template { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for U64 { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for VariableDefinition { fn lint(&self) -> String { let mut s = String::new(); s.push_str(&self.name); s.push('='); s.push_str(&self.value.lint()); s } } impl Lint for VariableValue { fn lint(&self) -> String { self.to_source().to_string() } } impl Lint for VersionValue { fn lint(&self) -> String { self.to_source().to_string() } } fn get_asserts_section(response: &Response) -> Option<&Section> { for s in &response.sections { if let SectionValue::Asserts(_) = s.value { return Some(s); } } None } fn get_captures_section(response: &Response) -> Option<&Section> { for s in &response.sections { if let SectionValue::Captures(_) = s.value { return Some(s); } } None } fn get_cookies_section(request: &Request) -> Option<&Section> { for s in &request.sections { if let SectionValue::Cookies(_) = s.value { return Some(s); } } None } fn get_form_params_section(request: &Request) -> Option<&Section> { for s in &request.sections { if let SectionValue::FormParams(_, _) = s.value { return Some(s); } } None } fn get_option_section(request: &Request) -> Option<&Section> { for s in &request.sections { if let SectionValue::Options(_) = s.value { return Some(s); } } None } fn get_multipart_section(request: &Request) -> Option<&Section> { for s in &request.sections { if let SectionValue::MultipartFormData(_, _) = s.value { return Some(s); } } None } fn get_query_params_section(request: &Request) -> Option<&Section> { for s in &request.sections { if let SectionValue::QueryParams(_, _) = s.value { return Some(s); } } None } fn get_basic_auth_section(request: &Request) -> Option<&Section> { for s in &request.sections { if let SectionValue::BasicAuth(Some(_)) = s.value { return Some(s); } } None } fn lint_duration_option(option: &DurationOption, default_unit: DurationUnit) -> String { match option { DurationOption::Literal(duration) => lint_duration(duration, default_unit), DurationOption::Placeholder(expr) => expr.lint(), } } fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> String { let mut s = String::new(); s.push_str(&duration.value.lint()); let unit = duration.unit.unwrap_or(default_unit); s.push_str(&unit.to_string()); s } #[cfg(test)] mod tests { use crate::linter::lint_hurl_file; use hurl_core::parser; #[test] fn test_lint_hurl_file() { let src = r#" # comment 1 #comment 2 with trailing spaces GET https://foo.com [Form] bar : baz [Options] location : true HTTP 200"#; let file = parser::parse_hurl_file(src).unwrap(); let linted = lint_hurl_file(&file); assert_eq!( linted, r#" # comment 1 #comment 2 with trailing spaces GET https://foo.com [Options] location: true [Form] bar: baz HTTP 200 "# ); } } hurlfmt-7.1.0/src/linter/rules.rs000064400000000000000000000570401046102023000151040ustar 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, FilenameParam, 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.redacted { one_whitespace() } else { empty_whitespace() }; 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, redacted: capture.redacted, 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, QueryValue::Redirects => QueryValue::Redirects, } } 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::FilenameParam(file_param) => { MultipartParam::FilenameParam(lint_file_param(file_param)) } } } fn lint_file_param(file_param: &FilenameParam) -> FilenameParam { 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(); FilenameParam { 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-7.1.0/src/main.rs000064400000000000000000000161701046102023000134000ustar 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, IsTerminal, 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 color = if let Some(value) = opts.color { value } else { io::stdout().is_terminal() }; let logger = Logger::new(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, 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: {filename}\n")); 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"); } } }