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 hurl_core::ast::{
19
    Assert, Base64, Body, BooleanOption, Bytes, Capture, CertificateAttributeName, Comment, Cookie,
20
    CookiePath, CountOption, Duration, DurationOption, Entry, EntryOption, File, FilenameParam,
21
    FilenameValue, FilterValue, Hex, HurlFile, I64, IntegerValue, JsonValue, KeyValue,
22
    LineTerminator, Method, MultilineString, MultipartParam, NaturalOption, Number, OptionKind,
23
    Placeholder, Predicate, PredicateFuncValue, PredicateValue, Query, QueryValue, Regex,
24
    RegexValue, Request, Response, Section, SectionValue, StatusValue, Template, U64,
25
    VariableDefinition, VariableValue, VerbosityOption, VersionValue,
26
};
27
use hurl_core::types::{Count, DurationUnit, ToSource};
28

            
29
/// Lint a parsed `HurlFile` to a string.
30
56
pub fn lint_hurl_file(file: &HurlFile) -> String {
31
56
    file.lint()
32
}
33

            
34
/// Lint something (usually a Hurl AST node) to a string.
35
trait Lint {
36
    fn lint(&self) -> String;
37
}
38

            
39
impl Lint for Assert {
40
256
    fn lint(&self) -> String {
41
256
        let mut s = String::new();
42
256
        self.line_terminators
43
256
            .iter()
44
256
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
45
256
        s.push_str(&self.query.lint());
46
256
        if !self.filters.is_empty() {
47
78
            s.push(' ');
48
78
            let filters = self
49
78
                .filters
50
78
                .iter()
51
96
                .map(|(_, f)| f.value.lint())
52
78
                .collect::<Vec<_>>()
53
78
                .join(" ");
54
78
            s.push_str(&filters);
55
        }
56
256
        s.push(' ');
57
256
        s.push_str(&self.predicate.lint());
58
256
        s.push_str(&lint_lt(&self.line_terminator0, true));
59
256
        s
60
    }
61
}
62

            
63
impl Lint for Base64 {
64
10
    fn lint(&self) -> String {
65
10
        let mut s = String::new();
66
10
        s.push_str("base64,");
67
10
        s.push_str(self.source.as_str());
68
10
        s.push(';');
69
10
        s
70
    }
71
}
72

            
73
impl Lint for Body {
74
72
    fn lint(&self) -> String {
75
72
        let mut s = String::new();
76
72
        self.line_terminators
77
72
            .iter()
78
72
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
79
72
        s.push_str(&self.value.lint());
80
72
        s.push_str(&lint_lt(&self.line_terminator0, true));
81
72
        s
82
    }
83
}
84

            
85
impl Lint for BooleanOption {
86
98
    fn lint(&self) -> String {
87
98
        match self {
88
58
            BooleanOption::Literal(value) => value.to_string(),
89
40
            BooleanOption::Placeholder(value) => value.lint(),
90
        }
91
    }
92
}
93

            
94
impl Lint for Bytes {
95
72
    fn lint(&self) -> String {
96
72
        match self {
97
4
            Bytes::Json(value) => value.lint(),
98
2
            Bytes::Xml(value) => value.clone(),
99
34
            Bytes::MultilineString(value) => value.lint(),
100
10
            Bytes::OnelineString(value) => value.lint(),
101
8
            Bytes::Base64(value) => value.lint(),
102
8
            Bytes::File(value) => value.lint(),
103
6
            Bytes::Hex(value) => value.lint(),
104
        }
105
    }
106
}
107

            
108
impl Lint for Capture {
109
12
    fn lint(&self) -> String {
110
12
        let mut s = String::new();
111
12
        self.line_terminators
112
12
            .iter()
113
12
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
114
12
        s.push_str(&self.name.lint());
115
12
        s.push(':');
116
12
        s.push(' ');
117
12
        s.push_str(&self.query.lint());
118
12
        if !self.filters.is_empty() {
119
2
            s.push(' ');
120
2
            let filters = self
121
2
                .filters
122
2
                .iter()
123
2
                .map(|(_, f)| f.value.lint())
124
2
                .collect::<Vec<_>>()
125
2
                .join(" ");
126
2
            s.push_str(&filters);
127
        }
128
12
        if self.redacted {
129
4
            s.push(' ');
130
4
            s.push_str("redact");
131
        }
132
12
        s.push_str(&lint_lt(&self.line_terminator0, true));
133
12
        s
134
    }
135
}
136

            
137
impl Lint for CertificateAttributeName {
138
22
    fn lint(&self) -> String {
139
22
        self.to_source().to_string()
140
    }
141
}
142

            
143
impl Lint for Cookie {
144
6
    fn lint(&self) -> String {
145
6
        let mut s = String::new();
146
6
        self.line_terminators
147
6
            .iter()
148
6
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
149
6
        s.push_str(&self.name.lint());
150
6
        s.push(':');
151
6
        s.push(' ');
152
6
        s.push_str(&self.value.lint());
153
6
        s.push_str(&lint_lt(&self.line_terminator0, true));
154
6
        s
155
    }
156
}
157

            
158
impl Lint for CookiePath {
159
6
    fn lint(&self) -> String {
160
6
        self.to_source().to_string()
161
    }
162
}
163

            
164
impl Lint for Comment {
165
212
    fn lint(&self) -> String {
166
212
        format!("#{}", self.value.trim_end())
167
    }
168
}
169

            
170
impl Lint for Count {
171
14
    fn lint(&self) -> String {
172
14
        self.to_string()
173
    }
174
}
175

            
176
impl Lint for CountOption {
177
20
    fn lint(&self) -> String {
178
20
        match self {
179
14
            CountOption::Literal(value) => value.lint(),
180
6
            CountOption::Placeholder(value) => value.lint(),
181
        }
182
    }
183
}
184

            
185
impl Lint for Entry {
186
190
    fn lint(&self) -> String {
187
190
        let mut s = String::new();
188
190
        s.push_str(&self.request.lint());
189
190
        if let Some(response) = &self.response {
190
76
            s.push_str(&response.lint());
191
        }
192
190
        s
193
    }
194
}
195

            
196
impl Lint for EntryOption {
197
242
    fn lint(&self) -> String {
198
242
        let mut s = String::new();
199
242
        self.line_terminators
200
242
            .iter()
201
242
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
202
242
        s.push_str(&self.kind.lint());
203
242
        s.push_str(&lint_lt(&self.line_terminator0, true));
204
242
        s
205
    }
206
}
207

            
208
impl Lint for File {
209
10
    fn lint(&self) -> String {
210
10
        let mut s = String::new();
211
10
        s.push_str("file,");
212
10
        s.push_str(&self.filename.lint());
213
10
        s.push(';');
214
10
        s
215
    }
216
}
217

            
218
impl Lint for FilenameParam {
219
4
    fn lint(&self) -> String {
220
4
        let mut s = String::new();
221
4
        self.line_terminators
222
4
            .iter()
223
4
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
224
4
        s.push_str(&self.key.lint());
225
4
        s.push(':');
226
4
        s.push(' ');
227
4
        s.push_str(&self.value.lint());
228
4
        s.push_str(&lint_lt(&self.line_terminator0, true));
229
4
        s
230
    }
231
}
232

            
233
impl Lint for FilenameValue {
234
4
    fn lint(&self) -> String {
235
4
        let mut s = String::new();
236
4
        s.push_str("file,");
237
4
        s.push_str(&self.filename.lint());
238
4
        s.push(';');
239
4
        if let Some(content_type) = &self.content_type {
240
2
            s.push(' ');
241
2
            s.push_str(&content_type.lint());
242
        }
243
4
        s
244
    }
245
}
246

            
247
impl Lint for FilterValue {
248
98
    fn lint(&self) -> String {
249
98
        let mut s = String::new();
250
98
        s.push_str(self.identifier());
251
98
        match self {
252
4
            FilterValue::CharsetDecode { encoding, .. } => {
253
4
                s.push(' ');
254
4
                s.push_str(&encoding.lint());
255
            }
256
2
            FilterValue::Decode { encoding, .. } => {
257
2
                s.push(' ');
258
2
                s.push_str(&encoding.lint());
259
            }
260
4
            FilterValue::Format { fmt, .. } => {
261
4
                s.push(' ');
262
4
                s.push_str(&fmt.lint());
263
            }
264
6
            FilterValue::DateFormat { fmt, .. } => {
265
6
                s.push(' ');
266
6
                s.push_str(&fmt.lint());
267
            }
268
6
            FilterValue::JsonPath { expr, .. } => {
269
6
                s.push(' ');
270
6
                s.push_str(&expr.lint());
271
            }
272
2
            FilterValue::Nth { n, .. } => {
273
2
                s.push(' ');
274
2
                s.push_str(&n.lint());
275
            }
276
2
            FilterValue::Regex { value, .. } => {
277
2
                s.push(' ');
278
2
                s.push_str(&value.lint());
279
            }
280
            FilterValue::Replace {
281
10
                old_value,
282
10
                new_value,
283
                ..
284
10
            } => {
285
10
                s.push(' ');
286
10
                s.push_str(&old_value.lint());
287
10
                s.push(' ');
288
10
                s.push_str(&new_value.lint());
289
            }
290
2
            FilterValue::Split { sep, .. } => {
291
2
                s.push(' ');
292
2
                s.push_str(&sep.lint());
293
            }
294
            FilterValue::ReplaceRegex {
295
2
                pattern, new_value, ..
296
2
            } => {
297
2
                s.push(' ');
298
2
                s.push_str(&pattern.lint());
299
2
                s.push(' ');
300
2
                s.push_str(&new_value.lint());
301
            }
302
2
            FilterValue::ToDate { fmt, .. } => {
303
2
                s.push(' ');
304
2
                s.push_str(&fmt.lint());
305
            }
306
2
            FilterValue::UrlQueryParam { param, .. } => {
307
2
                s.push(' ');
308
2
                s.push_str(&param.lint());
309
            }
310
2
            FilterValue::XPath { expr, .. } => {
311
2
                s.push(' ');
312
2
                s.push_str(&expr.lint());
313
            }
314
            FilterValue::Base64Decode
315
            | FilterValue::Base64Encode
316
            | FilterValue::Base64UrlSafeDecode
317
            | FilterValue::Base64UrlSafeEncode
318
            | FilterValue::Count
319
            | FilterValue::DaysAfterNow
320
            | FilterValue::DaysBeforeNow
321
            | FilterValue::First
322
            | FilterValue::HtmlEscape
323
            | FilterValue::HtmlUnescape
324
            | FilterValue::Last
325
            | FilterValue::Location
326
            | FilterValue::ToFloat
327
            | FilterValue::ToHex
328
            | FilterValue::ToInt
329
            | FilterValue::ToString
330
            | FilterValue::UrlDecode
331
            | FilterValue::UrlEncode
332
            | FilterValue::Utf8Decode
333
52
            | FilterValue::Utf8Encode => {}
334
        }
335
98
        s
336
    }
337
}
338

            
339
impl Lint for Hex {
340
24
    fn lint(&self) -> String {
341
24
        let mut s = String::new();
342
24
        s.push_str("hex,");
343
24
        s.push_str(self.source.as_str());
344
24
        s.push(';');
345
24
        s
346
    }
347
}
348

            
349
impl Lint for HurlFile {
350
56
    fn lint(&self) -> String {
351
56
        let mut s = String::new();
352
190
        self.entries.iter().for_each(|e| s.push_str(&e.lint()));
353
56
        self.line_terminators
354
56
            .iter()
355
56
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
356
56
        s
357
    }
358
}
359

            
360
impl Lint for IntegerValue {
361
2
    fn lint(&self) -> String {
362
2
        match self {
363
2
            IntegerValue::Literal(value) => value.lint(),
364
            IntegerValue::Placeholder(value) => value.lint(),
365
        }
366
    }
367
}
368

            
369
impl Lint for I64 {
370
2
    fn lint(&self) -> String {
371
2
        self.to_source().to_string()
372
    }
373
}
374

            
375
impl Lint for JsonValue {
376
4
    fn lint(&self) -> String {
377
4
        self.to_source().to_string()
378
    }
379
}
380

            
381
impl Lint for KeyValue {
382
88
    fn lint(&self) -> String {
383
88
        let mut s = String::new();
384
88
        self.line_terminators
385
88
            .iter()
386
88
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
387
88
        s.push_str(&self.key.lint());
388
88
        s.push(':');
389
88
        if !self.value.is_empty() {
390
84
            s.push(' ');
391
84
            s.push_str(&self.value.lint());
392
        }
393
88
        s.push_str(&lint_lt(&self.line_terminator0, true));
394
88
        s
395
    }
396
}
397

            
398
1222
fn lint_lt(lt: &LineTerminator, is_trailing: bool) -> String {
399
1222
    let mut s = String::new();
400
1222
    if let Some(comment) = &lt.comment {
401
212
        if is_trailing {
402
154
            // if line terminator is a trailing terminator, we keep the leading whitespaces
403
154
            // to keep user alignment.
404
154
            s.push_str(lt.space0.as_str());
405
        }
406
212
        s.push_str(&comment.lint());
407
1010
    };
408
    // We always terminate a file by a newline
409
1222
    if lt.newline.value.is_empty() {
410
2
        s.push('\n');
411
1220
    } else {
412
1220
        s.push_str(&lt.newline.value);
413
    }
414
1222
    s
415
}
416

            
417
impl Lint for Method {
418
190
    fn lint(&self) -> String {
419
190
        self.to_source().to_string()
420
    }
421
}
422

            
423
impl Lint for MultipartParam {
424
6
    fn lint(&self) -> String {
425
6
        let mut s = String::new();
426
6
        match self {
427
2
            MultipartParam::Param(param) => s.push_str(&param.lint()),
428
4
            MultipartParam::FilenameParam(param) => s.push_str(&param.lint()),
429
        }
430
6
        s
431
    }
432
}
433

            
434
impl Lint for MultilineString {
435
46
    fn lint(&self) -> String {
436
46
        self.to_source().to_string()
437
    }
438
}
439

            
440
impl Lint for NaturalOption {
441
4
    fn lint(&self) -> String {
442
4
        match self {
443
2
            NaturalOption::Literal(value) => value.lint(),
444
2
            NaturalOption::Placeholder(value) => value.lint(),
445
        }
446
    }
447
}
448

            
449
impl Lint for Number {
450
72
    fn lint(&self) -> String {
451
72
        self.to_source().to_string()
452
    }
453
}
454

            
455
impl Lint for OptionKind {
456
242
    fn lint(&self) -> String {
457
242
        let mut s = String::new();
458
242
        s.push_str(self.identifier());
459
242
        s.push(':');
460
242
        s.push(' ');
461
242
        let value = match self {
462
4
            OptionKind::AwsSigV4(value) => value.lint(),
463
4
            OptionKind::CaCertificate(value) => value.lint(),
464
6
            OptionKind::ClientCert(value) => value.lint(),
465
4
            OptionKind::ClientKey(value) => value.lint(),
466
4
            OptionKind::Compressed(value) => value.lint(),
467
4
            OptionKind::ConnectTo(value) => value.lint(),
468
4
            OptionKind::ConnectTimeout(value) => {
469
4
                lint_duration_option(value, DurationUnit::MilliSecond)
470
            }
471
12
            OptionKind::Delay(value) => lint_duration_option(value, DurationUnit::MilliSecond),
472
4
            OptionKind::Digest(value) => value.lint(),
473
4
            OptionKind::FailWithBody(value) => value.lint(),
474
4
            OptionKind::Header(value) => value.lint(),
475
4
            OptionKind::Http10(value) => value.lint(),
476
4
            OptionKind::Http11(value) => value.lint(),
477
4
            OptionKind::Http2(value) => value.lint(),
478
2
            OptionKind::Http2PriorKnowledge(value) => value.lint(),
479
4
            OptionKind::Http3(value) => value.lint(),
480
6
            OptionKind::Insecure(value) => value.lint(),
481
4
            OptionKind::IpV4(value) => value.lint(),
482
4
            OptionKind::IpV6(value) => value.lint(),
483
6
            OptionKind::FollowLocation(value) => value.lint(),
484
4
            OptionKind::FollowLocationTrusted(value) => value.lint(),
485
4
            OptionKind::LimitRate(value) => value.lint(),
486
4
            OptionKind::MaxRedirect(value) => value.lint(),
487
4
            OptionKind::MaxTime(value) => lint_duration_option(value, DurationUnit::MilliSecond),
488
6
            OptionKind::Negotiate(value) => value.lint(),
489
4
            OptionKind::NetRc(value) => value.lint(),
490
4
            OptionKind::NetRcFile(value) => value.lint(),
491
4
            OptionKind::NetRcOptional(value) => value.lint(),
492
4
            OptionKind::NoHeader(value) => value.lint(),
493
2
            OptionKind::NoJsonpathCoercion(value) => value.lint(),
494
6
            OptionKind::Ntlm(value) => value.lint(),
495
4
            OptionKind::Output(value) => value.lint(),
496
4
            OptionKind::PathAsIs(value) => value.lint(),
497
4
            OptionKind::PinnedPublicKey(value) => value.lint(),
498
4
            OptionKind::Proxy(value) => value.lint(),
499
6
            OptionKind::Repeat(value) => value.lint(),
500
4
            OptionKind::Resolve(value) => value.lint(),
501
10
            OptionKind::Retry(value) => value.lint(),
502
8
            OptionKind::RetryInterval(value) => {
503
8
                lint_duration_option(value, DurationUnit::MilliSecond)
504
            }
505
4
            OptionKind::Skip(value) => value.lint(),
506
4
            OptionKind::TlsMax(value) => value.lint(),
507
4
            OptionKind::TlsMin(value) => value.lint(),
508
4
            OptionKind::UnixSocket(value) => value.lint(),
509
8
            OptionKind::User(value) => value.lint(),
510
18
            OptionKind::Variable(value) => value.lint(),
511
10
            OptionKind::Verbose(value) => value.lint(),
512
4
            OptionKind::Verbosity(value) => value.lint(),
513
4
            OptionKind::VeryVerbose(value) => value.lint(),
514
        };
515
242
        s.push_str(&value);
516
242
        s
517
    }
518
}
519

            
520
impl Lint for Query {
521
268
    fn lint(&self) -> String {
522
268
        let mut s = String::new();
523
268
        s.push_str(self.value.identifier());
524
268
        match &self.value {
525
4
            QueryValue::Status => {}
526
2
            QueryValue::Version => {}
527
2
            QueryValue::Url => {}
528
8
            QueryValue::Header { name, .. } => {
529
8
                s.push(' ');
530
8
                s.push_str(&name.lint());
531
            }
532
6
            QueryValue::Cookie { expr, .. } => {
533
6
                s.push(' ');
534
6
                s.push_str(&expr.lint());
535
            }
536
22
            QueryValue::Body => {}
537
2
            QueryValue::Xpath { expr, .. } => {
538
2
                s.push(' ');
539
2
                s.push_str(&expr.lint());
540
            }
541
162
            QueryValue::Jsonpath { expr, .. } => {
542
162
                s.push(' ');
543
162
                s.push_str(&expr.lint());
544
            }
545
2
            QueryValue::Regex { value, .. } => {
546
2
                s.push(' ');
547
2
                s.push_str(&value.lint());
548
            }
549
6
            QueryValue::Variable { name, .. } => {
550
6
                s.push(' ');
551
6
                s.push_str(&name.lint());
552
            }
553
2
            QueryValue::Duration => {}
554
18
            QueryValue::Bytes => {}
555
            QueryValue::RawBytes => {}
556
4
            QueryValue::Sha256 => {}
557
2
            QueryValue::Md5 => {}
558
22
            QueryValue::Certificate { attribute_name, .. } => {
559
22
                s.push(' ');
560
22
                s.push_str(&attribute_name.lint());
561
            }
562
4
            QueryValue::Ip => {}
563
            QueryValue::Redirects => {}
564
        }
565
268
        s
566
    }
567
}
568

            
569
impl Lint for Placeholder {
570
58
    fn lint(&self) -> String {
571
58
        self.to_source().to_string()
572
    }
573
}
574

            
575
impl Lint for Predicate {
576
256
    fn lint(&self) -> String {
577
256
        let mut s = String::new();
578
256
        if self.not {
579
2
            s.push_str("not");
580
2
            s.push(' ');
581
        }
582
256
        s.push_str(&self.predicate_func.value.lint());
583
256
        s
584
    }
585
}
586

            
587
impl Lint for PredicateFuncValue {
588
256
    fn lint(&self) -> String {
589
256
        let mut s = String::new();
590
256
        s.push_str(self.identifier());
591
256
        match self {
592
168
            PredicateFuncValue::Equal { value, .. } => {
593
168
                s.push(' ');
594
168
                s.push_str(&value.lint());
595
            }
596
6
            PredicateFuncValue::NotEqual { value, .. } => {
597
6
                s.push(' ');
598
6
                s.push_str(&value.lint());
599
            }
600
6
            PredicateFuncValue::GreaterThan { value, .. } => {
601
6
                s.push(' ');
602
6
                s.push_str(&value.lint());
603
            }
604
2
            PredicateFuncValue::GreaterThanOrEqual { value, .. } => {
605
2
                s.push(' ');
606
2
                s.push_str(&value.lint());
607
            }
608
8
            PredicateFuncValue::LessThan { value, .. } => {
609
8
                s.push(' ');
610
8
                s.push_str(&value.lint());
611
            }
612
2
            PredicateFuncValue::LessThanOrEqual { value, .. } => {
613
2
                s.push(' ');
614
2
                s.push_str(&value.lint());
615
            }
616
6
            PredicateFuncValue::StartWith { value, .. } => {
617
6
                s.push(' ');
618
6
                s.push_str(&value.lint());
619
            }
620
4
            PredicateFuncValue::EndWith { value, .. } => {
621
4
                s.push(' ');
622
4
                s.push_str(&value.lint());
623
            }
624
6
            PredicateFuncValue::Contain { value, .. } => {
625
6
                s.push(' ');
626
6
                s.push_str(&value.lint());
627
            }
628
2
            PredicateFuncValue::Include { value, .. } => {
629
2
                s.push(' ');
630
2
                s.push_str(&value.lint());
631
            }
632
6
            PredicateFuncValue::Match { value, .. } => {
633
6
                s.push(' ');
634
6
                s.push_str(&value.lint());
635
            }
636
            PredicateFuncValue::Exist
637
            | PredicateFuncValue::IsBoolean
638
            | PredicateFuncValue::IsCollection
639
            | PredicateFuncValue::IsDate
640
            | PredicateFuncValue::IsEmpty
641
            | PredicateFuncValue::IsFloat
642
            | PredicateFuncValue::IsInteger
643
            | PredicateFuncValue::IsIpv4
644
            | PredicateFuncValue::IsIpv6
645
            | PredicateFuncValue::IsIsoDate
646
            | PredicateFuncValue::IsList
647
            | PredicateFuncValue::IsNumber
648
            | PredicateFuncValue::IsObject
649
            | PredicateFuncValue::IsString
650
40
            | PredicateFuncValue::IsUuid => {}
651
        }
652
256
        s
653
    }
654
}
655

            
656
impl Lint for PredicateValue {
657
216
    fn lint(&self) -> String {
658
216
        match self {
659
2
            PredicateValue::Base64(value) => value.lint(),
660
2
            PredicateValue::Bool(value) => value.to_string(),
661
2
            PredicateValue::File(value) => value.lint(),
662
18
            PredicateValue::Hex(value) => value.lint(),
663
12
            PredicateValue::MultilineString(value) => value.lint(),
664
2
            PredicateValue::Null => "null".to_string(),
665
72
            PredicateValue::Number(value) => value.lint(),
666
2
            PredicateValue::Placeholder(value) => value.lint(),
667
4
            PredicateValue::Regex(value) => value.lint(),
668
100
            PredicateValue::String(value) => value.lint(),
669
        }
670
    }
671
}
672

            
673
impl Lint for Regex {
674
8
    fn lint(&self) -> String {
675
8
        self.to_source().to_string()
676
    }
677
}
678

            
679
impl Lint for RegexValue {
680
6
    fn lint(&self) -> String {
681
6
        match self {
682
2
            RegexValue::Template(value) => value.lint(),
683
4
            RegexValue::Regex(value) => value.lint(),
684
        }
685
    }
686
}
687

            
688
impl Lint for Request {
689
190
    fn lint(&self) -> String {
690
190
        let mut s = String::new();
691
190
        self.line_terminators
692
190
            .iter()
693
190
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
694
190
        s.push_str(&self.method.lint());
695
190
        s.push(' ');
696
190
        s.push_str(&self.url.lint());
697
190
        s.push_str(&lint_lt(&self.line_terminator0, true));
698

            
699
190
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
700

            
701
        // We rewrite our file and reorder the various section.
702
190
        if let Some(section) = get_option_section(self) {
703
28
            s.push_str(&section.lint());
704
        }
705
190
        if let Some(section) = get_query_params_section(self) {
706
8
            s.push_str(&section.lint());
707
        }
708
190
        if let Some(section) = get_basic_auth_section(self) {
709
2
            s.push_str(&section.lint());
710
        }
711
190
        if let Some(section) = get_form_params_section(self) {
712
4
            s.push_str(&section.lint());
713
        }
714
190
        if let Some(section) = get_multipart_section(self) {
715
4
            s.push_str(&section.lint());
716
        }
717
190
        if let Some(section) = get_cookies_section(self) {
718
6
            s.push_str(&section.lint());
719
        }
720
190
        if let Some(body) = &self.body {
721
40
            s.push_str(&body.lint());
722
        }
723
190
        s
724
    }
725
}
726

            
727
impl Lint for Response {
728
76
    fn lint(&self) -> String {
729
76
        let mut s = String::new();
730
76
        self.line_terminators
731
76
            .iter()
732
76
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
733
76
        s.push_str(&self.version.value.lint());
734
76
        s.push(' ');
735
76
        s.push_str(&self.status.value.lint());
736
76
        s.push_str(&lint_lt(&self.line_terminator0, true));
737

            
738
76
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
739

            
740
76
        if let Some(section) = get_captures_section(self) {
741
8
            s.push_str(&section.lint());
742
        }
743
76
        if let Some(section) = get_asserts_section(self) {
744
32
            s.push_str(&section.lint());
745
        }
746
76
        if let Some(body) = &self.body {
747
32
            s.push_str(&body.lint());
748
        }
749
76
        s
750
    }
751
}
752

            
753
impl Lint for Section {
754
92
    fn lint(&self) -> String {
755
92
        let mut s = String::new();
756
92
        self.line_terminators
757
92
            .iter()
758
92
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
759
92
        s.push('[');
760
92
        s.push_str(self.identifier());
761
92
        s.push(']');
762
92
        s.push_str(&lint_lt(&self.line_terminator0, true));
763
92
        s.push_str(&self.value.lint());
764
92
        s
765
    }
766
}
767

            
768
impl Lint for SectionValue {
769
92
    fn lint(&self) -> String {
770
92
        let mut s = String::new();
771
2
        match self {
772
8
            SectionValue::QueryParams(params, _) => {
773
12
                params.iter().for_each(|p| s.push_str(&p.lint()));
774
            }
775
2
            SectionValue::BasicAuth(Some(auth)) => {
776
2
                s.push_str(&auth.lint());
777
            }
778
            SectionValue::BasicAuth(_) => {}
779
4
            SectionValue::FormParams(params, _) => {
780
8
                params.iter().for_each(|p| s.push_str(&p.lint()));
781
            }
782
4
            SectionValue::MultipartFormData(params, _) => {
783
6
                params.iter().for_each(|p| s.push_str(&p.lint()));
784
            }
785
6
            SectionValue::Cookies(cookies) => {
786
6
                cookies.iter().for_each(|c| s.push_str(&c.lint()));
787
            }
788
8
            SectionValue::Captures(captures) => {
789
12
                captures.iter().for_each(|c| s.push_str(&c.lint()));
790
            }
791
32
            SectionValue::Asserts(asserts) => {
792
256
                asserts.iter().for_each(|a| s.push_str(&a.lint()));
793
            }
794
28
            SectionValue::Options(options) => {
795
242
                options.iter().for_each(|o| s.push_str(&o.lint()));
796
            }
797
        }
798
92
        s
799
    }
800
}
801

            
802
impl Lint for StatusValue {
803
76
    fn lint(&self) -> String {
804
76
        self.to_source().to_string()
805
    }
806
}
807

            
808
impl Lint for Template {
809
818
    fn lint(&self) -> String {
810
818
        self.to_source().to_string()
811
    }
812
}
813

            
814
impl Lint for U64 {
815
22
    fn lint(&self) -> String {
816
22
        self.to_source().to_string()
817
    }
818
}
819

            
820
impl Lint for VariableDefinition {
821
18
    fn lint(&self) -> String {
822
18
        let mut s = String::new();
823
18
        s.push_str(&self.name);
824
18
        s.push('=');
825
18
        s.push_str(&self.value.lint());
826
18
        s
827
    }
828
}
829

            
830
impl Lint for VariableValue {
831
18
    fn lint(&self) -> String {
832
18
        self.to_source().to_string()
833
    }
834
}
835

            
836
impl Lint for VersionValue {
837
76
    fn lint(&self) -> String {
838
76
        self.to_source().to_string()
839
    }
840
}
841

            
842
impl Lint for VerbosityOption {
843
4
    fn lint(&self) -> String {
844
4
        self.to_string()
845
    }
846
}
847

            
848
76
fn get_asserts_section(response: &Response) -> Option<&Section> {
849
76
    for s in &response.sections {
850
40
        if let SectionValue::Asserts(_) = s.value {
851
32
            return Some(s);
852
        }
853
    }
854
44
    None
855
}
856

            
857
76
fn get_captures_section(response: &Response) -> Option<&Section> {
858
76
    for s in &response.sections {
859
34
        if let SectionValue::Captures(_) = s.value {
860
8
            return Some(s);
861
        }
862
    }
863
68
    None
864
}
865

            
866
190
fn get_cookies_section(request: &Request) -> Option<&Section> {
867
190
    for s in &request.sections {
868
46
        if let SectionValue::Cookies(_) = s.value {
869
6
            return Some(s);
870
        }
871
    }
872
184
    None
873
}
874

            
875
190
fn get_form_params_section(request: &Request) -> Option<&Section> {
876
190
    for s in &request.sections {
877
40
        if let SectionValue::FormParams(_, _) = s.value {
878
4
            return Some(s);
879
        }
880
    }
881
186
    None
882
}
883

            
884
190
fn get_option_section(request: &Request) -> Option<&Section> {
885
190
    for s in &request.sections {
886
52
        if let SectionValue::Options(_) = s.value {
887
28
            return Some(s);
888
        }
889
    }
890
162
    None
891
}
892

            
893
190
fn get_multipart_section(request: &Request) -> Option<&Section> {
894
190
    for s in &request.sections {
895
44
        if let SectionValue::MultipartFormData(_, _) = s.value {
896
4
            return Some(s);
897
        }
898
    }
899
186
    None
900
}
901

            
902
190
fn get_query_params_section(request: &Request) -> Option<&Section> {
903
190
    for s in &request.sections {
904
34
        if let SectionValue::QueryParams(_, _) = s.value {
905
8
            return Some(s);
906
        }
907
    }
908
182
    None
909
}
910

            
911
190
fn get_basic_auth_section(request: &Request) -> Option<&Section> {
912
190
    for s in &request.sections {
913
2
        if let SectionValue::BasicAuth(Some(_)) = s.value {
914
2
            return Some(s);
915
        }
916
    }
917
188
    None
918
}
919

            
920
28
fn lint_duration_option(option: &DurationOption, default_unit: DurationUnit) -> String {
921
28
    match option {
922
20
        DurationOption::Literal(duration) => lint_duration(duration, default_unit),
923
8
        DurationOption::Placeholder(expr) => expr.lint(),
924
    }
925
}
926

            
927
20
fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> String {
928
20
    let mut s = String::new();
929
20
    s.push_str(&duration.value.lint());
930
20
    let unit = duration.unit.unwrap_or(default_unit);
931
20
    s.push_str(&unit.to_string());
932
20
    s
933
}
934

            
935
#[cfg(test)]
936
mod tests {
937
    use crate::linter::lint_hurl_file;
938
    use hurl_core::parser;
939

            
940
    #[test]
941
    fn test_lint_hurl_file() {
942
        let src = r#"
943
    # comment 1
944
  #comment 2 with trailing spaces    
945
  GET   https://foo.com
946
[Form]
947
  bar : baz
948
  [Options]
949
 location : true
950
HTTP   200"#;
951
        let file = parser::parse_hurl_file(src).unwrap();
952
        let linted = lint_hurl_file(&file);
953
        assert_eq!(
954
            linted,
955
            r#"
956
# comment 1
957
#comment 2 with trailing spaces
958
GET https://foo.com
959
[Options]
960
location: true
961
[Form]
962
bar: baz
963
HTTP 200
964
"#
965
        );
966
    }
967
}