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
26805
    pub fn identifier(&self) -> &'static str {
41
26805
        match self.value {
42
17610
            SectionValue::Asserts(_) => "Asserts",
43
490
            SectionValue::QueryParams(_, true) => "Query",
44
455
            SectionValue::QueryParams(_, false) => "QueryStringParams",
45
185
            SectionValue::BasicAuth(_) => "BasicAuth",
46
130
            SectionValue::FormParams(_, true) => "Form",
47
215
            SectionValue::FormParams(_, false) => "FormParams",
48
710
            SectionValue::Cookies(_) => "Cookies",
49
1415
            SectionValue::Captures(_) => "Captures",
50
75
            SectionValue::MultipartFormData(_, true) => "Multipart",
51
280
            SectionValue::MultipartFormData(_, false) => "MultipartFormData",
52
5240
            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
6055
    pub fn identifier(&self) -> &'static str {
186
6055
        match self {
187
50
            QueryValue::Status => "status",
188
35
            QueryValue::Version => "version",
189
125
            QueryValue::Url => "url",
190
395
            QueryValue::Header { .. } => "header",
191
125
            QueryValue::Cookie { .. } => "cookie",
192
420
            QueryValue::Body => "body",
193
160
            QueryValue::Xpath { .. } => "xpath",
194
3350
            QueryValue::Jsonpath { .. } => "jsonpath",
195
55
            QueryValue::Regex { .. } => "regex",
196
285
            QueryValue::Variable { .. } => "variable",
197
30
            QueryValue::Duration => "duration",
198
370
            QueryValue::Bytes => "bytes",
199
70
            QueryValue::RawBytes => "rawbytes",
200
90
            QueryValue::Sha256 => "sha256",
201
65
            QueryValue::Md5 => "md5",
202
220
            QueryValue::Certificate { .. } => "certificate",
203
40
            QueryValue::Ip => "ip",
204
170
            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
15
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
223
15
        let mut buf = self.name.to_string();
224
15
        if let Some(attribute) = &self.attribute {
225
10
            let s = format!("[{}]", attribute.identifier());
226
10
            buf.push_str(s.as_str());
227
        }
228
15
        write!(f, "{buf}")
229
    }
230
}
231

            
232
impl ToSource for CookiePath {
233
110
    fn to_source(&self) -> SourceString {
234
110
        let mut source = SourceString::new();
235
110
        source.push('"');
236
110
        source.push_str(self.name.to_source().as_str());
237
110
        if let Some(attribute) = &self.attribute {
238
85
            let s = format!("[{}]", attribute.name.value());
239
85
            source.push_str(&s);
240
        }
241
110
        source.push('"');
242
110
        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
10
    fn identifier(&self) -> &'static str {
255
10
        match self.name {
256
            CookieAttributeName::MaxAge(_) => "Max-Age",
257
            CookieAttributeName::Value(_) => "Value",
258
10
            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
85
    pub fn value(&self) -> String {
282
85
        match self {
283
            CookieAttributeName::Value(value)
284
35
            | CookieAttributeName::Expires(value)
285
5
            | CookieAttributeName::MaxAge(value)
286
5
            | CookieAttributeName::Domain(value)
287
5
            | CookieAttributeName::Path(value)
288
30
            | CookieAttributeName::Secure(value)
289
5
            | CookieAttributeName::HttpOnly(value)
290
85
            | 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
220
    pub fn identifier(&self) -> &'static str {
309
220
        match self {
310
20
            CertificateAttributeName::Subject => "Subject",
311
20
            CertificateAttributeName::Issuer => "Issuer",
312
80
            CertificateAttributeName::StartDate => "Start-Date",
313
80
            CertificateAttributeName::ExpireDate => "Expire-Date",
314
20
            CertificateAttributeName::SerialNumber => "Serial-Number",
315
            CertificateAttributeName::SubjectAltName => "Subject-Alt-Name",
316
            CertificateAttributeName::Value => "Value",
317
        }
318
    }
319
}
320

            
321
impl ToSource for CertificateAttributeName {
322
165
    fn to_source(&self) -> SourceString {
323
165
        let mut s = SourceString::new();
324
165
        s.push('"');
325
165
        s.push_str(self.identifier());
326
165
        s.push('"');
327
165
        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
5675
    pub fn identifier(&self) -> &'static str {
432
5675
        match self {
433
3700
            PredicateFuncValue::Equal { .. } => "==",
434
105
            PredicateFuncValue::NotEqual { .. } => "!=",
435
120
            PredicateFuncValue::GreaterThan { .. } => ">",
436
35
            PredicateFuncValue::GreaterThanOrEqual { .. } => ">=",
437
110
            PredicateFuncValue::LessThan { .. } => "<",
438
45
            PredicateFuncValue::LessThanOrEqual { .. } => "<=",
439
215
            PredicateFuncValue::StartWith { .. } => "startsWith",
440
70
            PredicateFuncValue::EndWith { .. } => "endsWith",
441
180
            PredicateFuncValue::Contain { .. } => "contains",
442
20
            PredicateFuncValue::Include { .. } => "includes",
443
160
            PredicateFuncValue::Match { .. } => "matches",
444
275
            PredicateFuncValue::Exist => "exists",
445
30
            PredicateFuncValue::IsBoolean => "isBoolean",
446
55
            PredicateFuncValue::IsCollection => "isCollection",
447
65
            PredicateFuncValue::IsDate => "isDate",
448
60
            PredicateFuncValue::IsEmpty => "isEmpty",
449
35
            PredicateFuncValue::IsFloat => "isFloat",
450
30
            PredicateFuncValue::IsInteger => "isInteger",
451
50
            PredicateFuncValue::IsIpv4 => "isIpv4",
452
50
            PredicateFuncValue::IsIpv6 => "isIpv6",
453
55
            PredicateFuncValue::IsIsoDate => "isIsoDate",
454
55
            PredicateFuncValue::IsList => "isList",
455
35
            PredicateFuncValue::IsNumber => "isNumber",
456
50
            PredicateFuncValue::IsObject => "isObject",
457
30
            PredicateFuncValue::IsString => "isString",
458
40
            PredicateFuncValue::IsUuid => "isUuid",
459
        }
460
    }
461
}
462

            
463
impl ToSource for PredicateFuncValue {
464
3770
    fn to_source(&self) -> SourceString {
465
3770
        let mut s = SourceString::new();
466
3770
        s.push_str(self.identifier());
467
3770
        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
}