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
25915
    pub fn identifier(&self) -> &'static str {
40
25915
        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
1335
            SectionValue::Captures(_) => "Captures",
49
75
            SectionValue::MultipartFormData(_, true) => "Multipart",
50
280
            SectionValue::MultipartFormData(_, false) => "MultipartFormData",
51
4705
            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
}
300

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

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

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

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

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

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

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

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

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

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

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

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