Refacto options im hurlfmt

This commit is contained in:
Fabrice Reix 2023-03-10 11:10:40 +01:00
parent 3cddc20f34
commit 6d40373c71
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
7 changed files with 371 additions and 215 deletions

View File

@ -18,6 +18,7 @@
use colored::*;
use hurl_core::error::Error;
use hurl_core::parser;
use std::path::PathBuf;
use crate::linter;
@ -32,7 +33,7 @@ pub fn make_logger_error_message(color: bool) -> impl Fn(bool, &str) {
pub fn make_logger_parser_error(
lines: Vec<String>,
color: bool,
filename: Option<String>,
filename: Option<PathBuf>,
) -> impl Fn(&parser::Error, bool) {
move |error: &parser::Error, warning: bool| {
log_error(lines.clone(), color, filename.clone(), error, warning)
@ -42,7 +43,7 @@ pub fn make_logger_parser_error(
pub fn make_logger_linter_error(
lines: Vec<String>,
color: bool,
filename: Option<String>,
filename: Option<PathBuf>,
) -> impl Fn(&linter::Error, bool) {
move |error: &linter::Error, warning: bool| {
log_error(lines.clone(), color, filename.clone(), error, warning)
@ -76,7 +77,7 @@ fn log_verbose(verbose: bool, message: &str) {
fn log_error(
lines: Vec<String>,
color: bool,
filename: Option<String>,
filename: Option<PathBuf>,
error: &dyn Error,
warning: bool,
) {
@ -106,7 +107,7 @@ fn log_error(
eprintln!(
"{}--> {}:{}:{}",
" ".repeat(line_number_size).as_str(),
filename,
filename.display(),
error.source_info().start.line,
error.source_info().start.column,
);

View File

@ -24,8 +24,7 @@ pub use self::logger::{
mod fs;
mod logger;
mod options;
pub use self::options::{get_string, get_strings, has_flag};
pub mod options;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CliError {

View File

@ -1,32 +0,0 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 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;
pub fn get_string(matches: &ArgMatches, name: &str) -> Option<String> {
matches.get_one::<String>(name).map(|x| x.to_string())
}
pub fn get_strings(matches: &ArgMatches, name: &str) -> Option<Vec<String>> {
matches
.get_many::<String>(name)
.map(|v| v.map(|x| x.to_string()).collect())
}
pub fn has_flag(matches: &ArgMatches, name: &str) -> bool {
matches.get_one::<bool>(name) == Some(&true)
}

View File

@ -0,0 +1,94 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 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::ArgAction;
pub fn check() -> clap::Arg {
clap::Arg::new("check")
.long("check")
.help("Run in 'check' mode")
.action(ArgAction::SetTrue)
.conflicts_with("format")
.conflicts_with("output")
}
pub fn color() -> clap::Arg {
clap::Arg::new("color")
.long("color")
.help("Colorize Output")
.action(ArgAction::SetTrue)
.conflicts_with("no_color")
.conflicts_with("in_place")
}
pub fn format() -> clap::Arg {
clap::Arg::new("format")
.long("format")
.value_name("FORMAT")
.help("Specify output format: hurl, json or html")
.conflicts_with("check")
.default_value("text")
.num_args(1)
}
pub fn in_place() -> clap::Arg {
clap::Arg::new("in_place")
.long("in-place")
.help("Modify file in place")
.action(ArgAction::SetTrue)
.conflicts_with("output")
.conflicts_with("color")
}
pub fn input_file() -> clap::Arg {
clap::Arg::new("INPUT")
.help("Sets the input file to use")
.required(false)
.index(1)
.num_args(1)
}
pub fn no_color() -> clap::Arg {
clap::Arg::new("no_color")
.long("no-color")
.help("Do not colorize output")
.action(ArgAction::SetTrue)
.conflicts_with("color")
}
pub fn no_format() -> clap::Arg {
clap::Arg::new("no_format")
.long("no-format")
.help("Do not format output")
.action(ArgAction::SetTrue)
}
pub fn output() -> clap::Arg {
clap::Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("Write to FILE instead of stdout")
.num_args(1)
}
pub fn standalone() -> clap::Arg {
clap::Arg::new("standalone")
.long("standalone")
.help("Standalone Html")
.action(ArgAction::SetTrue)
}

View File

@ -0,0 +1,110 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 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 super::OptionsError;
use crate::cli::options::OutputFormat;
use atty::Stream;
use clap::ArgMatches;
use std::path::{Path, PathBuf};
pub fn check(arg_matches: &ArgMatches) -> bool {
has_flag(arg_matches, "check")
}
pub fn color(arg_matches: &ArgMatches) -> bool {
if has_flag(arg_matches, "color") {
true
} else if has_flag(arg_matches, "no_color") || has_flag(arg_matches, "in_place") {
false
} else {
atty::is(Stream::Stdout)
}
}
pub fn output_format(arg_matches: &ArgMatches) -> Result<Option<OutputFormat>, OptionsError> {
if has_flag(arg_matches, "no_format") {
Ok(None)
} else {
match get_string(arg_matches, "format").unwrap().as_str() {
"text" => Ok(Some(OutputFormat::Hurl)),
"json" => Ok(Some(OutputFormat::Json)),
"html" => Ok(Some(OutputFormat::Html)),
v => Err(OptionsError::Error(format!("Invalid output format {v}"))),
}
}
}
pub fn in_place(arg_matches: &ArgMatches) -> Result<bool, OptionsError> {
if has_flag(arg_matches, "in_place") {
if get_string(arg_matches, "format") != Some("text".to_string()) {
Err(OptionsError::Error(
"You can use --in-place only text format!".to_string(),
))
} else if get_string(arg_matches, "input_file").is_none() {
Err(OptionsError::Error(
"You can not use --in-place with standard input stream!".to_string(),
))
} else {
Ok(true)
}
} else {
Ok(false)
}
}
pub fn input_file(arg_matches: &ArgMatches) -> Result<Option<PathBuf>, OptionsError> {
match get_string(arg_matches, "INPUT") {
None => Ok(None),
Some(s) => {
let path = Path::new(&s);
if path.exists() {
Ok(Some(path.to_path_buf()))
} else {
Err(OptionsError::Error(format!(
"input file {} does not exist",
path.display()
)))
}
}
}
}
pub fn output_file(arg_matches: &ArgMatches) -> Option<PathBuf> {
get_string(arg_matches, "output").map(|s| Path::new(&s).to_path_buf())
}
pub fn standalone(arg_matches: &ArgMatches) -> Result<bool, OptionsError> {
if has_flag(arg_matches, "standalone") {
if get_string(arg_matches, "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::<bool>(name) == Some(&true)
}
pub fn get_string(matches: &ArgMatches, name: &str) -> Option<String> {
matches.get_one::<String>(name).map(|x| x.to_string())
}

View File

@ -0,0 +1,103 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 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 atty::Stream;
use clap::ArgMatches;
use std::env;
use std::path::PathBuf;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Options {
pub check: bool,
pub color: bool,
pub in_place: bool,
pub input_file: Option<PathBuf>,
pub output_file: Option<PathBuf>,
pub output_format: Option<OutputFormat>,
pub standalone: bool,
}
#[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<clap::Error> 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<Options, OptionsError> {
let mut command = clap::Command::new("hurlfmt")
.version(clap::crate_version!())
.disable_colored_help(true)
.about("Format hurl FILE")
.arg(commands::check())
.arg(commands::color())
.arg(commands::format())
.arg(commands::in_place())
.arg(commands::input_file())
.arg(commands::no_color())
.arg(commands::no_format())
.arg(commands::output())
.arg(commands::standalone());
let arg_matches = command.try_get_matches_from_mut(env::args_os())?;
let opts = parse_matches(&arg_matches)?;
if opts.input_file.is_none() && atty::is(Stream::Stdin) {
let help = command.render_help().to_string();
return Err(OptionsError::Error(help));
}
Ok(opts)
}
fn parse_matches(arg_matches: &ArgMatches) -> Result<Options, OptionsError> {
let check = matches::check(arg_matches);
let color = matches::color(arg_matches);
let in_place = matches::in_place(arg_matches)?;
let input_file = matches::input_file(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_file,
output_file,
output_format,
standalone,
})
}

View File

@ -16,12 +16,11 @@
*
*/
use std::io::{self, Read, Write};
use std::path::Path;
use std::path::PathBuf;
use std::process;
use atty::Stream;
use clap::ArgAction;
use hurl_core::parser;
use hurlfmt::cli::options::{OptionsError, OutputFormat};
use hurlfmt::{cli, format, linter};
#[cfg(target_family = "unix")]
@ -36,130 +35,40 @@ pub fn init_colored() {
}
fn main() {
let mut app = clap::Command::new("hurlfmt")
.version(clap::crate_version!())
.disable_colored_help(true)
.about("Format hurl FILE")
.arg(
clap::Arg::new("INPUT")
.help("Sets the input file to use")
.required(false)
.index(1)
.num_args(1),
)
.arg(
clap::Arg::new("check")
.long("check")
.help("Run in 'check' mode")
.action(ArgAction::SetTrue)
.conflicts_with("format")
.conflicts_with("output"),
)
.arg(
clap::Arg::new("color")
.long("color")
.help("Colorize Output")
.action(ArgAction::SetTrue)
.conflicts_with("no_color")
.conflicts_with("in_place"),
)
.arg(
clap::Arg::new("format")
.long("format")
.value_name("FORMAT")
.help("Specify output format: text (default), json or html")
.conflicts_with("check")
.default_value("text")
.num_args(1),
)
.arg(
clap::Arg::new("in_place")
.long("in-place")
.help("Modify file in place")
.action(ArgAction::SetTrue)
.conflicts_with("output")
.conflicts_with("color"),
)
.arg(
clap::Arg::new("no_color")
.long("no-color")
.help("Do not colorize output")
.action(ArgAction::SetTrue)
.conflicts_with("color"),
)
.arg(
clap::Arg::new("no_format")
.long("no-format")
.help("Do not format output")
.action(ArgAction::SetTrue),
)
.arg(
clap::Arg::new("output")
.short('o')
.long("output")
.value_name("FILE")
.help("Write to FILE instead of stdout")
.num_args(1),
)
.arg(
clap::Arg::new("standalone")
.long("standalone")
.help("Standalone Html")
.action(ArgAction::SetTrue),
);
let opts = match cli::options::parse() {
Ok(v) => v,
Err(e) => match e {
OptionsError::Info(message) => {
eprintln!("{message}");
process::exit(0);
}
OptionsError::Error(message) => {
eprintln!("{message}");
process::exit(1);
}
},
};
let matches = app.clone().get_matches();
init_colored();
// Additional checks
if cli::has_flag(&matches, "standalone")
&& cli::get_string(&matches, "format") != Some("html".to_string())
{
eprintln!("use --standalone option only with html output");
process::exit(1);
}
let output_color = if cli::has_flag(&matches, "color") {
true
} else if cli::has_flag(&matches, "no_color") || cli::has_flag(&matches, "in_place") {
false
} else {
atty::is(Stream::Stdout)
};
let log_error_message = cli::make_logger_error_message(output_color);
let filename = match cli::get_string(&matches, "INPUT") {
None => "-".to_string(),
Some(v) => v,
};
if filename == "-" && atty::is(Stream::Stdin) {
if app.print_help().is_err() {
panic!("panic during printing help");
let log_error_message = cli::make_logger_error_message(opts.color);
let contents = if let Some(filename) = &opts.input_file {
match cli::read_to_string(&filename.display().to_string()) {
Ok(s) => s,
Err(e) => {
log_error_message(
false,
format!(
"Input file {} can not be read - {}",
filename.display(),
e.message
)
.as_str(),
);
process::exit(2);
}
}
println!();
process::exit(1);
} else if filename != "-" && !Path::new(&filename).exists() {
eprintln!("Input file {filename} does not exit!");
process::exit(1);
};
if cli::has_flag(&matches, "in_place") {
if filename == "-" {
log_error_message(
true,
"You can not use --in-place with standard input stream!",
);
process::exit(1);
};
if cli::get_string(&matches, "format") != Some("text".to_string()) {
log_error_message(true, "You can use --in-place only text format!");
process::exit(1);
};
}
let contents = if filename == "-" {
} else {
let mut contents = String::new();
if let Err(e) = io::stdin().read_to_string(&mut contents) {
log_error_message(
@ -169,17 +78,6 @@ fn main() {
process::exit(2);
}
contents
} else {
match cli::read_to_string(&filename) {
Ok(s) => s,
Err(e) => {
log_error_message(
false,
format!("Input stream can not be read - {}", e.message).as_str(),
);
process::exit(2);
}
}
};
let lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
@ -188,65 +86,49 @@ fn main() {
.collect();
let lines: Vec<String> = lines.iter().map(|s| (*s).to_string()).collect();
let optional_filename = if filename.is_empty() {
None
} else {
Some(filename.to_string())
};
let output_file = if cli::has_flag(&matches, "in_place") {
Some(filename)
} else {
cli::get_string(&matches, "output")
};
let log_parser_error =
cli::make_logger_parser_error(lines.clone(), output_color, optional_filename.clone());
let log_linter_error = cli::make_logger_linter_error(lines, output_color, optional_filename);
cli::make_logger_parser_error(lines.clone(), opts.color, opts.input_file.clone());
let log_linter_error =
cli::make_logger_linter_error(lines, opts.color, opts.input_file.clone());
match parser::parse_hurl_file(&contents) {
Err(e) => {
log_parser_error(&e, false);
process::exit(2);
}
Ok(hurl_file) => {
if cli::has_flag(&matches, "check") {
if opts.check {
for e in linter::check_hurl_file(&hurl_file).iter() {
log_linter_error(e, true);
}
process::exit(1);
} else {
let output = match cli::get_string(&matches, "format").unwrap().as_str() {
"text" => {
let hurl_file = if cli::has_flag(&matches, "no_format") {
hurl_file
} else {
linter::lint_hurl_file(&hurl_file)
let output = match opts.output_format {
None => format::format_text(hurl_file, opts.color),
Some(fmt) => {
let output = match fmt {
OutputFormat::Hurl => {
let hurl_file = linter::lint_hurl_file(&hurl_file);
format::format_text(hurl_file, opts.color)
}
OutputFormat::Json => format::format_json(&hurl_file),
OutputFormat::Html => {
hurl_core::format::format_html(&hurl_file, opts.standalone)
}
};
format::format_text(hurl_file, output_color)
}
"json" => format::format_json(&hurl_file),
"html" => {
let standalone = cli::has_flag(&matches, "standalone");
hurl_core::format::format_html(&hurl_file, standalone)
}
"ast" => format!("{hurl_file:#?}"),
_ => {
eprintln!("Invalid output option - expecting text, html or json");
process::exit(1);
if !output.ends_with('\n') {
format!("{output}\n")
} else {
output
}
}
};
let output = if !output.ends_with('\n') && !cli::has_flag(&matches, "no_format") {
format!("{output}\n")
} else {
output
};
write_output(output.into_bytes(), output_file.as_deref());
write_output(output.into_bytes(), opts.output_file);
}
}
}
}
fn write_output(bytes: Vec<u8>, filename: Option<&str>) {
fn write_output(bytes: Vec<u8>, filename: Option<PathBuf>) {
match filename {
None => {
let stdout = io::stdout();
@ -256,11 +138,10 @@ fn write_output(bytes: Vec<u8>, filename: Option<&str>) {
.write_all(bytes.as_slice())
.expect("writing bytes to console");
}
Some(filename) => {
let path = Path::new(filename);
let mut file = match std::fs::File::create(path) {
Some(path_buf) => {
let mut file = match std::fs::File::create(&path_buf) {
Err(why) => {
eprintln!("Issue writing to {}: {:?}", path.display(), why);
eprintln!("Issue writing to {}: {:?}", path_buf.display(), why);
std::process::exit(1);
}
Ok(file) => file,