1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2025 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use std::io::{self, IsTerminal, Write};
19
use std::path::PathBuf;
20
use std::process;
21

            
22
use hurl_core::input::{Input, InputKind};
23
use hurl_core::text;
24
use hurlfmt::cli::options::{InputFormat, OptionsError, OutputFormat};
25
use hurlfmt::cli::Logger;
26
use hurlfmt::command::check::CheckError;
27
use hurlfmt::command::export::ExportError;
28
use hurlfmt::command::format::FormatError;
29
use hurlfmt::{cli, command};
30

            
31
const EXIT_OK: i32 = 0;
32
const EXIT_ERROR: i32 = 1;
33
const EXIT_INVALID_INPUT: i32 = 2;
34
const EXIT_LINT_ISSUE: i32 = 3;
35

            
36
/// Executes `hurlfmt` entry point.
37
213
fn main() {
38
213
    text::init_crate_colored();
39

            
40
213
    let opts = match cli::options::parse() {
41
207
        Ok(v) => v,
42
6
        Err(e) => match e {
43
6
            OptionsError::Info(message) => {
44
6
                print!("{message}");
45
6
                process::exit(EXIT_OK);
46
            }
47
            OptionsError::Error(message) => {
48
                eprintln!("{message}");
49
                process::exit(EXIT_ERROR);
50
            }
51
        },
52
    };
53
207
    let color = if let Some(value) = opts.color {
54
12
        value
55
    } else {
56
195
        io::stdout().is_terminal()
57
    };
58
207
    let logger = Logger::new(color);
59
207

            
60
207
    if opts.check {
61
6
        process_check_command(&opts.input_files, opts.output_file, &logger);
62
201
    } else if opts.in_place {
63
6
        process_format_command(&opts.input_files, &logger);
64
195
    } else {
65
195
        process_export_command(
66
195
            &opts.input_files,
67
195
            opts.output_file,
68
195
            &logger,
69
195
            &opts.input_format,
70
195
            &opts.output_format,
71
195
            opts.standalone,
72
195
            color,
73
195
        );
74
    }
75
}
76

            
77
6
fn process_check_command(input_files: &[Input], output_file: Option<PathBuf>, logger: &Logger) {
78
6
    let errors = command::check::run(input_files);
79
6
    if errors.is_empty() {
80
3
        process::exit(EXIT_OK);
81
    } else {
82
3
        let mut count = 0;
83
3
        let mut invalid_input = false;
84
3
        let mut output_all = String::new();
85

            
86
12
        for e in &errors {
87
9
            match e {
88
3
                CheckError::IO { filename, message } => {
89
3
                    logger.error(&format!(
90
3
                        "Input file {filename} can not be read - {message}"
91
3
                    ));
92
3
                    invalid_input = true;
93
                }
94
                CheckError::Parse {
95
3
                    content,
96
3
                    input_file,
97
3
                    error,
98
3
                } => {
99
3
                    logger.error_parsing(content, input_file, error);
100
3
                    invalid_input = true;
101
                }
102
3
                CheckError::Unformatted(filename) => {
103
3
                    output_all.push_str(&format!("would reformat: {}\n", filename));
104
3
                    count += 1;
105
                }
106
            }
107
        }
108
3
        if count > 0 {
109
3
            output_all.push_str(&format!(
110
3
                "{count} file{} would be reformatted",
111
3
                if count > 1 { "s" } else { "" }
112
            ));
113
        }
114
3
        write_output(&output_all, output_file, logger);
115
3
        if invalid_input {
116
3
            process::exit(EXIT_INVALID_INPUT);
117
        } else {
118
            process::exit(EXIT_LINT_ISSUE);
119
        }
120
    }
121
}
122

            
123
6
fn process_format_command(input_files: &[Input], logger: &Logger) {
124
6
    let mut input_files2 = vec![];
125
12
    for input_file in input_files {
126
6
        if let InputKind::File(path) = input_file.kind() {
127
6
            input_files2.push(path.clone());
128
6
        } else {
129
            logger.error("Standard input can be formatted in place!");
130
            process::exit(EXIT_INVALID_INPUT);
131
        }
132
    }
133

            
134
6
    let errors = command::format::run(&input_files2);
135
6
    if errors.is_empty() {
136
3
        process::exit(EXIT_OK);
137
    } else {
138
6
        for e in &errors {
139
3
            match e {
140
                FormatError::IO { filename, message } => {
141
                    logger.error(&format!(
142
                        "Input file {filename} can not be read - {message}"
143
                    ));
144
                }
145
                FormatError::Parse {
146
3
                    content,
147
3
                    input_file,
148
3
                    error,
149
3
                } => {
150
3
                    logger.error_parsing(content, input_file, error);
151
                }
152
            }
153
        }
154
3
        process::exit(EXIT_INVALID_INPUT);
155
    }
156
}
157

            
158
195
fn process_export_command(
159
195
    input_files: &[Input],
160
195
    output_file: Option<PathBuf>,
161
195
    logger: &Logger,
162
195
    input_format: &InputFormat,
163
195
    output_format: &OutputFormat,
164
195
    standalone: bool,
165
195
    color: bool,
166
195
) {
167
195
    let mut error = false;
168
195
    let mut output_all = String::new();
169
195
    let results = command::export::run(input_files, input_format, output_format, standalone, color);
170
393
    for result in &results {
171
198
        match result {
172
189
            Ok(output) => output_all.push_str(output),
173
9
            Err(e) => {
174
9
                error = true;
175
9
                match e {
176
3
                    ExportError::IO { filename, message } => {
177
3
                        logger.error(&format!(
178
3
                            "Input file {filename} can not be read - {message}"
179
3
                        ));
180
3
                        error = true;
181
                    }
182
                    ExportError::Parse {
183
6
                        content,
184
6
                        input_file,
185
6
                        error,
186
6
                    } => {
187
6
                        logger.error_parsing(content, input_file, error);
188
                    }
189
                    ExportError::Curl(s) => logger.error(&format!("error curl {s} d")),
190
                }
191
            }
192
        }
193
    }
194
195
    write_output(&output_all, output_file, logger);
195
195

            
196
195
    if error {
197
9
        process::exit(EXIT_INVALID_INPUT);
198
    } else {
199
186
        process::exit(EXIT_OK);
200
    }
201
}
202

            
203
198
fn write_output(content: &str, filename: Option<PathBuf>, logger: &Logger) {
204
198
    let content = if !content.ends_with('\n') {
205
129
        format!("{content}\n")
206
    } else {
207
69
        content.to_string()
208
    };
209

            
210
198
    let bytes = content.into_bytes();
211
198

            
212
198
    match filename {
213
        None => {
214
198
            let stdout = io::stdout();
215
198
            let mut handle = stdout.lock();
216

            
217
198
            if let Err(why) = handle.write_all(bytes.as_slice()) {
218
                logger.error(&format!("Issue writing to stdout: {why}"));
219
                process::exit(EXIT_ERROR);
220
            }
221
        }
222
        Some(path_buf) => {
223
            let mut file = match std::fs::File::create(&path_buf) {
224
                Err(why) => {
225
                    eprintln!("Issue writing to {}: {:?}", path_buf.display(), why);
226
                    process::exit(EXIT_ERROR);
227
                }
228
                Ok(file) => file,
229
            };
230
            file.write_all(bytes.as_slice())
231
                .expect("writing bytes to file");
232
        }
233
    }
234
}