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 std::fmt;
19

            
20
use crate::types::{SourceString, ToSource};
21

            
22
use super::core::Filter;
23
use super::option::EntryOption;
24
use super::primitive::{
25
    Base64, File, Hex, KeyValue, LineTerminator, MultilineString, Number, Placeholder, Regex,
26
    SourceInfo, Template, Whitespace,
27
};
28

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

            
38
impl Section {
39
    /// Returns the Hurl string identifier of this section.
40
23088
    pub fn identifier(&self) -> &'static str {
41
23088
        match self.value {
42
14980
            SectionValue::Asserts(_) => "Asserts",
43
400
            SectionValue::QueryParams(_, true) => "Query",
44
356
            SectionValue::QueryParams(_, false) => "QueryStringParams",
45
180
            SectionValue::BasicAuth(_) => "BasicAuth",
46
120
            SectionValue::FormParams(_, true) => "Form",
47
172
            SectionValue::FormParams(_, false) => "FormParams",
48
696
            SectionValue::Cookies(_) => "Cookies",
49
1176
            SectionValue::Captures(_) => "Captures",
50
68
            SectionValue::MultipartFormData(_, true) => "Multipart",
51
232
            SectionValue::MultipartFormData(_, false) => "MultipartFormData",
52
4708
            SectionValue::Options(_) => "Options",
53
        }
54
    }
55
}
56

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

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

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

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

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

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

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

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

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

            
183
impl QueryValue {
184
    /// Returns the Hurl string identifier of this query type.
185
4912
    pub fn identifier(&self) -> &'static str {
186
4912
        match self {
187
44
            QueryValue::Status => "status",
188
28
            QueryValue::Version => "version",
189
104
            QueryValue::Url => "url",
190
320
            QueryValue::Header { .. } => "header",
191
100
            QueryValue::Cookie { .. } => "cookie",
192
336
            QueryValue::Body => "body",
193
128
            QueryValue::Xpath { .. } => "xpath",
194
2728
            QueryValue::Jsonpath { .. } => "jsonpath",
195
44
            QueryValue::Regex { .. } => "regex",
196
228
            QueryValue::Variable { .. } => "variable",
197
24
            QueryValue::Duration => "duration",
198
296
            QueryValue::Bytes => "bytes",
199
56
            QueryValue::RawBytes => "rawbytes",
200
72
            QueryValue::Sha256 => "sha256",
201
52
            QueryValue::Md5 => "md5",
202
176
            QueryValue::Certificate { .. } => "certificate",
203
32
            QueryValue::Ip => "ip",
204
144
            QueryValue::Redirects => "redirects",
205
        }
206
    }
207
}
208

            
209
#[derive(Clone, Debug, PartialEq, Eq)]
210
pub enum RegexValue {
211
    Template(Template),
212
    Regex(Regex),
213
}
214

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

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

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

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

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

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

            
280
impl CookieAttributeName {
281
68
    pub fn value(&self) -> String {
282
68
        match self {
283
            CookieAttributeName::Value(value)
284
28
            | CookieAttributeName::Expires(value)
285
4
            | CookieAttributeName::MaxAge(value)
286
4
            | CookieAttributeName::Domain(value)
287
4
            | CookieAttributeName::Path(value)
288
24
            | CookieAttributeName::Secure(value)
289
4
            | CookieAttributeName::HttpOnly(value)
290
68
            | CookieAttributeName::SameSite(value) => value.to_string(),
291
        }
292
    }
293
}
294

            
295
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
296
pub enum CertificateAttributeName {
297
    Subject,
298
    Issuer,
299
    StartDate,
300
    ExpireDate,
301
    SerialNumber,
302
    SubjectAltName,
303
    Value,
304
}
305

            
306
impl CertificateAttributeName {
307
    /// Returns the Hurl string identifier of this certificate attribute name.
308
176
    pub fn identifier(&self) -> &'static str {
309
176
        match self {
310
16
            CertificateAttributeName::Subject => "Subject",
311
16
            CertificateAttributeName::Issuer => "Issuer",
312
64
            CertificateAttributeName::StartDate => "Start-Date",
313
64
            CertificateAttributeName::ExpireDate => "Expire-Date",
314
16
            CertificateAttributeName::SerialNumber => "Serial-Number",
315
            CertificateAttributeName::SubjectAltName => "Subject-Alt-Name",
316
            CertificateAttributeName::Value => "Value",
317
        }
318
    }
