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
26090
    pub fn identifier(&self) -> &'static str {
41
26090
        match self.value {
42
17450
            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
1355
            SectionValue::Captures(_) => "Captures",
50
75
            SectionValue::MultipartFormData(_, true) => "Multipart",
51
280
            SectionValue::MultipartFormData(_, false) => "MultipartFormData",
52
4745
            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
6035
    pub fn identifier(&self) -> &'static str {
186
6035
        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
350
            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
}
304

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

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

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

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

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

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

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

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

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

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

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

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