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, 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
204
fn main() {
38
204
    text::init_crate_colored();
39

            
40
204
    let opts = match cli::options::parse() {
41
198
        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

            
54
198
    let logger = Logger::new(opts.color);
55
198

            
56
198
    if opts.check {
57
6
        process_check_command(&opts.input_files, opts.output_file, &logger);
58
192
    } else if opts.in_place {
59
6
        process_format_command(&opts.input_files, &logger);
60
186
    } else {
61
186
        process_export_command(
62
186
            &opts.input_files,
63
186
            opts.output_file,
64
186
            &logger,
65
186
            &opts.input_format,
66
186
            &opts.output_format,
67
186
            opts.standalone,
68
186
            opts.color,
69
186
        );
70
    }
71
}
72

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

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

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

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

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

            
192
186
    if error {
193
9
        process::exit(EXIT_INVALID_INPUT);
194
    } else {
195
177
        process::exit(EXIT_OK);
196
    }
197
}
198

            
199
189
fn write_output(content: &str, filename: Option<PathBuf>, logger: &Logger) {
200
189
    let content = if !content.ends_with('\n') {
201
123
        format!("{content}\n")
202
    } else {
203
66
        content.to_string()
204
    };
205

            
206
189
    let bytes = content.into_bytes();
207
189

            
208
189
    match filename {
209
        None => {
210
189
            let stdout = io::stdout();
211
189
            let mut handle = stdout.lock();
212

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