1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2024 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 crate::linter::{LinterError, LinterErrorKind};
19
use hurl_core::ast::{
20
    Assert, Base64, Body, Bytes, Capture, Comment, Cookie, CookieAttribute, CookieAttributeName,
21
    CookiePath, DurationOption, Entry, EntryOption, File, FileParam, Filter, FilterValue, GraphQl,
22
    Hex, HurlFile, KeyValue, LineTerminator, MultilineString, MultilineStringAttribute,
23
    MultilineStringKind, MultipartParam, OptionKind, Predicate, PredicateFunc, PredicateFuncValue,
24
    PredicateValue, Query, QueryValue, RegexValue, Request, Response, Section, SectionValue,
25
    SourceInfo, Template, Text, VariableDefinition, Whitespace,
26
};
27
use hurl_core::reader::Pos;
28
use hurl_core::typing::{Duration, DurationUnit};
29

            
30
/// Returns lint errors for the `hurl_file`.
31
6
pub fn check_hurl_file(hurl_file: &HurlFile) -> Vec<LinterError> {
32
6
    hurl_file.entries.iter().flat_map(check_entry).collect()
33
}
34

            
35
/// Returns a new linted instance from this `hurl_file`.
36
72
pub fn lint_hurl_file(hurl_file: &HurlFile) -> HurlFile {
37
72
    HurlFile {
38
72
        entries: hurl_file.entries.iter().map(lint_entry).collect(),
39
72
        line_terminators: hurl_file.line_terminators.clone(),
40
    }
41
}
42

            
43
6
fn check_entry(entry: &Entry) -> Vec<LinterError> {
44
6
    let mut errors = vec![];
45
6
    errors.append(&mut check_request(&entry.request));
46
6
    if let Some(response) = &entry.response {
47
        errors.append(&mut check_response(response));
48
    }
49
6
    errors
50
}
51

            
52
234
fn lint_entry(entry: &Entry) -> Entry {
53
234
    let request = lint_request(&entry.request);
54
234
    let response = entry.response.as_ref().map(lint_response);
55
234
    Entry { request, response }
56
}
57

            
58
6
fn check_request(request: &Request) -> Vec<LinterError> {
59
6
    let mut errors = vec![];
60
6
    if !request.space0.value.is_empty() {
61
        errors.push(LinterError {
62
            source_info: request.space0.source_info,
63
            kind: LinterErrorKind::UnnecessarySpace,
64
        });
65
    }
66
6
    if request.space1.value != " " {
67
3
        errors.push(LinterError {
68
3
            source_info: request.space1.source_info,
69
3
            kind: LinterErrorKind::OneSpace,
70
3
        });
71
    }
72
6
    for error in check_line_terminator(&request.line_terminator0) {
73
3
        errors.push(error);
74
    }
75
6
    errors.extend(request.sections.iter().flat_map(check_section));
76
6
    errors
77
}
78

            
79
234
fn lint_request(request: &Request) -> Request {
80
234
    let line_terminators = request.line_terminators.clone();
81
234
    let space0 = empty_whitespace();
82
234
    let method = request.method.clone();
83
234
    let space1 = one_whitespace();
84
234

            
85
234
    let url = request.url.clone();
86
234
    let line_terminator0 = lint_line_terminator(&request.line_terminator0);
87
234
    let headers = request.headers.iter().map(lint_key_value).collect();
88
234
    let body = request.body.as_ref().map(lint_body);
89
234
    let mut sections: Vec<Section> = request.sections.iter().map(lint_section).collect();
90
268
    sections.sort_by_key(|k| section_value_index(k.value.clone()));
91
234

            
92
234
    let source_info = SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0));
93
234
    Request {
94
234
        line_terminators,
95
234
        space0,
96
234
        method,
97
234
        space1,
98
234
        url,
99
234
        line_terminator0,
100
234
        headers,
101
234
        sections,
102
234
        body,
103
234
        source_info,
104
    }
