1
/*
2
 * Hurl (https://hurl.dev)
3
 * Copyright (C) 2025 Orange
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *          http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
use std::fmt;
19

            
20
use crate::ast::option::EntryOption;
21
use crate::ast::primitive::{
22
    Base64, File, Hex, KeyValue, LineTerminator, MultilineString, Number, Placeholder, Regex,
23
    SourceInfo, Template, Whitespace,
24
};
25
use crate::ast::Filter;
26
use crate::types::{SourceString, ToSource};
27

            
28
#[derive(Clone, Debug, PartialEq, Eq)]
29
pub struct Section {
30
    pub line_terminators: Vec<LineTerminator>,
31
    pub space0: Whitespace,
32
    pub line_terminator0: LineTerminator,
33
    pub value: SectionValue,
34
    pub source_info: SourceInfo,
35
}
36

            
37
impl Section {
38
    /// Returns the Hurl string identifier of this section.
39
25965
    pub fn identifier(&self) -> &'static str {
40
25965
        match self.value {
41
17355
            SectionValue::Asserts(_) => "Asserts",
42
475
            SectionValue::QueryParams(_, true) => "Query",
43
470
            SectionValue::QueryParams(_, false) => "QueryStringParams",
44
185
            SectionValue::BasicAuth(_) => "BasicAuth",
45
130
            SectionValue::FormParams(_, true) => "Form",
46
215
            SectionValue::FormParams(_, false) => "FormParams",
47
690
            SectionValue::Cookies(_) => "Cookies",
48
1355
            SectionValue::Captures(_) => "Captures",
49
75
            SectionValue::MultipartFormData(_, true) => "Multipart",
50
280
            SectionValue::MultipartFormData(_, false) => "MultipartFormData",
51
4735
            SectionValue::Options(_) => "Options",
52
        }
53
    }
54
}
55

            
56
#[derive(Clone, Debug, PartialEq, Eq)]
57
#[allow(clippy::large_enum_variant)]
58
pub enum SectionValue {
59
    QueryParams(Vec<KeyValue>, bool), // boolean param indicates if we use the short syntax
60
    BasicAuth(Option<KeyValue>),      // boolean param indicates if we use the short syntax
61
    FormParams(Vec<KeyValue>, bool),
62
    MultipartFormData(Vec<MultipartParam>, bool), // boolean param indicates if we use the short syntax
63
    Cookies(Vec<Cookie>),
64
    Captures(Vec<Capture>),
65
    Asserts(Vec<Assert>),
66
    Options(Vec<EntryOption>),
67
}
68

            
69
#[derive(Clone, Debug, PartialEq, Eq)]
70
pub struct Cookie {
71
    pub line_terminators: Vec<LineTerminator>,
72
    pub space0: Whitespace,
73
    pub name: Template,
74
    pub space1: Whitespace,
75
    pub space2: Whitespace,
76
    pub value: Template,
77
    pub line_terminator0: LineTerminator,
78
}
79

            
80
#[allow(clippy::large_enum_variant)]
81
#[derive(Clone, Debug, PartialEq, Eq)]
82
pub enum MultipartParam {
83
    Param(KeyValue),
84
    FilenameParam(FilenameParam),
85
}
86

            
87
#[derive(Clone, Debug, PartialEq, Eq)]
88
pub struct FilenameParam {
89
    pub line_terminators: Vec<LineTerminator>,
90
    pub space0: Whitespace,
91
    pub key: Template,
92
    pub space1: Whitespace,
93
    pub space2: Whitespace,
94
    pub value: FilenameValue,
95
    pub line_terminator0: LineTerminator,
96
}
97

            
98
#[derive(Clone, Debug, PartialEq, Eq)]
99
pub struct FilenameValue {
100
    pub space0: Whitespace,
101
    pub filename: Template,
102
    pub space1: Whitespace,
103
    pub space2: Whitespace,
104
    pub content_type: Option<Template>,
105
}
106

            
107
#[derive(Clone, Debug, PartialEq, Eq)]
108
pub struct Capture {
109
    pub line_terminators: Vec<LineTerminator>,
110
    pub space0: Whitespace,
111
    pub name: Template,
112
    pub space1: Whitespace,
113
    pub space2: Whitespace,
114
    pub query: Query,
115
    pub filters: Vec<(Whitespace, Filter)>,
116
    pub space3: Whitespace,
117
    pub redacted: bool,
118
    pub line_terminator0: LineTerminator,
119
}
120

            
121
#[derive(Clone, Debug, PartialEq, Eq)]
122
pub struct Assert {
123
    pub line_terminators: Vec<LineTerminator>,
124
    pub space0: Whitespace,
125
    pub query: Query,
126
    pub filters: Vec<(Whitespace, Filter)>,
127
    pub space1: Whitespace,
128
    pub predicate: Predicate,
129
    pub line_terminator0: LineTerminator,
130
}
131

            
132
#[derive(Clone, Debug, PartialEq, Eq)]
133
pub struct Query {
134
    pub source_info: SourceInfo,
135
    pub value: QueryValue,
136
}
137

            
138
#[derive(Clone, Debug, PartialEq, Eq)]
139
#[allow(clippy::large_enum_variant)]
140
pub enum QueryValue {
141
    Status,
142
    Version,
143
    Url,
144
    Header {
145
        space0: Whitespace,
146
        name: Template,
147
    },
148
    Cookie {
149
        space0: Whitespace,
150
        expr: CookiePath,
151
    },
152
    Body,
153
    Xpath {
154
        space0: Whitespace,
155
        expr: Template,
156
    },
157
    Jsonpath {
158
        space0: Whitespace,
159
        expr: Template,
160
    },
161
    Regex {
162
        space0: Whitespace,
163
        value: RegexValue,
164
    },
165
    Variable {
166
        space0: Whitespace,
167
        name: Template,
168
    },
169
    Duration,
170
    Bytes,
171
    Sha256,
172
    Md5,
173
    Certificate {
174
        space0: Whitespace,
175
        attribute_name: CertificateAttributeName,
176
    },
177
    Ip,
178
    Redirects,
179
}
180

            
181
impl QueryValue {
182
    /// Returns the Hurl string identifier of this query type.
183
5920
    pub fn identifier(&self) -> &'static str {
184
5920
        match self {
185
50
            QueryValue::Status => "status",
186
35
            QueryValue::Version => "version",
187
125
            QueryValue::Url => "url",
188
395
            QueryValue::Header { .. } => "header",
189
125
            QueryValue::Cookie { .. } => "cookie",
190
420
            QueryValue::Body => "body",
191
160
            QueryValue::Xpath { .. } => "xpath",
192
3305
            QueryValue::Jsonpath { .. } => "jsonpath",
193
55
            QueryValue::Regex { .. } => "regex",
194
285
            QueryValue::Variable { .. } => "variable",
195
30
            QueryValue::Duration => "duration",
196
350
            QueryValue::Bytes => "bytes",
197
90
            QueryValue::Sha256 => "sha256",
198
65
            QueryValue::Md5 => "md5",
199
220
            QueryValue::Certificate { .. } => "certificate",
200
40
            QueryValue::Ip => "ip",
201
170
            QueryValue::Redirects => "redirects",
202
        }
203
    }
204
}
205

            
206
#[derive(Clone, Debug, PartialEq, Eq)]
207
pub enum RegexValue {
208
    Template(Template),
209
    Regex(Regex),
210
}
211

            
212
#[derive(Clone, Debug, PartialEq, Eq)]
213
pub struct CookiePath {
214
    pub name: Template,
215
    pub attribute: Option<CookieAttribute>,
216
}
217

            
218
impl fmt::Display for CookiePath {
219
15
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
220
15
        let mut buf = self.name.to_string();
221
15
        if let Some(attribute) = &self.attribute {
222
10
            let s = format!("[{}]", attribute.identifier());
223
10
            buf.push_str(s.as_str());
224
        }
225
15
        write!(f, "{buf}")
226
    }
227
}
228

            
229
impl ToSource for CookiePath {
230
110
    fn to_source(&self) -> SourceString {
231
110
        let mut source = SourceString::new();
232
110
        source.push('"');
233
110
        source.push_str(self.name.to_source().as_str());
234
110
        if let Some(attribute) = &self.attribute {
235
85
            let s = format!("[{}]", attribute.name.value());
236
85
            source.push_str(&s);
237
        }
238
110
        source.push('"');
239
110
        source
240
    }
241
}
242

            
243
#[derive(Clone, Debug, PartialEq, Eq)]
244
pub struct CookieAttribute {
245
    pub space0: Whitespace,
246
    pub name: CookieAttributeName,
247
    pub space1: Whitespace,
248
}
249

            
250
impl CookieAttribute {
251
10
    fn identifier(&self) -> &'static str {
252
10
        match self.name {
253
            CookieAttributeName::MaxAge(_) => "Max-Age",
254
            CookieAttributeName::Value(_) => "Value",
255
10
            CookieAttributeName::Expires(_) => "Expires",
256
            CookieAttributeName::Domain(_) => "Domain",
257
            CookieAttributeName::Path(_) => "Path",
258
            CookieAttributeName::Secure(_) => "Secure",
259
            CookieAttributeName::HttpOnly(_) => "HttpOnly",
260
            CookieAttributeName::SameSite(_) => "SameSite",
261
        }
262
    }
263
}
264

            
265
#[derive(Clone, Debug, PartialEq, Eq)]
266
pub enum CookieAttributeName {
267
    Value(String),
268
    Expires(String),
269
    MaxAge(String),
270
    Domain(String),
271
    Path(String),
272
    Secure(String),
273
    HttpOnly(String),
274
    SameSite(String),
275
}
276

            
277
impl CookieAttributeName {
278
85
    pub fn value(&self) -> String {
279
85
        match self {
280
            CookieAttributeName::Value(value)
281
35
            | CookieAttributeName::Expires(value)
282
5
            | CookieAttributeName::MaxAge(value)
283
5
            | CookieAttributeName::Domain(value)
284
5
            | CookieAttributeName::Path(value)
285
30
            | CookieAttributeName::Secure(value)
286
5
            | CookieAttributeName::HttpOnly(value)
287
85
            | CookieAttributeName::SameSite(value) => value.to_string(),
288
        }
289
    }
290
}
291

            
292
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
293
pub enum CertificateAttributeName {
294
    Subject,
295
    Issuer,
296
    StartDate,
297
    ExpireDate,
298
    SerialNumber,
299
    SubjectAltName,
300
}
301

            
302
impl CertificateAttributeName {
303
    /// Returns the Hurl string identifier of this certificate attribute name.
304
220
    pub fn identifier(&self) -> &'static str {
305
220
        match self {
306
20
            CertificateAttributeName::Subject => "Subject",
307
20
            CertificateAttributeName::Issuer => "Issuer",
308
80
            CertificateAttributeName::StartDate => "Start-Date",
309
80
            CertificateAttributeName::ExpireDate => "Expire-Date",
310
20
            CertificateAttributeName::SerialNumber => "Serial-Number",
311
            CertificateAttributeName::SubjectAltName => "Subject-Alt-Name",
312
        }
313
    }
314
}
315

            
316
impl ToSource for CertificateAttributeName {
317
165
    fn to_source(&self) -> SourceString {
318
165
        let mut s = SourceString::new();
319
165
        s.push('"');
320
165
        s.push_str(self.identifier());
321
165
        s.push('"');
322
165
        s
323
    }
324
}
325

            
326
#[derive(Clone, Debug, PartialEq, Eq)]
327
pub struct Predicate {
328
    pub not: bool,
329
    pub space0: Whitespace,
330
    pub predicate_func: PredicateFunc,
331
}
332

            
333
#[derive(Clone, Debug, PartialEq, Eq)]
334
pub struct Not {
335
    pub value: bool,
336
    pub space0: Whitespace,
337
}
338

            
339
#[derive(Clone, Debug, PartialEq, Eq)]
340
pub struct PredicateFunc {
341
    pub source_info: SourceInfo,
342
    pub value: PredicateFuncValue,
343
}
344

            
345
#[derive(Clone, Debug, PartialEq, Eq)]
346
#[allow(clippy::large_enum_variant)]
347
pub enum PredicateValue {
348
    Base64(Base64),
349
    Bool(bool),
350
    File(File),
351
    Hex(Hex),
352
    MultilineString(MultilineString),
353
    Null,
354
    Number(Number),
355
    Placeholder(Placeholder),
356
    Regex(Regex),
357
    String(Template),
358
}
359

            
360
#[derive(Clone, Debug, PartialEq, Eq)]
361
#[allow(clippy::large_enum_variant)]
362
pub enum PredicateFuncValue {
363
    Equal {
364
        space0: Whitespace,
365
        value: PredicateValue,
366
    },
367
    NotEqual {
368
        space0: Whitespace,
369
        value: PredicateValue,
370
    },
371
    GreaterThan {
372
        space0: Whitespace,
373
        value: PredicateValue,
374
    },
375
    GreaterThanOrEqual {
376
        space0: Whitespace,
377
        value: PredicateValue,
378
    },
379
    LessThan {
380
        space0: Whitespace,
381
        value: PredicateValue,
382
    },
383
    LessThanOrEqual {
384
        space0: Whitespace,
385
        value: PredicateValue,
386
    },
387
    StartWith {
388
        space0: Whitespace,
389
        value: PredicateValue,
390
    },
391
    EndWith {
392
        space0: Whitespace,
393
        value: PredicateValue,
394
    },
395
    Contain {
396
        space0: Whitespace,
397
        value: PredicateValue,
398
    },
399
    Include {
400
        space0: Whitespace,
401
        value: PredicateValue,
402
    },
403
    Match {
404
        space0: Whitespace,
405
        value: PredicateValue,
406
    },
407
    Exist,
408
    IsBoolean,
409
    IsCollection,
410
    IsDate,
411
    IsEmpty,
412
    IsFloat,
413
    IsInteger,
414
    IsIpv4,
415
    IsIpv6,
416
    IsIsoDate,
417
    IsList,
418
    IsNumber,
419
    IsObject,
420
    IsString,
421
    IsUuid,
422
}
423

            
424
impl PredicateFuncValue {
425
    /// Returns the Hurl string identifier of this predicate.
426
5540
    pub fn identifier(&self) -> &'static str {
427
5540
        match self {
428
3610
            PredicateFuncValue::Equal { .. } => "==",
429
105
            PredicateFuncValue::NotEqual { .. } => "!=",
430
120
            PredicateFuncValue::GreaterThan { .. } => ">",
431
35
            PredicateFuncValue::GreaterThanOrEqual { .. } => ">=",
432
110
            PredicateFuncValue::LessThan { .. } => "<",
433
45
            PredicateFuncValue::LessThanOrEqual { .. } => "<=",
434
180
            PredicateFuncValue::StartWith { .. } => "startsWith",
435
70
            PredicateFuncValue::EndWith { .. } => "endsWith",
436
180
            PredicateFuncValue::Contain { .. } => "contains",
437
20
            PredicateFuncValue::Include { .. } => "includes",
438
160
            PredicateFuncValue::Match { .. } => "matches",
439
260
            PredicateFuncValue::Exist => "exists",
440
30
            PredicateFuncValue::IsBoolean => "isBoolean",
441
55
            PredicateFuncValue::IsCollection => "isCollection",
442
65
            PredicateFuncValue::IsDate => "isDate",
443
65
            PredicateFuncValue::IsEmpty => "isEmpty",
444
35
            PredicateFuncValue::IsFloat => "isFloat",
445
30
            PredicateFuncValue::IsInteger => "isInteger",
446
50
            PredicateFuncValue::IsIpv4 => "isIpv4",
447
50
            PredicateFuncValue::IsIpv6 => "isIpv6",
448
55
            PredicateFuncValue::IsIsoDate => "isIsoDate",
449
55
            PredicateFuncValue::IsList => "isList",
450
35
            PredicateFuncValue::IsNumber => "isNumber",
451
50
            PredicateFuncValue::IsObject => "isObject",
452
30
            PredicateFuncValue::IsString => "isString",
453
40
            PredicateFuncValue::IsUuid => "isUuid",
454
        }
455
    }
456
}
457

            
458
impl ToSource for PredicateFuncValue {
459
3650
    fn to_source(&self) -> SourceString {
460
3650
        let mut s = SourceString::new();
461
3650
        s.push_str(self.identifier());
462
3650
        s
463
    }
464
}
465

            
466
#[cfg(test)]
467
mod tests {
468
    use super::*;
469
    use crate::ast::primitive::{SourceInfo, Template, TemplateElement};
470
    use crate::reader::Pos;
471
    use crate::types::ToSource;
472

            
473
    fn whitespace() -> Whitespace {
474
        Whitespace {
475
            value: String::new(),
476
            source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
477
        }
478
    }
479

            
480
    #[test]
481
    fn test_cookie_path() {
482
        assert_eq!(
483
            CookiePath {
484
                name: Template {
485
                    delimiter: None,
486
                    elements: vec![TemplateElement::String {
487
                        value: "LSID".to_string(),
488
                        source: "unused".to_source(),
489
                    }],
490
                    source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
491
                },
492
                attribute: Some(CookieAttribute {
493
                    space0: whitespace(),
494
                    name: CookieAttributeName::MaxAge("Max-Age".to_string()),
495
                    space1: whitespace(),
496
                }),
497
            }
498
            .to_string(),
499
            "LSID[Max-Age]".to_string()
500
        );
501
    }
502
}