1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2026 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::Logger;
25
use hurlfmt::cli::options::{InputFormat, OptionsError, OutputFormat};
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
144
fn main() {
38
144
    text::init_crate_colored();
39

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

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

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

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

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

            
134
4
    let errors = command::format::run(&input_files2);
135
4
    if errors.is_empty() {
136
2
        process::exit(EXIT_OK);
137
    } else {
138
2
        for e in &errors {
139
2
            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
2
                    content,
147
2
                    input_file,
148
2
                    error,
149
2
                } => {
150
2
                    logger.error_parsing(content, input_file, error);
151
                }
152
            }
153
        }
154
2
        process::exit(EXIT_INVALID_INPUT);
155
    }
156
}
157

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

            
196
132
    if error {
197
8
        process::exit(EXIT_INVALID_INPUT);
198
    } else {
199
124
        process::exit(EXIT_OK);
200
    }
201
}
202

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

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

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

            
217
134
            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
}