105
}
106

            
107
fn check_response(response: &Response) -> Vec<LinterError> {
108
    let mut errors = vec![];
109
    if !response.space0.value.is_empty() {
110
        errors.push(LinterError {
111
            source_info: response.space0.source_info,
112
            kind: LinterErrorKind::UnnecessarySpace,
113
        });
114
    }
115
    errors.extend(response.sections.iter().flat_map(check_section));
116
    errors
117
}
118

            
119
105
fn lint_response(response: &Response) -> Response {
120
105
    let line_terminators = response.line_terminators.clone();
121
105
    let space0 = empty_whitespace();
122
105
    let version = response.version.clone();
123
105
    let space1 = response.space1.clone();
124
105
    let status = response.status.clone();
125
105
    let line_terminator0 = response.line_terminator0.clone();
126
105
    let headers = response.headers.iter().map(lint_key_value).collect();
127
105
    let mut sections: Vec<Section> = response.sections.iter().map(lint_section).collect();
128
111
    sections.sort_by_key(|k| section_value_index(k.value.clone()));
129
105
    let body = response.body.clone();
130
105

            
131
105
    Response {
132
105
        line_terminators,
133
105
        space0,
134
105
        version,
135
105
        space1,
136
105
        status,
137
105
        line_terminator0,
138
105
        headers,
139
105
        sections,
140
105
        body,
141
105
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
142
    }
143
}
144

            
145
6
fn check_section(section: &Section) -> Vec<LinterError> {
146
6
    let mut errors = vec![];
147
6
    if !section.space0.value.is_empty() {
148
3
        errors.push(LinterError {
149
3
            source_info: section.space0.source_info,
150
3
            kind: LinterErrorKind::UnnecessarySpace,
151
3
        });
152
    }
153
6
    for error in check_line_terminator(&section.line_terminator0) {
154
        errors.push(error);
155
    }
156
6
    errors
157
}
158

            
159
111
fn lint_section(section: &Section) -> Section {
160
111
    let line_terminators = section.line_terminators.clone();
161
111
    let line_terminator0 = section.line_terminator0.clone();
162
111
    let value = lint_section_value(&section.value);
163
111
    Section {
164
111
        line_terminators,
165
111
        space0: empty_whitespace(),
166
111
        value,
167
111
        line_terminator0,
168
111
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
169
    }
170
}
171

            
172
111
fn lint_section_value(section_value: &SectionValue) -> SectionValue {
173
111
    match section_value {
174
12
        SectionValue::QueryParams(params, short) => {
175
12
            SectionValue::QueryParams(params.iter().map(lint_key_value).collect(), *short)
176
        }
177
3
        SectionValue::BasicAuth(param) => {
178
3
            SectionValue::BasicAuth(param.as_ref().map(lint_key_value))
179
        }
180
9
        SectionValue::Captures(captures) => {
181
9
            SectionValue::Captures(captures.iter().map(lint_capture).collect())
182
        }
183
42
        SectionValue::Asserts(asserts) => {
184
42
            SectionValue::Asserts(asserts.iter().map(lint_assert).collect())
185
        }
186
6
        SectionValue::FormParams(params, short) => {
187
6
            SectionValue::FormParams(params.iter().map(lint_key_value).collect(), *short)
188
        }
189
6
        SectionValue::MultipartFormData(params, short) => SectionValue::MultipartFormData(
190
6
            params.iter().map(lint_multipart_param).collect(),
191
6
            *short,
192
6
        ),
193
9
        SectionValue::Cookies(cookies) => {
194
9
            SectionValue::Cookies(cookies.iter().map(lint_cookie).collect())
195
        }
196
24
        SectionValue::Options(options) => {
197
24
            SectionValue::Options(options.iter().map(lint_entry_option).collect())
198
        }
199
    }
200
}
201

            
202
120
fn section_value_index(section_value: SectionValue) -> u32 {
203
120
    match section_value {
204
        // Request sections
205
27
        SectionValue::Options(_) => 0,
206
15
        SectionValue::QueryParams(_, _) => 1,
207
9
        SectionValue::BasicAuth(_) => 2,
208
18
        SectionValue::FormParams(_, _) => 3,
209
18
        SectionValue::MultipartFormData(_, _) => 4,
210
15
        SectionValue::Cookies(_) => 5,
211
        // Response sections
212
9
        SectionValue::Captures(_) => 0,
213
9
        SectionValue::Asserts(_) => 1,
214
    }
215
}
216

            
217
312
fn lint_assert(assert: &Assert) -> Assert {
218
312
    let filters = assert
219
312
        .filters
220
312
        .iter()
221
341
        .map(|(_, f)| (one_whitespace(), lint_filter(f)))
222
312
        .collect();
223
312
    Assert {
224
312
        line_terminators: assert.line_terminators.clone(),
225
312
        space0: empty_whitespace(),
226
312
        query: lint_query(&assert.query),
227
312
        filters,
228
312
        space1: one_whitespace(),
229
312
        predicate: lint_predicate(&assert.predicate),
230
312
        line_terminator0: assert.line_terminator0.clone(),
231
    }
232
}
233

            
234
6
fn lint_capture(capture: &Capture) -> Capture {
235
6
    let filters = capture
236
6
        .filters
237
6
        .iter()
238
7
        .map(|(_, f)| (one_whitespace(), lint_filter(f)))
239
6
        .collect();
240
6
    Capture {
241
6
        line_terminators: capture.line_terminators.clone(),
242
6
        space0: empty_whitespace(),
243
6
        name: capture.name.clone(),
244
6
        space1: empty_whitespace(),
245
6
        space2: one_whitespace(),
246
6
        query: lint_query(&capture.query),
247
6
        filters,
248
6
        line_terminator0: lint_line_terminator(&capture.line_terminator0),
249
    }
250
}
251

            
252
318
fn lint_query(query: &Query) -> Query {
253
318
    Query {
254
318
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
255
318
        value: lint_query_value(&query.value),
256
    }
257
}
258

            
259
318
fn lint_query_value(query_value: &QueryValue) -> QueryValue {
260
318
    match query_value {
261
6
        QueryValue::Status => QueryValue::Status,
262
3
        QueryValue::Url => QueryValue::Url,
263
12
        QueryValue::Header { name, .. } => QueryValue::Header {
264
12
            name: name.clone(),
265
12
            space0: one_whitespace(),
266
12
        },
267
        QueryValue::Cookie {
268
6
            expr: CookiePath { name, attribute },
269
6
            ..
270
6
        } => {
271
6
            let attribute = attribute.as_ref().map(lint_cookie_attribute);
272
6
            QueryValue::Cookie {
273
6
                space0: one_whitespace(),
274
6
                expr: CookiePath {
275
6
                    name: name.clone(),
276
6
                    attribute,
277
6
                },
278
            }
279
        }
280
30
        QueryValue::Body => QueryValue::Body,
281
3
        QueryValue::Xpath { expr, .. } => QueryValue::Xpath {
282
3
            expr: expr.clone(),
283
3
            space0: one_whitespace(),
284
3
        },
285
189
        QueryValue::Jsonpath { expr, .. } => QueryValue::Jsonpath {
286
189
            expr: expr.clone(),
287
189
            space0: one_whitespace(),
288
189
        },
289
3
        QueryValue::Regex { value, .. } => QueryValue::Regex {
290
3
            value: lint_regex_value(value),
291
3
            space0: one_whitespace(),
292
3
        },
293
9
        QueryValue::Variable { name, .. } => QueryValue::Variable {
294
9
            name: name.clone(),
295
9
            space0: one_whitespace(),
296
9
        },
297
3
        QueryValue::Duration => QueryValue::Duration,
298
18
        QueryValue::Bytes => QueryValue::Bytes,
299
3
        QueryValue::Sha256 => QueryValue::Sha256,
300
3
        QueryValue::Md5 => QueryValue::Md5,
301
        QueryValue::Certificate {
302
30
            attribute_name: field,
303
30
            ..
304
30
        } => QueryValue::Certificate {
305
30
            attribute_name: *field,
306
30
            space0: one_whitespace(),
307
30
        },
308
    }
309
}
310

            
311
6
fn lint_regex_value(regex_value: &RegexValue) -> RegexValue {
312
6
    match regex_value {
313
3
        RegexValue::Template(template) => RegexValue::Template(lint_template(template)),
314
3
        RegexValue::Regex(regex) => RegexValue::Regex(regex.clone()),
315
    }
316
}
317

            
318
3
fn lint_cookie_attribute(cookie_attribute: &CookieAttribute) -> CookieAttribute {
319
3
    let space0 = empty_whitespace();
320
3
    let name = lint_cookie_attribute_name(&cookie_attribute.name);
321
3
    let space1 = empty_whitespace();
322
3
    CookieAttribute {
323
3
        space0,
324
3
        name,
325
3
        space1,
326
    }
327
}
328

            
329
3
fn lint_cookie_attribute_name(cookie_attribute_name: &CookieAttributeName) -> CookieAttributeName {
330
3
    match cookie_attribute_name {
331
        CookieAttributeName::Value(_) => CookieAttributeName::Value("Value".to_string()),
332
3
        CookieAttributeName::Expires(_) => CookieAttributeName::Expires("Expires".to_string()),
333
        CookieAttributeName::MaxAge(_) => CookieAttributeName::MaxAge("Max-Age".to_string()),
334
        CookieAttributeName::Domain(_) => CookieAttributeName::Domain("Domain".to_string()),
335
        CookieAttributeName::Path(_) => CookieAttributeName::Path("Path".to_string()),
336
        CookieAttributeName::Secure(_) => CookieAttributeName::Secure("Secure".to_string()),
337
        CookieAttributeName::HttpOnly(_) => CookieAttributeName::HttpOnly("HttpOnly".to_string()),
338
        CookieAttributeName::SameSite(_) => CookieAttributeName::SameSite("SameSite".to_string()),
339
    }
340
}
341

            
342
312
fn lint_predicate(predicate: &Predicate) -> Predicate {
343
312
    Predicate {
344
312
        not: predicate.not,
345
312
        space0: if predicate.not {
346
3
            one_whitespace()
347
        } else {
348
309
            empty_whitespace()
349
        },
350
312
        predicate_func: lint_predicate_func(&predicate.predicate_func),
351
    }
352
}
353

            
354
312
fn lint_predicate_func(predicate_func: &PredicateFunc) -> PredicateFunc {
355
312
    PredicateFunc {
356
312
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
357
312
        value: lint_predicate_func_value(&predicate_func.value),
358
    }
359
}
360

            
361
312
fn lint_predicate_func_value(predicate_func_value: &PredicateFuncValue) -> PredicateFuncValue {
362
312
    match predicate_func_value {
363
201
        PredicateFuncValue::Equal { value, .. } => PredicateFuncValue::Equal {
364
201
            space0: one_whitespace(),
365
201
            value: lint_predicate_value(value),
366
201
        },
367
9
        PredicateFuncValue::NotEqual { value, .. } => PredicateFuncValue::NotEqual {
368
9
            space0: one_whitespace(),
369
9
            value: lint_predicate_value(value),
370
9
        },
371
9
        PredicateFuncValue::GreaterThan { value, .. } => PredicateFuncValue::GreaterThan {
372
9
            space0: one_whitespace(),
373
9
            value: lint_predicate_value(value),
374
9
        },
375
3
        PredicateFuncValue::GreaterThanOrEqual { value, .. } => {
376
3
            PredicateFuncValue::GreaterThanOrEqual {
377
3
                space0: one_whitespace(),
378
3
                value: lint_predicate_value(value),
379
            }
380
        }
381
12
        PredicateFuncValue::LessThan { value, .. } => PredicateFuncValue::LessThan {
382
12
            space0: one_whitespace(),
383
12
            value: lint_predicate_value(value),
384
12
        },
385
3
        PredicateFuncValue::LessThanOrEqual { value, .. } => PredicateFuncValue::LessThanOrEqual {
386
3
            space0: one_whitespace(),
387
3
            value: lint_predicate_value(value),
388
3
        },
389
6
        PredicateFuncValue::Contain { value, .. } => PredicateFuncValue::Contain {
390
6
            space0: one_whitespace(),
391
6
            value: lint_predicate_value(value),
392
6
        },
393

            
394
3
        PredicateFuncValue::Include { value, .. } => PredicateFuncValue::Include {
395
3
            space0: one_whitespace(),
396
3
            value: lint_predicate_value(value),
397
3
        },
398

            
399
6
        PredicateFuncValue::Match { value, .. } => PredicateFuncValue::Match {
400
6
            space0: one_whitespace(),
401
6
            value: lint_predicate_value(value),
402
6
        },
403
9
        PredicateFuncValue::StartWith { value, .. } => PredicateFuncValue::StartWith {
404
9
            space0: one_whitespace(),
405
9
            value: lint_predicate_value(value),
406
9
        },
407
6
        PredicateFuncValue::EndWith { value, .. } => PredicateFuncValue::EndWith {
408
6
            space0: one_whitespace(),
409
6
            value: lint_predicate_value(value),
410
6
        },
411
3
        PredicateFuncValue::IsInteger => PredicateFuncValue::IsInteger,
412
3
        PredicateFuncValue::IsFloat => PredicateFuncValue::IsFloat,
413
3
        PredicateFuncValue::IsBoolean => PredicateFuncValue::IsBoolean,
414
3
        PredicateFuncValue::IsString => PredicateFuncValue::IsString,
415
3
        PredicateFuncValue::IsCollection => PredicateFuncValue::IsCollection,
416
9
        PredicateFuncValue::IsDate => PredicateFuncValue::IsDate,
417
3
        PredicateFuncValue::IsIsoDate => PredicateFuncValue::IsIsoDate,
418
12
        PredicateFuncValue::Exist => PredicateFuncValue::Exist,
419
3
        PredicateFuncValue::IsEmpty => PredicateFuncValue::IsEmpty,
420
3
        PredicateFuncValue::IsNumber => PredicateFuncValue::IsNumber,
421
    }
422
}
423

            
424
267
fn lint_predicate_value(predicate_value: &PredicateValue) -> PredicateValue {
425
267
    match predicate_value {
426
105
        PredicateValue::String(value) => PredicateValue::String(lint_template(value)),
427
15
        PredicateValue::MultilineString(value) => {
428
15
            PredicateValue::MultilineString(lint_multiline_string(value))
429
        }
430
3
        PredicateValue::Bool(value) => PredicateValue::Bool(*value),
431
3
        PredicateValue::Null => PredicateValue::Null,
432
108
        PredicateValue::Number(value) => PredicateValue::Number(value.clone()),
433
3
        PredicateValue::File(value) => PredicateValue::File(lint_file(value)),
434
21
        PredicateValue::Hex(value) => PredicateValue::Hex(lint_hex(value)),
435
3
        PredicateValue::Base64(value) => PredicateValue::Base64(lint_base64(value)),
436
3
        PredicateValue::Placeholder(value) => PredicateValue::Placeholder(value.clone()),
437
3
        PredicateValue::Regex(value) => PredicateValue::Regex(value.clone()),
438
    }
439
}
440

            
441
36
fn lint_multiline_string(multiline_string: &MultilineString) -> MultilineString {
442
36
    match multiline_string {
443
        MultilineString {
444
18
            kind: MultilineStringKind::Text(value),
445
18
            attributes,
446
18
        } => MultilineString {
447
18
            kind: MultilineStringKind::Text(lint_text(value)),
448
18
            attributes: lint_multiline_string_attributes(attributes),
449
18
        },
450
        MultilineString {
451
6
            kind: MultilineStringKind::Json(value),
452
6
            attributes,
453
6
        } => MultilineString {
454
6
            kind: MultilineStringKind::Json(lint_text(value)),
455
6
            attributes: lint_multiline_string_attributes(attributes),
456
6
        },
457
        MultilineString {
458
6
            kind: MultilineStringKind::Xml(value),
459
6
            attributes,
460
6
        } => MultilineString {
461
6
            kind: MultilineStringKind::Xml(lint_text(value)),
462
6
            attributes: lint_multiline_string_attributes(attributes),
463
6
        },
464
        MultilineString {
465
6
            kind: MultilineStringKind::GraphQl(value),
466
6
            attributes,
467
6
        } => MultilineString {
468
6
            kind: MultilineStringKind::GraphQl(lint_graphql(value)),
469
6
            attributes: lint_multiline_string_attributes(attributes),
470
6
        },
471
    }
472
}
473

            
474
36
fn lint_multiline_string_attributes(
475
36
    attributes: &[MultilineStringAttribute],
476
36
) -> Vec<MultilineStringAttribute> {
477
36
    attributes.to_vec()
478
}
479

            
480
30
fn lint_text(text: &Text) -> Text {
481
30
    let space = empty_whitespace();
482
30
    let newline = text.newline.clone();
483
30
    let value = lint_template(&text.value);
484
30
    Text {
485
30
        space,
486
30
        newline,
487
30
        value,
488
    }
489
}
490

            
491
6
fn lint_graphql(graphql: &GraphQl) -> GraphQl {
492
6
    let space = empty_whitespace();
493
6
    let newline = graphql.newline.clone();
494
6
    let value = lint_template(&graphql.value);
495
6
    let variables = graphql.variables.clone();
496
6
    GraphQl {
497
6
        space,
498
6
        newline,
499
6
        value,
500
6
        variables,
501
    }
502
}
503

            
504
9
fn lint_cookie(cookie: &Cookie) -> Cookie {
505
9
    cookie.clone()
506
}
507

            
508
45
fn lint_body(body: &Body) -> Body {
509
45
    let line_terminators = body.line_terminators.clone();
510
45
    let space0 = empty_whitespace();
511
45
    let value = lint_bytes(&body.value);
512
45
    let line_terminator0 = body.line_terminator0.clone();
513
45
    Body {
514
45
        line_terminators,
515
45
        space0,
516
45
        value,
517
45
        line_terminator0,
518
    }
519
}
520

            
521
45
fn lint_bytes(bytes: &Bytes) -> Bytes {
522
45
    match bytes {
523
6
        Bytes::File(value) => Bytes::File(lint_file(value)),
524
6
        Bytes::Base64(value) => Bytes::Base64(lint_base64(value)),
525
3
        Bytes::Hex(value) => Bytes::Hex(lint_hex(value)),
526
3
        Bytes::Json(value) => Bytes::Json(value.clone()),
527
3
        Bytes::OnelineString(value) => Bytes::OnelineString(lint_template(value)),
528
21
        Bytes::MultilineString(value) => Bytes::MultilineString(lint_multiline_string(value)),
529
3
        Bytes::Xml(value) => Bytes::Xml(value.clone()),
530
    }
531
}
532

            
533
9
fn lint_base64(base64: &Base64) -> Base64 {
534
9
    Base64 {
535
9
        space0: empty_whitespace(),
536
9
        value: base64.value.clone(),
537
9
        encoded: base64.encoded.clone(),
538
9
        space1: empty_whitespace(),
539
    }
540
}
541

            
542
24
fn lint_hex(hex: &Hex) -> Hex {
543
24
    Hex {
544
24
        space0: empty_whitespace(),
545
24
        value: hex.value.clone(),
546
24
        encoded: hex.encoded.clone(),
547
24
        space1: empty_whitespace(),
548
    }
549
}
550

            
551
9
fn lint_file(file: &File) -> File {
552
9
    File {
553
9
        space0: empty_whitespace(),
554
9
        filename: lint_template(&file.filename),
555
9
        space1: empty_whitespace(),
556
    }
557
}
558

            
559
123
fn lint_key_value(key_value: &KeyValue) -> KeyValue {
560
123
    KeyValue {
561
123
        line_terminators: key_value.line_terminators.clone(),
562
123
        space0: empty_whitespace(),
563
123
        key: key_value.key.clone(),
564
123
        space1: empty_whitespace(),
565
123
        space2: if key_value.value.elements.is_empty() {
566
3
            empty_whitespace()
567
        } else {
568
120
            one_whitespace()
569
        },
570
123
        value: key_value.value.clone(),
571
123
        line_terminator0: key_value.line_terminator0.clone(),
572
    }
573
}
574

            
575
9
fn lint_multipart_param(multipart_param: &MultipartParam) -> MultipartParam {
576
9
    match multipart_param {
577
3
        MultipartParam::Param(param) => MultipartParam::Param(lint_key_value(param)),
578
6
        MultipartParam::FileParam(file_param) => {
579
6
            MultipartParam::FileParam(lint_file_param(file_param))
580
        }
581
    }
582
}
583

            
584
6
fn lint_file_param(file_param: &FileParam) -> FileParam {
585
6
    let line_terminators = file_param.line_terminators.clone();
586
6
    let space0 = file_param.space0.clone();
587
6
    let key = file_param.key.clone();
588
6
    let space1 = file_param.space1.clone();
589
6
    let space2 = file_param.space2.clone();
590
6
    let value = file_param.value.clone();
591
6
    let line_terminator0 = file_param.line_terminator0.clone();
592
6
    FileParam {
593
6
        line_terminators,
594
6
        space0,
595
6
        key,
596
6
        space1,
597
6
        space2,
598
6
        value,
599
6
        line_terminator0,
600
    }
601
}
602

            
603
2331
fn empty_whitespace() -> Whitespace {
604
2331
    Whitespace {
605
2331
        value: String::new(),
606
2331
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
607
    }
608
}
609

            
610
1554
fn one_whitespace() -> Whitespace {
611
1554
    Whitespace {
612
1554
        value: " ".to_string(),
613
1554
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
614
    }
615
}
616

            
617
12
fn check_line_terminator(line_terminator: &LineTerminator) -> Vec<LinterError> {
618
12
    let mut errors = vec![];
619
12
    match &line_terminator.comment {
620
        Some(_) => {}
621
        None => {
622
12
            if !line_terminator.space0.value.is_empty() {
623
3
                errors.push(LinterError {
624
3
                    source_info: line_terminator.space0.source_info,
625
3
                    kind: LinterErrorKind::UnnecessarySpace,
626
3
                });
627
            }
628
        }
629
    }
630
12
    errors
631
}
632

            
633
240
fn lint_line_terminator(line_terminator: &LineTerminator) -> LineTerminator {
634
240
    let space0 = match line_terminator.comment {
635
240
        None => empty_whitespace(),
636
        Some(_) => Whitespace {
637
            value: line_terminator.space0.value.clone(),
638
            source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
639
        },
640
    };
641
240
    let comment = line_terminator.comment.as_ref().map(lint_comment);
642
240
    let newline = Whitespace {
643
240
        value: if line_terminator.newline.value.is_empty() {
644
            String::new()
645
        } else {
646
240
            "\n".to_string()
647
        },
648
240
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
649
240
    };
650
240
    LineTerminator {
651
240
        space0,
652
240
        comment,
653
240
        newline,
654
    }
655
}
656

            
657
fn lint_comment(comment: &Comment) -> Comment {
658
    Comment {
659
        value: if comment.value.starts_with(' ') {
660
            comment.value.clone()
661
        } else {
662
            format!(" {}", comment.value)
663
        },
664
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
665
    }
666
}
667

            
668
156
fn lint_template(template: &Template) -> Template {
669
156
    template.clone()
670
}
671

            
672
267
fn lint_entry_option(entry_option: &EntryOption) -> EntryOption {
673
267
    EntryOption {
674
267
        line_terminators: entry_option.line_terminators.clone(),
675
267
        space0: empty_whitespace(),
676
267
        space1: empty_whitespace(),
677
267
        space2: one_whitespace(),
678
267
        kind: lint_option_kind(&entry_option.kind),
679
267
        line_terminator0: entry_option.line_terminator0.clone(),
680
    }
681
}
682

            
683
267
fn lint_option_kind(option_kind: &OptionKind) -> OptionKind {
684
267
    match option_kind {
685
12
        OptionKind::Delay(duration) => {
686
12
            OptionKind::Delay(lint_duration_option(duration, DurationUnit::MilliSecond))
687
        }
688
12
        OptionKind::RetryInterval(duration) => {
689
12
            OptionKind::RetryInterval(lint_duration_option(duration, DurationUnit::MilliSecond))
690
        }
691
27
        OptionKind::Variable(var_def) => OptionKind::Variable(lint_variable_definition(var_def)),
692
216
        _ => option_kind.clone(),
693
    }
694
}
695

            
696
24
fn lint_duration_option(
697
24
    duration_option: &DurationOption,
698
24
    default_unit: DurationUnit,
699
24
) -> DurationOption {
700
24
    match duration_option {
701
18
        DurationOption::Literal(duration) => {
702
18
            DurationOption::Literal(lint_duration(duration, default_unit))
703
        }
704
6
        DurationOption::Placeholder(expr) => DurationOption::Placeholder(expr.clone()),
705
    }
706
}
707

            
708
18
fn lint_duration(duration: &Duration, default_unit: DurationUnit) -> Duration {
709
18
    let value = duration.value.clone();
710
18
    let unit = Some(duration.unit.unwrap_or(default_unit));
711
18
    Duration { value, unit }
712
}
713

            
714
90
fn lint_filter(filter: &Filter) -> Filter {
715
90
    Filter {
716
90
        source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
717
90
        value: lint_filter_value(&filter.value),
718
    }
719
}
720

            
721
90
fn lint_filter_value(filter_value: &FilterValue) -> FilterValue {
722
90
    match filter_value {
723
3
        FilterValue::Regex { value, .. } => FilterValue::Regex {
724
3
            space0: one_whitespace(),
725
3
            value: lint_regex_value(value),
726
3
        },
727
87
        f => f.clone(),
728
    }
729
}
730

            
731
27
fn lint_variable_definition(var_def: &VariableDefinition) -> VariableDefinition {
732
27
    VariableDefinition {
733
27
        space0: empty_whitespace(),
734
27
        space1: empty_whitespace(),
735
27
        ..var_def.clone()
736
    }
737
}
738

            
739
#[cfg(test)]
740
mod tests {
741
    use super::*;
742

            
743
    #[test]
744
    fn test_hurl_file() {
745
        let hurl_file = HurlFile {
746
            entries: vec![],
747
            line_terminators: vec![],
748
        };
749
        let hurl_file_linted = HurlFile {
750
            entries: vec![],
751
            line_terminators: vec![],
752
        };
753
        assert_eq!(check_hurl_file(&hurl_file), vec![]);
754
        assert_eq!(lint_hurl_file(&hurl_file), hurl_file_linted);
755
    }
756

            
757
    #[test]
758
    fn test_entry() {
759
        let entry = HurlFile {
760
            entries: vec![],
761
            line_terminators: vec![],
762
        };
763
        let entry_linted = HurlFile {
764
            entries: vec![],
765
            line_terminators: vec![],
766
        };
767
        assert_eq!(check_hurl_file(&entry), vec![]);
768
        assert_eq!(lint_hurl_file(&entry), entry_linted);
769
    }
770
}