Lines
92.5 %
Functions
89.47 %
Branches
100 %
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2024 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use crate::ast::json;
use crate::reader::Pos;
use crate::typing::{Count, Duration};
///
/// Hurl AST
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HurlFile {
pub entries: Vec<Entry>,
pub line_terminators: Vec<LineTerminator>,
}
pub struct Entry {
pub request: Request,
pub response: Option<Response>,
impl Entry {
/// Returns the source information for this entry.
pub fn source_info(&self) -> SourceInfo {
self.request.space0.source_info
pub struct Request {
pub space0: Whitespace,
pub method: Method,
pub space1: Whitespace,
pub url: Template,
pub line_terminator0: LineTerminator,
pub headers: Vec<Header>,
pub sections: Vec<Section>,
pub body: Option<Body>,
pub source_info: SourceInfo,
impl Request {
pub fn querystring_params(&self) -> Vec<KeyValue> {
for section in &self.sections {
if let SectionValue::QueryParams(params, _) = §ion.value {
return params.clone();
vec![]
pub fn form_params(&self) -> Vec<KeyValue> {
if let SectionValue::FormParams(params, _) = §ion.value {
pub fn multipart_form_data(&self) -> Vec<MultipartParam> {
if let SectionValue::MultipartFormData(params, _) = §ion.value {
pub fn cookies(&self) -> Vec<Cookie> {
if let SectionValue::Cookies(cookies) = §ion.value {
return cookies.clone();
pub fn basic_auth(&self) -> Option<KeyValue> {
if let SectionValue::BasicAuth(kv) = §ion.value {
return kv.clone();
None
pub fn options(&self) -> Vec<EntryOption> {
if let SectionValue::Options(options) = §ion.value {
return options.clone();
pub struct Response {
pub version: Version,
pub status: Status,
impl Response {
/// Returns the captures list of this spec response.
pub fn captures(&self) -> &[Capture] {
for section in self.sections.iter() {
if let SectionValue::Captures(captures) = §ion.value {
return captures;
&[]
/// Returns the asserts list of this spec response.
pub fn asserts(&self) -> &[Assert] {
if let SectionValue::Asserts(asserts) = §ion.value {
return asserts;
pub struct Method(pub String);
pub struct Version {
pub value: VersionValue,
pub enum VersionValue {
Version1,
Version11,
Version2,
Version3,
VersionAny,
VersionAnyLegacy,
pub struct Status {
pub value: StatusValue,
pub enum StatusValue {
Any,
Specific(u64),
pub type Header = KeyValue;
pub struct Body {
pub value: Bytes,
//
// Sections
pub struct Section {
pub value: SectionValue,
impl Section {
pub fn name(&self) -> &str {
match self.value {
SectionValue::Asserts(_) => "Asserts",
SectionValue::QueryParams(_, true) => "Query",
SectionValue::QueryParams(_, false) => "QueryStringParams",
SectionValue::BasicAuth(_) => "BasicAuth",
SectionValue::FormParams(_, true) => "Form",
SectionValue::FormParams(_, false) => "FormParams",
SectionValue::Cookies(_) => "Cookies",
SectionValue::Captures(_) => "Captures",
SectionValue::MultipartFormData(_, true) => "Multipart",
SectionValue::MultipartFormData(_, false) => "MultipartFormData",
SectionValue::Options(_) => "Options",
#[allow(clippy::large_enum_variant)]
pub enum SectionValue {
QueryParams(Vec<KeyValue>, bool), // boolean param indicates if we use the short syntax
BasicAuth(Option<KeyValue>), // boolean param indicates if we use the short syntax
FormParams(Vec<KeyValue>, bool),
MultipartFormData(Vec<MultipartParam>, bool), // boolean param indicates if we use the short syntax
Cookies(Vec<Cookie>),
Captures(Vec<Capture>),
Asserts(Vec<Assert>),
Options(Vec<EntryOption>),
pub struct Cookie {
pub name: Template,
pub space2: Whitespace,
pub value: Template,
pub struct KeyValue {
pub key: Template,
pub enum MultipartParam {
Param(KeyValue),
FileParam(FileParam),
pub struct FileParam {
pub value: FileValue,
pub struct FileValue {
pub filename: Template,
pub content_type: Option<String>,
pub struct Capture {
pub query: Query,
pub filters: Vec<(Whitespace, Filter)>,
pub struct Assert {
pub predicate: Predicate,
pub struct Query {
pub value: QueryValue,
pub enum QueryValue {
Status,
Url,
Header {
space0: Whitespace,
name: Template,
},
Cookie {
expr: CookiePath,
Body,
Xpath {
expr: Template,
Jsonpath {
Regex {
value: RegexValue,
Variable {
Duration,
Bytes,
Sha256,
Md5,
Certificate {
attribute_name: CertificateAttributeName,
pub enum RegexValue {
Template(Template),
Regex(Regex),
pub struct CookiePath {
pub attribute: Option<CookieAttribute>,
pub struct CookieAttribute {
pub name: CookieAttributeName,
pub enum CookieAttributeName {
Value(String),
Expires(String),
MaxAge(String),
Domain(String),
Path(String),
Secure(String),
HttpOnly(String),
SameSite(String),
impl CookieAttributeName {
pub fn value(&self) -> String {
match self {
CookieAttributeName::Value(value)
| CookieAttributeName::Expires(value)
| CookieAttributeName::MaxAge(value)
| CookieAttributeName::Domain(value)
| CookieAttributeName::Path(value)
| CookieAttributeName::Secure(value)
| CookieAttributeName::HttpOnly(value)
| CookieAttributeName::SameSite(value) => value.to_string(),
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum CertificateAttributeName {
Subject,
Issuer,
StartDate,
ExpireDate,
SerialNumber,
pub struct Predicate {
pub not: bool,
pub predicate_func: PredicateFunc,
pub struct Not {
pub value: bool,
pub struct PredicateFunc {
pub value: PredicateFuncValue,
pub enum PredicateValue {
Base64(Base64),
Bool(bool),
File(File),
Hex(Hex),
MultilineString(MultilineString),
Null,
Number(Number),
Placeholder(Placeholder),
String(Template),
pub enum PredicateFuncValue {
Equal {
value: PredicateValue,
operator: bool,
NotEqual {
GreaterThan {
GreaterThanOrEqual {
LessThan {
LessThanOrEqual {
StartWith {
EndWith {
Contain {
Include {
Match {
IsInteger,
IsFloat,
IsBoolean,
IsString,
IsCollection,
IsDate,
IsIsoDate,
Exist,
IsEmpty,
IsNumber,
// Primitives
pub struct MultilineString {
pub kind: MultilineStringKind,
pub attributes: Vec<MultilineStringAttribute>,
pub enum MultilineStringKind {
Text(Text),
Json(Text),
Xml(Text),
GraphQl(GraphQl),
pub enum MultilineStringAttribute {
Escape,
NoVariable,
impl MultilineString {
pub fn lang(&self) -> &'static str {
match self.kind {
MultilineStringKind::Text(_) => "",
MultilineStringKind::Json(_) => "json",
MultilineStringKind::Xml(_) => "xml",
MultilineStringKind::GraphQl(_) => "graphql",
pub fn value(&self) -> Template {
match &self.kind {
MultilineStringKind::Text(text)
| MultilineStringKind::Json(text)
| MultilineStringKind::Xml(text) => text.value.clone(),
MultilineStringKind::GraphQl(text) => text.value.clone(),
pub struct Text {
pub space: Whitespace,
pub newline: Whitespace,
pub struct GraphQl {
pub variables: Option<GraphQlVariables>,
pub struct GraphQlVariables {
pub value: json::Value,
pub whitespace: Whitespace,
pub struct Base64 {
pub value: Vec<u8>,
pub encoded: String,
pub struct File {
pub struct Template {
pub delimiter: Option<char>,
pub elements: Vec<TemplateElement>,
pub enum TemplateElement {
// TODO: explain the difference between value and encoded
String { value: String, encoded: String },
pub struct Comment {
pub value: String,
pub struct EncodedString {
pub quotes: bool,
pub struct Whitespace {
pub enum Number {
Float(Float),
Integer(i64),
BigInteger(String),
// keep Number terminology for both Integer and Decimal Numbers
// different representation for the same float value
// 1.01 and 1.010
#[derive(Clone, Debug)]
pub struct Float {
pub value: f64,
pub encoded: String, // as defined in Hurl
impl PartialEq for Float {
fn eq(&self, other: &Self) -> bool {
self.encoded == other.encoded
impl Eq for Float {}
pub struct LineTerminator {
pub comment: Option<Comment>,
pub enum Bytes {
Json(json::Value),
Xml(String),
OnelineString(Template),
pub struct Hex {
// Literal Regex
pub struct Regex {
pub inner: regex::Regex,
impl PartialEq for Regex {
self.inner.to_string() == other.inner.to_string()
impl Eq for Regex {}
pub struct SourceInfo {
pub start: Pos,
pub end: Pos,
impl SourceInfo {
pub fn new(start: Pos, end: Pos) -> SourceInfo {
SourceInfo { start, end }
pub struct Placeholder {
pub expr: Expr,
pub struct Expr {
pub kind: ExprKind,
pub enum ExprKind {
Variable(Variable),
Function(Function),
pub struct Variable {
pub name: String,
pub enum Function {
NewDate,
NewUuid,
/// Check that variable name is not reserved
/// (would conflicts with an existing function)
pub fn is_variable_reserved(name: &str) -> bool {
["getEnv", "newDate", "newUuid"].contains(&name)
pub struct EntryOption {
pub kind: OptionKind,
pub enum OptionKind {
AwsSigV4(Template),
CaCertificate(Template),
ClientCert(Template),
ClientKey(Template),
Compressed(BooleanOption),
ConnectTo(Template),
ConnectTimeout(DurationOption),
Delay(DurationOption),
Http10(BooleanOption),
Http11(BooleanOption),
Http2(BooleanOption),
Http3(BooleanOption),
Insecure(BooleanOption),
IpV4(BooleanOption),
IpV6(BooleanOption),
FollowLocation(BooleanOption),
FollowLocationTrusted(BooleanOption),
LimitRate(NaturalOption),
MaxRedirect(CountOption),
NetRc(BooleanOption),
NetRcFile(Template),
NetRcOptional(BooleanOption),
Output(Template),
PathAsIs(BooleanOption),
Proxy(Template),
Repeat(CountOption),
Resolve(Template),
Retry(CountOption),
RetryInterval(DurationOption),
Skip(BooleanOption),
UnixSocket(Template),
User(Template),
Variable(VariableDefinition),
Verbose(BooleanOption),
VeryVerbose(BooleanOption),
impl OptionKind {
pub fn name(&self) -> &'static str {
OptionKind::AwsSigV4(_) => "aws-sigv4",
OptionKind::CaCertificate(_) => "cacert",
OptionKind::ClientCert(_) => "cert",
OptionKind::ClientKey(_) => "key",
OptionKind::Compressed(_) => "compressed",
OptionKind::ConnectTo(_) => "connect-to",
OptionKind::ConnectTimeout(_) => "connect-timeout",
OptionKind::Delay(_) => "delay",
OptionKind::FollowLocation(_) => "location",
OptionKind::FollowLocationTrusted(_) => "location-trusted",
OptionKind::Http10(_) => "http1.0",
OptionKind::Http11(_) => "http1.1",
OptionKind::Http2(_) => "http2",
OptionKind::Http3(_) => "http3",
OptionKind::Insecure(_) => "insecure",
OptionKind::IpV4(_) => "ipv4",
OptionKind::IpV6(_) => "ipv6",
OptionKind::LimitRate(_) => "limit-rate",
OptionKind::MaxRedirect(_) => "max-redirs",
OptionKind::NetRc(_) => "netrc",
OptionKind::NetRcFile(_) => "netrc-file",
OptionKind::NetRcOptional(_) => "netrc-optional",
OptionKind::Output(_) => "output",
OptionKind::PathAsIs(_) => "path-as-is",
OptionKind::Proxy(_) => "proxy",
OptionKind::Repeat(_) => "repeat",
OptionKind::Resolve(_) => "resolve",
OptionKind::Retry(_) => "retry",
OptionKind::RetryInterval(_) => "retry-interval",
OptionKind::Skip(_) => "skip",
OptionKind::UnixSocket(_) => "unix-socket",
OptionKind::User(_) => "user",
OptionKind::Variable(_) => "variable",
OptionKind::Verbose(_) => "verbose",
OptionKind::VeryVerbose(_) => "very-verbose",
pub fn value_as_str(&self) -> String {
OptionKind::AwsSigV4(value) => value.to_string(),
OptionKind::CaCertificate(filename) => filename.to_string(),
OptionKind::ClientCert(filename) => filename.to_string(),
OptionKind::ClientKey(filename) => filename.to_string(),
OptionKind::Compressed(value) => value.to_string(),
OptionKind::ConnectTo(value) => value.to_string(),
OptionKind::ConnectTimeout(value) => value.to_string(),
OptionKind::Delay(value) => value.to_string(),
OptionKind::FollowLocation(value) => value.to_string(),
OptionKind::FollowLocationTrusted(value) => value.to_string(),
OptionKind::Http10(value) => value.to_string(),
OptionKind::Http11(value) => value.to_string(),
OptionKind::Http2(value) => value.to_string(),
OptionKind::Http3(value) => value.to_string(),
OptionKind::Insecure(value) => value.to_string(),
OptionKind::IpV4(value) => value.to_string(),
OptionKind::IpV6(value) => value.to_string(),
OptionKind::LimitRate(value) => value.to_string(),
OptionKind::MaxRedirect(value) => value.to_string(),
OptionKind::NetRc(value) => value.to_string(),
OptionKind::NetRcFile(filename) => filename.to_string(),
OptionKind::NetRcOptional(value) => value.to_string(),
OptionKind::Output(filename) => filename.to_string(),
OptionKind::PathAsIs(value) => value.to_string(),
OptionKind::Proxy(value) => value.to_string(),
OptionKind::Repeat(value) => value.to_string(),
OptionKind::Resolve(value) => value.to_string(),
OptionKind::Retry(value) => value.to_string(),
OptionKind::RetryInterval(value) => value.to_string(),
OptionKind::Skip(value) => value.to_string(),
OptionKind::UnixSocket(value) => value.to_string(),
OptionKind::User(value) => value.to_string(),
OptionKind::Variable(VariableDefinition { name, value, .. }) => {
format!("{name}={value}")
OptionKind::Verbose(value) => value.to_string(),
OptionKind::VeryVerbose(value) => value.to_string(),
pub enum BooleanOption {
Literal(bool),
pub enum NaturalOption {
Literal(u64),
pub enum CountOption {
Literal(Count),
pub enum DurationOption {
Literal(Duration),
pub struct VariableDefinition {
pub value: VariableValue,
pub enum VariableValue {
pub struct Filter {
pub value: FilterValue,
pub enum FilterValue {
Count,
DaysAfterNow,
DaysBeforeNow,
Decode {
encoding: Template,
Format {
fmt: Template,
HtmlEscape,
HtmlUnescape,
JsonPath {
Nth {
n: u64,
Replace {
old_value: RegexValue,
space1: Whitespace,
new_value: Template,
Split {
sep: Template,
ToDate {
ToFloat,
ToInt,
UrlDecode,
UrlEncode,
XPath {