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::typing::{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
6580
    pub fn identifier(&self) -> &'static str {
40
6580
        match self.value {
41
730
            SectionValue::Asserts(_) => "Asserts",
42
350
            SectionValue::QueryParams(_, true) => "Query",
43
405
            SectionValue::QueryParams(_, false) => "QueryStringParams",
44
170
            SectionValue::BasicAuth(_) => "BasicAuth",
45
75
            SectionValue::FormParams(_, true) => "Form",
46
175
            SectionValue::FormParams(_, false) => "FormParams",
47
190
            SectionValue::Cookies(_) => "Cookies",
48
125
            SectionValue::Captures(_) => "Captures",
49
75
            SectionValue::MultipartFormData(_, true) => "Multipart",
50
240
            SectionValue::MultipartFormData(_, false) => "MultipartFormData",
51
4045
            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
4690
    pub fn identifier(&self) -> &'static str {
184
4690
        match self {
185
40
            QueryValue::Status => "status",
186
30
            QueryValue::Version => "version",
187
60
            QueryValue::Url => "url",
188
345
            QueryValue::Header { .. } => "header",
189
95
            QueryValue::Cookie { .. } => "cookie",
190
365
            QueryValue::Body => "body",
191
155
            QueryValue::Xpath { .. } => "xpath",
192
2655
            QueryValue::Jsonpath { .. } => "jsonpath",
193
40
            QueryValue::Regex { .. } => "regex",
194
255
            QueryValue::Variable { .. } => "variable",
195
25
            QueryValue::Duration => "duration",
196
305
            QueryValue::Bytes => "bytes",
197
80
            QueryValue::Sha256 => "sha256",
198
60
            QueryValue::Md5 => "md5",
199
150
            QueryValue::Certificate { .. } => "certificate",
200
30
            QueryValue::Ip => "ip",
201
            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
10
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
220
10
        let mut buf = self.name.to_string();
221
10
        if let Some(attribute) = &self.attribute {
222
5
            let s = format!("[{}]", attribute.identifier());
223
5
            buf.push_str(s.as_str());
224
        }
225
10
        write!(f, "{buf}")
226
    }
227
}
228

            
229
impl ToSource for CookiePath {
230
85
    fn to_source(&self) -> SourceString {
231
85
        let mut source = SourceString::new();
232
85
        source.push('"');
233
85
        source.push_str(self.name.to_source().as_str());
234
85
        if let Some(attribute) = &self.attribute {
235
65
            let s = format!("[{}]", attribute.name.value());
236
65
            source.push_str(&s);
237
        }
238
85
        source.push('"');
239
85
        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
5
    fn identifier(&self) -> &'static str {
252
5
        match self.name {
253
            CookieAttributeName::MaxAge(_) => "Max-Age",
254
            CookieAttributeName::Value(_) => "Value",
255
5
            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
65
    pub fn value(&self) -> String {
279
65
        match self {
280
            CookieAttributeName::Value(value)
281
15
            | 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
65
            | 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
150
    pub fn identifier(&self) -> &'static str {
304
150
        match self {
305
15
            CertificateAttributeName::Subject => "Subject",
306
15
            CertificateAttributeName::Issuer => "Issuer",
307
45
            CertificateAttributeName::StartDate => "Start-Date",
308
60
            CertificateAttributeName::ExpireDate => "Expire-Date",
309
15
            CertificateAttributeName::SerialNumber => "Serial-Number",
310
        }
311
    }
312
}
313

            
314
impl ToSource for CertificateAttributeName {
315
100
    fn to_source(&self) -> SourceString {
316
100
        let mut s = SourceString::new();
317
100
        s.push('"');
318
100
        s.push_str(self.identifier());
319
100
        s.push('"');
320
100
        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
    IsInteger,
406
    IsFloat,
407
    IsBoolean,
408
    IsString,
409
    IsCollection,
410
    IsDate,
411
    IsIsoDate,
412
    Exist,
413
    IsEmpty,
414
    IsNumber,
415
    IsIpv4,
416
    IsIpv6,
417
}
418

            
419
impl PredicateFuncValue {
420
    /// Returns the Hurl string identifier of this predicate.
421
4360
    pub fn identifier(&self) -> &'static str {
422
4360
        match self {
423
2805
            PredicateFuncValue::Equal { .. } => "==",
424
90
            PredicateFuncValue::NotEqual { .. } => "!=",
425
105
            PredicateFuncValue::GreaterThan { .. } => ">",
426
30
            PredicateFuncValue::GreaterThanOrEqual { .. } => ">=",
427
90
            PredicateFuncValue::LessThan { .. } => "<",
428
40
            PredicateFuncValue::LessThanOrEqual { .. } => "<=",
429
165
            PredicateFuncValue::StartWith { .. } => "startsWith",
430
60
            PredicateFuncValue::EndWith { .. } => "endsWith",
431
165
            PredicateFuncValue::Contain { .. } => "contains",
432
15
            PredicateFuncValue::Include { .. } => "includes",
433
130
            PredicateFuncValue::Match { .. } => "matches",
434
25
            PredicateFuncValue::IsInteger => "isInteger",
435
30
            PredicateFuncValue::IsFloat => "isFloat",
436
25
            PredicateFuncValue::IsBoolean => "isBoolean",
437
25
            PredicateFuncValue::IsString => "isString",
438
50
            PredicateFuncValue::IsCollection => "isCollection",
439
50
            PredicateFuncValue::IsDate => "isDate",
440
50
            PredicateFuncValue::IsIsoDate => "isIsoDate",
441
230
            PredicateFuncValue::Exist => "exists",
442
60
            PredicateFuncValue::IsEmpty => "isEmpty",
443
30
            PredicateFuncValue::IsNumber => "isNumber",
444
45
            PredicateFuncValue::IsIpv4 => "isIpv4",
445
45
            PredicateFuncValue::IsIpv6 => "isIpv6",
446
        }
447
    }
448
}
449

            
450
impl ToSource for PredicateFuncValue {
451
3175
    fn to_source(&self) -> SourceString {
452
3175
        let mut s = SourceString::new();
453
3175
        s.push_str(self.identifier());
454
3175
        s
455
    }
456
}
457

            
458
#[cfg(test)]
459
mod tests {
460
    use super::*;
461
    use crate::ast::primitive::{SourceInfo, Template, TemplateElement};
462
    use crate::reader::Pos;
463
    use crate::typing::ToSource;
464

            
465
    fn whitespace() -> Whitespace {
466
        Whitespace {
467
            value: String::new(),
468
            source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
469
        }
470
    }
471

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