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

            
29
/// Lint a parsed `HurlFile` to a string.
30
84
pub fn lint_hurl_file(file: &HurlFile) -> String {
31
84
    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
360
    fn lint(&self) -> String {
41
360
        let mut s = String::new();
42
360
        self.line_terminators
43
360
            .iter()
44
360
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
45
360
        s.push_str(&self.query.lint());
46
360
        if !self.filters.is_empty() {
47
102
            s.push(' ');
48
102
            let filters = self
49
102
                .filters
50
102
                .iter()
51
157
                .map(|(_, f)| f.value.lint())
52
102
                .collect::<Vec<_>>()
53
102
                .join(" ");
54
102
            s.push_str(&filters);
55
        }
56
360
        s.push(' ');
57
360
        s.push_str(&self.predicate.lint());
58
360
        s.push_str(&lint_lt(&self.line_terminator0, true));
59
360
        s
60
    }
61
}
62

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

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

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

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

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

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

            
143
impl Lint for Cookie {
144
9
    fn lint(&self) -> String {
145
9
        let mut s = String::new();
146
9
        self.line_terminators
147
9
            .iter()
148
9
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
149
9
        s.push_str(&self.name.lint());
150
9
        s.push(':');
151
9
        s.push(' ');
152
9
        s.push_str(&self.value.lint());
153
9
        s.push_str(&lint_lt(&self.line_terminator0, true));
154
9
        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
288
    fn lint(&self) -> String {
166
288
        format!("#{}", self.value.trim_end())
167
    }
168
}
169

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

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

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

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

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

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

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

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

            
329
impl Lint for Hex {
330
36
    fn lint(&self) -> String {
331
36
        let mut s = String::new();
332
36
        s.push_str("hex,");
333
36
        s.push_str(self.source.as_str());
334
36
        s.push(';');
335
36
        s
336
    }
337
}
338

            
339
impl Lint for HurlFile {
340
84
    fn lint(&self) -> String {
341
84
        let mut s = String::new();
342
298
        self.entries.iter().for_each(|e| s.push_str(&e.lint()));
343
84
        self.line_terminators
344
84
            .iter()
345
92
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
346
84
        s
347
    }
348
}
349

            
350
impl Lint for IntegerValue {
351
3
    fn lint(&self) -> String {
352
3
        match self {
353
3
            IntegerValue::Literal(value) => value.lint(),
354
            IntegerValue::Placeholder(value) => value.lint(),
355
        }
356
    }
357
}
358

            
359
impl Lint for I64 {
360
3
    fn lint(&self) -> String {
361
3
        self.to_source().to_string()
362
    }
363
}
364

            
365
impl Lint for JsonValue {
366
6
    fn lint(&self) -> String {
367
6
        self.to_source().to_string()
368
    }
369
}
370

            
371
impl Lint for KeyValue {
372
126
    fn lint(&self) -> String {
373
126
        let mut s = String::new();
374
126
        self.line_terminators
375
126
            .iter()
376
127
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
377
126
        s.push_str(&self.key.lint());
378
126
        s.push(':');
379
126
        if !self.value.is_empty() {
380
120
            s.push(' ');
381
120
            s.push_str(&self.value.lint());
382
        }
383
126
        s.push_str(&lint_lt(&self.line_terminator0, true));
384
126
        s
385
    }
386
}
387

            
388
1680
fn lint_lt(lt: &LineTerminator, is_trailing: bool) -> String {
389
1680
    let mut s = String::new();
390
1680
    if let Some(comment) = &lt.comment {
391
288
        if is_trailing {
392
204
            // if line terminator is a trailing terminator, we keep the leading whitespaces
393
204
            // to keep user alignment.
394
204
            s.push_str(lt.space0.as_str());
395
        }
396
288
        s.push_str(&comment.lint());
397
1392
    };
398
    // We always terminate a file by a newline
399
1680
    if lt.newline.value.is_empty() {
400
3
        s.push('\n');
401
1677
    } else {
402
1677
        s.push_str(&lt.newline.value);
403
    }
404
1680
    s
405
}
406

            
407
impl Lint for Method {
408
270
    fn lint(&self) -> String {
409
270
        self.to_source().to_string()
410
    }
411
}
412

            
413
impl Lint for MultipartParam {
414
9
    fn lint(&self) -> String {
415
9
        let mut s = String::new();
416
9
        match self {
417
3
            MultipartParam::Param(param) => s.push_str(&param.lint()),
418
6
            MultipartParam::FilenameParam(param) => s.push_str(&param.lint()),
419
        }
420
9
        s
421
    }
422
}
423

            
424
impl Lint for MultilineString {
425
60
    fn lint(&self) -> String {
426
60
        self.to_source().to_string()
427
    }
428
}
429

            
430
impl Lint for NaturalOption {
431
6
    fn lint(&self) -> String {
432
6
        match self {
433
3
            NaturalOption::Literal(value) => value.lint(),
434
3
            NaturalOption::Placeholder(value) => value.lint(),
435
        }
436
    }
437
}
438

            
439
impl Lint for Number {
440
108
    fn lint(&self) -> String {
441
108
        self.to_source().to_string()
442
    }
443
}
444

            
445
impl Lint for OptionKind {
446
291
    fn lint(&self) -> String {
447
291
        let mut s = String::new();
448
291
        s.push_str(self.identifier());
449
291
        s.push(':');
450
291
        s.push(' ');
451
291
        let value = match self {
452
6
            OptionKind::AwsSigV4(value) => value.lint(),
453
6
            OptionKind::CaCertificate(value) => value.lint(),
454
9
            OptionKind::ClientCert(value) => value.lint(),
455
6
            OptionKind::ClientKey(value) => value.lint(),
456
6
            OptionKind::Compressed(value) => value.lint(),
457
6
            OptionKind::ConnectTo(value) => value.lint(),
458
6
            OptionKind::ConnectTimeout(value) => {
459
6
                lint_duration_option(value, DurationUnit::MilliSecond)
460
            }
461
12
            OptionKind::Delay(value) => lint_duration_option(value, DurationUnit::MilliSecond),
462
6
            OptionKind::Header(value) => value.lint(),
463
6
            OptionKind::Http10(value) => value.lint(),
464
6
            OptionKind::Http11(value) => value.lint(),
465
6
            OptionKind::Http2(value) => value.lint(),
466
6
            OptionKind::Http3(value) => value.lint(),
467
9
            OptionKind::Insecure(value) => value.lint(),
468
6
            OptionKind::IpV4(value) => value.lint(),
469
6
            OptionKind::IpV6(value) => value.lint(),
470
9
            OptionKind::FollowLocation(value) => value.lint(),
471
6
            OptionKind::FollowLocationTrusted(value) => value.lint(),
472
6
            OptionKind::LimitRate(value) => value.lint(),
473
6
            OptionKind::MaxRedirect(value) => value.lint(),
474
6
            OptionKind::MaxTime(value) => lint_duration_option(value, DurationUnit::MilliSecond),
475
6
            OptionKind::NetRc(value) => value.lint(),
476
6
            OptionKind::NetRcFile(value) => value.lint(),
477
6
            OptionKind::NetRcOptional(value) => value.lint(),
478
6
            OptionKind::Output(value) => value.lint(),
479
6
            OptionKind::PathAsIs(value) => value.lint(),
480
6
            OptionKind::PinnedPublicKey(value) => value.lint(),
481
6
            OptionKind::Proxy(value) => value.lint(),
482
9
            OptionKind::Repeat(value) => value.lint(),
483
6
            OptionKind::Resolve(value) => value.lint(),
484
15
            OptionKind::Retry(value) => value.lint(),
485
12
            OptionKind::RetryInterval(value) => {
486
12
                lint_duration_option(value, DurationUnit::MilliSecond)
487
            }
488
6
            OptionKind::Skip(value) => value.lint(),
489
6
            OptionKind::UnixSocket(value) => value.lint(),
490
6
            OptionKind::User(value) => value.lint(),
491
27
            OptionKind::Variable(value) => value.lint(),
492
15
            OptionKind::Verbose(value) => value.lint(),
493
6
            OptionKind::VeryVerbose(value) => value.lint(),
494
        };
495
291
        s.push_str(&value);
496
291
        s
497
    }
498
}
499

            
500
impl Lint for Query {
501
378
    fn lint(&self) -> String {
502
378
        let mut s = String::new();
503
378
        s.push_str(self.value.identifier());
504
378
        match &self.value {
505
6
            QueryValue::Status => {}
506
3
            QueryValue::Version => {}
507
3
            QueryValue::Url => {}
508
12
            QueryValue::Header { name, .. } => {
509
12
                s.push(' ');
510
12
                s.push_str(&name.lint());
511
            }
512
6
            QueryValue::Cookie { expr, .. } => {
513
6
                s.push(' ');
514
6
                s.push_str(&expr.lint());
515
            }
516
33
            QueryValue::Body => {}
517
3
            QueryValue::Xpath { expr, .. } => {
518
3
                s.push(' ');
519
3
                s.push_str(&expr.lint());
520
            }
521
228
            QueryValue::Jsonpath { expr, .. } => {
522
228
                s.push(' ');
523
228
                s.push_str(&expr.lint());
524
            }
525
3
            QueryValue::Regex { value, .. } => {
526
3
                s.push(' ');
527
3
                s.push_str(&value.lint());
528
            }
529
9
            QueryValue::Variable { name, .. } => {
530
9
                s.push(' ');
531
9
                s.push_str(&name.lint());
532
            }
533
3
            QueryValue::Duration => {}
534
24
            QueryValue::Bytes => {}
535
6
            QueryValue::Sha256 => {}
536
3
            QueryValue::Md5 => {}
537
30
            QueryValue::Certificate { attribute_name, .. } => {
538
30
                s.push(' ');
539
30
                s.push_str(&attribute_name.lint());
540
            }
541
6
            QueryValue::Ip => {}
542
            QueryValue::Redirects => {}
543
        }
544
378
        s
545
    }
546
}
547

            
548
impl Lint for Placeholder {
549
75
    fn lint(&self) -> String {
550
75
        self.to_source().to_string()
551
    }
552
}
553

            
554
impl Lint for Predicate {
555
360
    fn lint(&self) -> String {
556
360
        let mut s = String::new();
557
360
        if self.not {
558
3
            s.push_str("not");
559
3
            s.push(' ');
560
        }
561
360
        s.push_str(&self.predicate_func.value.lint());
562
360
        s
563
    }
564
}
565

            
566
impl Lint for PredicateFuncValue {
567
360
    fn lint(&self) -> String {
568
360
        let mut s = String::new();
569
360
        s.push_str(self.identifier());
570
360
        match self {
571
237
            PredicateFuncValue::Equal { value, .. } => {
572
237
                s.push(' ');
573
237
                s.push_str(&value.lint());
574
            }
575
9
            PredicateFuncValue::NotEqual { value, .. } => {
576
9
                s.push(' ');
577
9
                s.push_str(&value.lint());
578
            }
579
9
            PredicateFuncValue::GreaterThan { value, .. } => {
580
9
                s.push(' ');
581
9
                s.push_str(&value.lint());
582
            }
583
3
            PredicateFuncValue::GreaterThanOrEqual { value, .. } => {
584
3
                s.push(' ');
585
3
                s.push_str(&value.lint());
586
            }
587
12
            PredicateFuncValue::LessThan { value, .. } => {
588
12
                s.push(' ');
589
12
                s.push_str(&value.lint());
590
            }
591
3
            PredicateFuncValue::LessThanOrEqual { value, .. } => {
592
3
                s.push(' ');
593
3
                s.push_str(&value.lint());
594
            }
595
9
            PredicateFuncValue::StartWith { value, .. } => {
596
9
                s.push(' ');
597
9
                s.push_str(&value.lint());
598
            }
599
6
            PredicateFuncValue::EndWith { value, .. } => {
600
6
                s.push(' ');
601
6
                s.push_str(&value.lint());
602
            }
603
9
            PredicateFuncValue::Contain { value, .. } => {
604
9
                s.push(' ');
605
9
                s.push_str(&value.lint());
606
            }
607
3
            PredicateFuncValue::Include { value, .. } => {
608
3
                s.push(' ');
609
3
                s.push_str(&value.lint());
610
            }
611
9
            PredicateFuncValue::Match { value, .. } => {
612
9
                s.push(' ');
613
9
                s.push_str(&value.lint());
614
            }
615
            PredicateFuncValue::IsInteger
616
            | PredicateFuncValue::IsFloat
617
            | PredicateFuncValue::IsBoolean
618
            | PredicateFuncValue::IsString
619
            | PredicateFuncValue::IsCollection
620
            | PredicateFuncValue::IsDate
621
            | PredicateFuncValue::IsIsoDate
622
            | PredicateFuncValue::Exist
623
            | PredicateFuncValue::IsEmpty
624
            | PredicateFuncValue::IsNumber
625
            | PredicateFuncValue::IsIpv4
626
51
            | PredicateFuncValue::IsIpv6 => {}
627
        }
628
360
        s
629
    }
630
}
631

            
632
impl Lint for PredicateValue {
633
309
    fn lint(&self) -> String {
634
309
        match self {
635
3
            PredicateValue::Base64(value) => value.lint(),
636
3
            PredicateValue::Bool(value) => value.to_string(),
637
3
            PredicateValue::File(value) => value.lint(),
638
27
            PredicateValue::Hex(value) => value.lint(),
639
18
            PredicateValue::MultilineString(value) => value.lint(),
640
3
            PredicateValue::Null => "null".to_string(),
641
108
            PredicateValue::Number(value) => value.lint(),
642
3
            PredicateValue::Placeholder(value) => value.lint(),
643
6
            PredicateValue::Regex(value) => value.lint(),
644
135
            PredicateValue::String(value) => value.lint(),
645
        }
646
    }
647
}
648

            
649
impl Lint for Regex {
650
12
    fn lint(&self) -> String {
651
12
        self.to_source().to_string()
652
    }
653
}
654

            
655
impl Lint for RegexValue {
656
9
    fn lint(&self) -> String {
657
9
        match self {
658
3
            RegexValue::Template(value) => value.lint(),
659
6
            RegexValue::Regex(value) => value.lint(),
660
        }
661
    }
662
}
663

            
664
impl Lint for Request {
665
270
    fn lint(&self) -> String {
666
270
        let mut s = String::new();
667
270
        self.line_terminators
668
270
            .iter()
669
330
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
670
270
        s.push_str(&self.method.lint());
671
270
        s.push(' ');
672
270
        s.push_str(&self.url.lint());
673
270
        s.push_str(&lint_lt(&self.line_terminator0, true));
674

            
675
295
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
676

            
677
        // We rewrite our file and reorder the various section.
678
270
        if let Some(section) = get_option_section(self) {
679
30
            s.push_str(&section.lint());
680
        }
681
270
        if let Some(section) = get_query_params_section(self) {
682
12
            s.push_str(&section.lint());
683
        }
684
270
        if let Some(section) = get_basic_auth_section(self) {
685
3
            s.push_str(&section.lint());
686
        }
687
270
        if let Some(section) = get_form_params_section(self) {
688
6
            s.push_str(&section.lint());
689
        }
690
270
        if let Some(section) = get_multipart_section(self) {
691
6
            s.push_str(&section.lint());
692
        }
693
270
        if let Some(section) = get_cookies_section(self) {
694
9
            s.push_str(&section.lint());
695
        }
696
270
        if let Some(body) = &self.body {
697
54
            s.push_str(&body.lint());
698
        }
699
270
        s
700
    }
701
}
702

            
703
impl Lint for Response {
704
114
    fn lint(&self) -> String {
705
114
        let mut s = String::new();
706
114
        self.line_terminators
707
114
            .iter()
708
116
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
709
114
        s.push_str(&self.version.value.lint());
710
114
        s.push(' ');
711
114
        s.push_str(&self.status.value.lint());
712
114
        s.push_str(&lint_lt(&self.line_terminator0, true));
713

            
714
119
        self.headers.iter().for_each(|h| s.push_str(&h.lint()));
715

            
716
114
        if let Some(section) = get_captures_section(self) {
717
12
            s.push_str(&section.lint());
718
        }
719
114
        if let Some(section) = get_asserts_section(self) {
720
48
            s.push_str(&section.lint());
721
        }
722
114
        if let Some(body) = &self.body {
723
45
            s.push_str(&body.lint());
724
        }
725
114
        s
726
    }
727
}
728

            
729
impl Lint for Section {
730
126
    fn lint(&self) -> String {
731
126
        let mut s = String::new();
732
126
        self.line_terminators
733
126
            .iter()
734
136
            .for_each(|lt| s.push_str(&lint_lt(lt, false)));
735
126
        s.push('[');
736
126
        s.push_str(self.identifier());
737
126
        s.push(']');
738
126
        s.push_str(&lint_lt(&self.line_terminator0, true));
739
126
        s.push_str(&self.value.lint());
740
126
        s
741
    }
742
}
743

            
744
impl Lint for SectionValue {
745
126
    fn lint(&self) -> String {
746
126
        let mut s = String::new();
747
3
        match self {
748
12
            SectionValue::QueryParams(params, _) => {
749
22
                params.iter().for_each(|p| s.push_str(&p.lint()));
750
            }
751
3
            SectionValue::BasicAuth(Some(auth)) => {
752
3
                s.push_str(&auth.lint());
753
            }
754
            SectionValue::BasicAuth(_) => {}
755
6
            SectionValue::FormParams(params, _) => {
756
14
                params.iter().for_each(|p| s.push_str(&p.lint()));
757
            }
758
6
            SectionValue::MultipartFormData(params, _) => {
759
11
                params.iter().for_each(|p| s.push_str(&p.lint()));
760
            }
761
9
            SectionValue::Cookies(cookies) => {
762
12
                cookies.iter().for_each(|c| s.push_str(&c.lint()));
763
            }
764
12
            SectionValue::Captures(captures) => {
765
22
                captures.iter().for_each(|c| s.push_str(&c.lint()));
766
            }
767
48
            SectionValue::Asserts(asserts) => {
768
376
                asserts.iter().for_each(|a| s.push_str(&a.lint()));
769
            }
770
30
            SectionValue::Options(options) => {
771
301
                options.iter().for_each(|o| s.push_str(&o.lint()));
772
            }
773
        }
774
126
        s
775
    }
776
}
777

            
778
impl Lint for StatusValue {
779
114
    fn lint(&self) -> String {
780
114
        self.to_source().to_string()
781
    }
782
}
783

            
784
impl Lint for Template {
785
1137
    fn lint(&self) -> String {
786
1137
        self.to_source().to_string()
787
    }
788
}
789

            
790
impl Lint for U64 {
791
27
    fn lint(&self) -> String {
792
27
        self.to_source().to_string()
793
    }
794
}
795

            
796
impl Lint for VariableDefinition {
797
27
    fn lint(&self) -> String {
798
27
        let mut s = String::new();
799
27
        s.push_str(&self.name);
800
27
        s.push('=');
801
27
        s.push_str(&self.value.lint());
802
27
        s
803
    }
804
}
805

            
806
impl Lint for VariableValue {
807
27
    fn lint(&self) -> String {
808
27
        self.to_source().to_string()
809
    }
810
}
811

            
812
impl Lint for VersionValue {
813
114
    fn lint(&self) -> String {
814
114
        self.to_source().to_string()
815
    }
816
}
817

            
818
114
fn get_asserts_section(response: &Response) -> Option<&Section> {
819
126
    for s in &response.sections {
820
60
        if let SectionValue::Asserts(_) = s.value {
821
48
            return Some(s);
822
        }
823
    }
824
66
    None
825
}
826

            
827
114
fn get_captures_section(response: &Response) -> Option<&Section> {
828
153
    for s in &response.sections {
829
51
        if let SectionValue::Captures(_) = s.value {
830
12
            return Some(s);
831
        }
832
    }
833
102
    None
834
}
835

            
836
270
fn get_cookies_section(request: &Request) -> Option<&Section> {
837
318
    for s in &request.sections {
838
57
        if let SectionValue::Cookies(_) = s.value {
839
9
            return Some(s);
840
        }
841
    }
842
261
    None
843
}
844

            
845
270
fn get_form_params_section(request: &Request) -> Option<&Section> {
846
312
    for s in &request.sections {
847
48
        if let SectionValue::FormParams(_, _) = s.value {
848
6
            return Some(s);
849
        }
850
    }
851
264
    None
852
}
853

            
854
270
fn get_option_section(request: &Request) -> Option<&Section> {
855
306
    for s in &request.sections {
856
66
        if let SectionValue::Options(_) = s.value {
857
30
            return Some(s);
858
        }
859
    }
860
240
    None
861
}
862

            
863
270
fn get_multipart_section(request: &Request) -> Option<&Section> {
864
318
    for s in &request.sections {
865
54
        if let SectionValue::MultipartFormData(_, _) = s.value {
866
6
            return Some(s);
867
        }
868
    }
869
264
    None
870
}
871

            
872
270
fn get_query_params_section(request: &Request) -> Option<&Section> {
873
297
    for s in &request.sections {
874
39
        if let SectionValue::QueryParams(_, _) = s.value {
875
12
            return Some(s);
876
        }
877
    }
878
258
    None
879
}
880

            
881
270
fn get_basic_auth_section(request: &Request) -> Option<&Section> {
882
321
    for s in &request.sections {
883
3
        if let SectionValue::BasicAuth(Some(_)) = s.value {
884
3
            return Some(s);
885
        }
886
    }
887
267
    None
888
}
889

            
890
36
fn lint_duration_option(option: &DurationOption, default_unit: DurationUnit) -> String {
891
36
    match option {
892
24
        DurationOption::Literal(duration) => lint_duration(duration, default_unit),
893
12
        DurationOption::Placeholder(expr) => expr.lint(),
894
    }
895
}
896

            
897
24
fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> String {
898
24
    let mut s = String::new();
899
24
    s.push_str(&duration.value.lint());
900
24
    let unit = duration.unit.unwrap_or(default_unit);
901
24
    s.push_str(&unit.to_string());
902
24
    s
903
}
904

            
905
#[cfg(test)]
906
mod tests {
907
    use crate::linter::lint_hurl_file;
908
    use hurl_core::parser;
909

            
910
    #[test]
911
    fn test_lint_hurl_file() {
912
        let src = r#"
913
    # comment 1
914
  #comment 2 with trailing spaces    
915
  GET   https://foo.com
916
[Form]
917
  bar : baz
918
  [Options]
919
 location : true
920
HTTP   200"#;
921
        let file = parser::parse_hurl_file(src).unwrap();
922
        let linted = lint_hurl_file(&file);
923
        assert_eq!(
924
            linted,
925
            r#"
926
# comment 1
927
#comment 2 with trailing spaces
928
GET https://foo.com
929
[Options]
930
location: true
931
[Form]
932
bar: baz
933
HTTP 200
934
"#
935
        );
936
    }
937
}