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 hurl_core::ast::visit::Visitor;
19
use hurl_core::ast::{
20
    Comment, CookiePath, FilterValue, HurlFile, JsonValue, Method, MultilineString, Number,
21
    Placeholder, PredicateFuncValue, QueryValue, Regex, StatusValue, Template, VersionValue,
22
    Whitespace, U64,
23
};
24
use hurl_core::text::{Format, Style, StyledString};
25
use hurl_core::typing::{DurationUnit, SourceString, ToSource};
26

            
27
/// Format a Hurl `file` to text, using ANSI escape code if `color` is true.
28
84
pub fn format(file: &HurlFile, color: bool) -> String {
29
84
    let format = if color { Format::Ansi } else { Format::Plain };
30
84
    let mut fmt = TextFormatter::new();
31
84
    fmt.format(file, format)
32
}
33

            
34
/// A formatter for text and terminal export.
35
struct TextFormatter {
36
    buffer: StyledString,
37
}
38

            
39
impl TextFormatter {
40
    /// Creates a new text formatter supporting ANSI escape code coloring.
41
84
    fn new() -> Self {
42
84
        TextFormatter {
43
84
            buffer: StyledString::new(),
44
        }
45
    }
46

            
47
84
    fn format(&mut self, file: &HurlFile, format: Format) -> String {
48
84
        self.buffer.clear();
49
84
        self.visit_hurl_file(file);
50
84
        self.buffer.to_string(format)
51
    }
52
}
53

            
54
impl Visitor for TextFormatter {
55
15
    fn visit_base64_value(&mut self, _value: &[u8], source: &SourceString) {
56
15
        self.buffer.push_with(source.as_str(), Style::new().green());
57
    }
58

            
59
69
    fn visit_bool(&mut self, value: bool) {
60
69
        let value = value.to_string();
61
69
        self.buffer.push_with(value.as_str(), Style::new().cyan());
62
    }
63

            
64
6
    fn visit_cookie_path(&mut self, path: &CookiePath) {
65
6
        let value = path.to_source();
66
6
        self.buffer.push_with(value.as_str(), Style::new().green());
67
    }
68

            
69
288
    fn visit_comment(&mut self, comment: &Comment) {
70
288
        let value = comment.to_source();
71
288
        self.buffer
72
288
            .push_with(value.as_str(), Style::new().bright_black());
73
    }
74

            
75
24
    fn visit_duration_unit(&mut self, unit: DurationUnit) {
76
24
        let value = unit.to_string();
77
24
        self.buffer.push_with(&value, Style::new().cyan());
78
    }
79

            
80
60
    fn visit_filename(&mut self, filename: &Template) {
81
60
        let value = filename.to_source();
82
60
        self.buffer.push_with(value.as_str(), Style::new().green());
83
    }
84

            
85
126
    fn visit_filter_kind(&mut self, kind: &FilterValue) {
86
126
        let value = kind.identifier();
87
126
        self.buffer.push_with(value, Style::new().yellow());
88
    }
89

            
90
36
    fn visit_hex_value(&mut self, _value: &[u8], source: &SourceString) {
91
36
        self.buffer.push_with(source.as_str(), Style::new().green());
92
    }
93

            
94
9
    fn visit_i64(&mut self, n: i64) {
95
9
        let value = n.to_string();
96
9
        self.buffer.push_with(&value, Style::new().cyan());
97
    }
98

            
99
6
    fn visit_json_body(&mut self, json: &JsonValue) {
100
6
        let value = json.to_source();
101
6
        self.buffer.push_with(value.as_str(), Style::new().green());
102
    }
103

            
104
621
    fn visit_literal(&mut self, lit: &'static str) {
105
621
        self.buffer.push(lit);
106
    }
107

            
108
270
    fn visit_method(&mut self, method: &Method) {
109
270
        let value = method.to_source();
110
270
        self.buffer.push_with(value.as_str(), Style::new().yellow());
111
    }
112

            
113
60
    fn visit_multiline_string(&mut self, string: &MultilineString) {
114
60
        let value = string.to_source();
115
60
        self.buffer.push_with(value.as_str(), Style::new().green());
116
    }
117

            
118
3
    fn visit_not(&mut self, identifier: &'static str) {
119
3
        self.buffer.push_with(identifier, Style::new().yellow());
120
    }
121

            
122
6
    fn visit_null(&mut self, identifier: &'static str) {
123
6
        self.buffer.push_with(identifier, Style::new().cyan());
124
    }
125

            
126
114
    fn visit_number(&mut self, number: &Number) {
127
114
        let value = number.to_source();
128
114
        self.buffer.push_with(value.as_str(), Style::new().cyan());
129
    }
130

            
131
75
    fn visit_placeholder(&mut self, placeholder: &Placeholder) {
132
75
        let value = placeholder.to_source();
133
75
        self.buffer.push_with(value.as_str(), Style::new().green());
134
    }
135

            
136
360
    fn visit_predicate_kind(&mut self, kind: &PredicateFuncValue) {
137
360
        let value = kind.identifier();
138
360
        self.buffer.push_with(value, Style::new().yellow());
139
    }
140

            
141
378
    fn visit_query_kind(&mut self, kind: &QueryValue) {
142
378
        let value = kind.identifier();
143
378
        self.buffer.push_with(value, Style::new().cyan());
144
    }
145

            
146
12
    fn visit_regex(&mut self, regex: &Regex) {
147
12
        let value = regex.to_source();
148
12
        self.buffer.push_with(value.as_str(), Style::new().green());
149
    }
150

            
151
114
    fn visit_status(&mut self, value: &StatusValue) {
152
114
        let value = value.to_string();
153
114
        self.buffer.push(&value);
154
    }
155

            
156
327
    fn visit_string(&mut self, value: &str) {
157
327
        self.buffer.push_with(value, Style::new().green());
158
    }
159

            
160
126
    fn visit_section_header(&mut self, name: &str) {
161
126
        self.buffer.push_with(name, Style::new().magenta());
162
    }
163

            
164
828
    fn visit_template(&mut self, template: &Template) {
165
828
        let value = template.to_source();
166
828
        self.buffer.push_with(value.as_str(), Style::new().green());
167
    }
168

            
169
270
    fn visit_url(&mut self, url: &Template) {
170
270
        let value = url.to_source();
171
270
        self.buffer.push_with(value.as_str(), Style::new().green());
172
    }
173

            
174
27
    fn visit_u64(&mut self, n: &U64) {
175
27
        let value = n.to_source();
176
27
        self.buffer.push_with(value.as_str(), Style::new().cyan());
177
    }
178

            
179
15
    fn visit_usize(&mut self, n: usize) {
180
15
        let value = n.to_string();
181
15
        self.buffer.push_with(value.as_str(), Style::new().cyan());
182
    }
183

            
184
27
    fn visit_variable_name(&mut self, name: &str) {
185
27
        self.buffer.push(name);
186
    }
187

            
188
114
    fn visit_version(&mut self, value: &VersionValue) {
189
114
        let value = value.to_string();
190
114
        self.buffer.push(&value);
191
    }
192

            
193
3
    fn visit_xml_body(&mut self, xml: &str) {
194
3
        self.buffer.push_with(xml, Style::new().green());
195
    }
196

            
197
7452
    fn visit_whitespace(&mut self, ws: &Whitespace) {
198
7452
        self.buffer.push(ws.as_str());
199
    }
200
}
201

            
202
#[cfg(test)]
203
mod tests {
204
    use crate::format::text::TextFormatter;
205
    use hurl_core::parser::parse_hurl_file;
206
    use hurl_core::text::Format;
207

            
208
    #[test]
209
    fn format_hurl_file() {
210
        // For the crate colored to output ANSI escape code in test environment.
211
        hurl_core::text::init_crate_colored();
212

            
213
        let src = r#"
214
GET https://foo.com
215
header1: value1
216
header2: value2
217
[Form]
218
foo: bar
219
baz: 123
220
HTTP 200
221
[Asserts]
222
jsonpath "$.name" == "toto"
223
"#;
224
        let file = parse_hurl_file(src).unwrap();
225
        let mut fmt = TextFormatter::new();
226
        let dst = fmt.format(&file, Format::Plain);
227
        assert_eq!(src, dst);
228

            
229
        let dst = fmt.format(&file, Format::Ansi);
230
        assert_eq!(
231
            dst,
232
            r#"
233
GET https://foo.com
234
header1: value1
235
header2: value2
236
[Form]
237
foo: bar
238
baz: 123
239
HTTP 200
240
[Asserts]
241
jsonpath "$.name" == "toto"
242
"#
243
        );
244
    }
245
}