319
}
320

            
321
impl ToSource for CertificateAttributeName {
322
132
    fn to_source(&self) -> SourceString {
323
132
        let mut s = SourceString::new();
324
132
        s.push('"');
325
132
        s.push_str(self.identifier());
326
132
        s.push('"');
327
132
        s
328
    }
329
}
330

            
331
#[derive(Clone, Debug, PartialEq, Eq)]
332
pub struct Predicate {
333
    pub not: bool,
334
    pub space0: Whitespace,
335
    pub predicate_func: PredicateFunc,
336
}
337

            
338
#[derive(Clone, Debug, PartialEq, Eq)]
339
pub struct Not {
340
    pub value: bool,
341
    pub space0: Whitespace,
342
}
343

            
344
#[derive(Clone, Debug, PartialEq, Eq)]
345
pub struct PredicateFunc {
346
    pub source_info: SourceInfo,
347
    pub value: PredicateFuncValue,
348
}
349

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

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

            
429
impl PredicateFuncValue {
430
    /// Returns the Hurl string identifier of this predicate.
431
4604
    pub fn identifier(&self) -> &'static str {
432
4604
        match self {
433
2992
            PredicateFuncValue::Equal { .. } => "==",
434
96
            PredicateFuncValue::NotEqual { .. } => "!=",
435
96
            PredicateFuncValue::GreaterThan { .. } => ">",
436
28
            PredicateFuncValue::GreaterThanOrEqual { .. } => ">=",
437
88
            PredicateFuncValue::LessThan { .. } => "<",
438
36
            PredicateFuncValue::LessThanOrEqual { .. } => "<=",
439
172
            PredicateFuncValue::StartWith { .. } => "startsWith",
440
56
            PredicateFuncValue::EndWith { .. } => "endsWith",
441
144
            PredicateFuncValue::Contain { .. } => "contains",
442
16
            PredicateFuncValue::Include { .. } => "includes",
443
128
            PredicateFuncValue::Match { .. } => "matches",
444
240
            PredicateFuncValue::Exist => "exists",
445
24
            PredicateFuncValue::IsBoolean => "isBoolean",
446
44
            PredicateFuncValue::IsCollection => "isCollection",
447
52
            PredicateFuncValue::IsDate => "isDate",
448
48
            PredicateFuncValue::IsEmpty => "isEmpty",
449
28
            PredicateFuncValue::IsFloat => "isFloat",
450
24
            PredicateFuncValue::IsInteger => "isInteger",
451
40
            PredicateFuncValue::IsIpv4 => "isIpv4",
452
40
            PredicateFuncValue::IsIpv6 => "isIpv6",
453
44
            PredicateFuncValue::IsIsoDate => "isIsoDate",
454
44
            PredicateFuncValue::IsList => "isList",
455
28
            PredicateFuncValue::IsNumber => "isNumber",
456
40
            PredicateFuncValue::IsObject => "isObject",
457
24
            PredicateFuncValue::IsString => "isString",
458
32
            PredicateFuncValue::IsUuid => "isUuid",
459
        }
460
    }
461
}
462

            
463
impl ToSource for PredicateFuncValue {
464
3080
    fn to_source(&self) -> SourceString {
465
3080
        let mut s = SourceString::new();
466
3080
        s.push_str(self.identifier());
467
3080
        s
468
    }
469
}
470

            
471
#[cfg(test)]
472
mod tests {
473
    use super::*;
474
    use crate::ast::primitive::{SourceInfo, Template, TemplateElement};
475
    use crate::reader::Pos;
476
    use crate::types::ToSource;
477

            
478
    fn whitespace() -> Whitespace {
479
        Whitespace {
480
            value: String::new(),
481
            source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
482
        }
483
    }
484

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