Lines
100 %
Functions
Branches
/*
* 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 base64::engine::general_purpose;
use base64::Engine;
use hurl_core::ast::*;
use hurl_core::typing::{Count, Duration};
use super::serialize_json::*;
pub fn format(hurl_file: &HurlFile) -> String {
hurl_file.to_json().format()
}
pub trait ToJson {
fn to_json(&self) -> JValue;
impl ToJson for HurlFile {
fn to_json(&self) -> JValue {
JValue::Object(vec![(
"entries".to_string(),
JValue::List(self.entries.iter().map(|e| e.to_json()).collect()),
)])
impl ToJson for Entry {
let mut attributes = vec![("request".to_string(), self.request.to_json())];
if let Some(response) = &self.response {
attributes.push(("response".to_string(), response.to_json()));
JValue::Object(attributes)
impl ToJson for Request {
let mut attributes = vec![
(
"method".to_string(),
JValue::String(self.method.to_string()),
),
("url".to_string(), JValue::String(self.url.to_string())),
];
add_headers(&mut attributes, &self.headers);
if !self.querystring_params().is_empty() {
let params = self
.querystring_params()
.iter()
.map(|p| p.to_json())
.collect();
attributes.push(("query_string_params".to_string(), JValue::List(params)));
if !self.form_params().is_empty() {
let params = self.form_params().iter().map(|p| p.to_json()).collect();
attributes.push(("form_params".to_string(), JValue::List(params)));
if !self.multipart_form_data().is_empty() {
.multipart_form_data()
attributes.push(("multipart_form_data".to_string(), JValue::List(params)));
if !self.cookies().is_empty() {
let cookies = self.cookies().iter().map(|c| c.to_json()).collect();
attributes.push(("cookies".to_string(), JValue::List(cookies)));
if !self.options().is_empty() {
let options = self.options().iter().map(|c| c.to_json()).collect();
attributes.push(("options".to_string(), JValue::List(options)));
if let Some(body) = &self.body {
attributes.push(("body".to_string(), body.to_json()));
// Request comments (can be used to check custom commands)
let comments: Vec<_> = self
.line_terminators
.filter_map(|l| l.comment.as_ref())
if !comments.is_empty() {
let comments = comments.iter().map(|c| c.to_json()).collect();
attributes.push(("comments".to_string(), JValue::List(comments)));
impl ToJson for Response {
/// Transforms this response to a JSON object.
let mut attributes = vec![];
if let Some(v) = get_json_version(&self.version.value) {
attributes.push(("version".to_string(), JValue::String(v)));
if let StatusValue::Specific(n) = self.status.value {
attributes.push(("status".to_string(), JValue::Number(n.to_string())));
if !self.captures().is_empty() {
let captures = self.captures().iter().map(|c| c.to_json()).collect();
attributes.push(("captures".to_string(), JValue::List(captures)));
if !self.asserts().is_empty() {
let asserts = self.asserts().iter().map(|a| a.to_json()).collect();
attributes.push(("asserts".to_string(), JValue::List(asserts)));
fn add_headers(attributes: &mut Vec<(String, JValue)>, headers: &[Header]) {
if !headers.is_empty() {
let headers = JValue::List(headers.iter().map(|h| h.to_json()).collect());
attributes.push(("headers".to_string(), headers));
impl ToJson for Body {
self.value.to_json()
impl ToJson for Bytes {
match self {
Bytes::Base64(value) => value.to_json(),
Bytes::Hex(value) => value.to_json(),
Bytes::File(value) => value.to_json(),
Bytes::Json(value) => JValue::Object(vec![
("type".to_string(), JValue::String("json".to_string())),
("value".to_string(), value.to_json()),
]),
Bytes::Xml(value) => JValue::Object(vec![
("type".to_string(), JValue::String("xml".to_string())),
("value".to_string(), JValue::String(value.clone())),
Bytes::OnelineString(value) => JValue::Object(vec![
("type".to_string(), JValue::String("text".to_string())),
("value".to_string(), JValue::String(value.to_string())),
Bytes::MultilineString(multi) => {
// TODO: check these values. Maybe we want to have the same
// export when using:
//
// ~~~
// GET https://foo.com
// ```base64
// SGVsbG8gd29ybGQ=
// ```
// or
// base64,SGVsbG8gd29ybGQ=;
let lang = match multi {
MultilineString {
kind: MultilineStringKind::Text(_),
..
} => "text",
kind: MultilineStringKind::Json(_),
} => "json",
kind: MultilineStringKind::Xml(_),
} => "xml",
kind: MultilineStringKind::GraphQl(_),
} => "graphql",
};
JValue::Object(vec![
("type".to_string(), JValue::String(lang.to_string())),
("value".to_string(), JValue::String(multi.to_string())),
])
impl ToJson for Base64 {
let value = general_purpose::STANDARD.encode(&self.value);
("encoding".to_string(), JValue::String("base64".to_string())),
("value".to_string(), JValue::String(value)),
impl ToJson for Hex {
impl ToJson for File {
("type".to_string(), JValue::String("file".to_string())),
"filename".to_string(),
JValue::String(self.filename.to_string()),
fn get_json_version(version_value: &VersionValue) -> Option<String> {
match version_value {
VersionValue::Version1 => Some("HTTP/1.0".to_string()),
VersionValue::Version11 => Some("HTTP/1.1".to_string()),
VersionValue::Version2 => Some("HTTP/2".to_string()),
VersionValue::Version3 => Some("HTTP/3".to_string()),
VersionValue::VersionAny => None,
VersionValue::VersionAnyLegacy => None,
impl ToJson for KeyValue {
let attributes = vec![
("name".to_string(), JValue::String(self.key.to_string())),
("value".to_string(), JValue::String(self.value.to_string())),
impl ToJson for MultipartParam {
MultipartParam::Param(param) => param.to_json(),
MultipartParam::FileParam(param) => param.to_json(),
impl ToJson for FileParam {
JValue::String(self.value.filename.to_string()),
if let Some(content_type) = self.value.content_type.clone() {
attributes.push(("content_type".to_string(), JValue::String(content_type)));
impl ToJson for Cookie {
("name".to_string(), JValue::String(self.name.to_string())),
impl ToJson for EntryOption {
let value = match &self.kind {
OptionKind::AwsSigV4(value) => JValue::String(value.to_string()),
OptionKind::CaCertificate(filename) => JValue::String(filename.to_string()),
OptionKind::ClientCert(filename) => JValue::String(filename.to_string()),
OptionKind::ClientKey(filename) => JValue::String(filename.to_string()),
OptionKind::Compressed(value) => value.to_json(),
OptionKind::ConnectTo(value) => JValue::String(value.to_string()),
OptionKind::ConnectTimeout(value) => value.to_json(),
OptionKind::Delay(value) => value.to_json(),
OptionKind::FollowLocation(value) => value.to_json(),
OptionKind::FollowLocationTrusted(value) => value.to_json(),
OptionKind::Http10(value) => value.to_json(),
OptionKind::Http11(value) => value.to_json(),
OptionKind::Http2(value) => value.to_json(),
OptionKind::Http3(value) => value.to_json(),
OptionKind::Insecure(value) => value.to_json(),
OptionKind::IpV4(value) => value.to_json(),
OptionKind::IpV6(value) => value.to_json(),
OptionKind::LimitRate(value) => value.to_json(),
OptionKind::MaxRedirect(value) => value.to_json(),
OptionKind::NetRc(value) => value.to_json(),
OptionKind::NetRcFile(filename) => JValue::String(filename.to_string()),
OptionKind::NetRcOptional(value) => value.to_json(),
OptionKind::Output(filename) => JValue::String(filename.to_string()),
OptionKind::PathAsIs(value) => value.to_json(),
OptionKind::Proxy(value) => JValue::String(value.to_string()),
OptionKind::Repeat(value) => value.to_json(),
OptionKind::Resolve(value) => JValue::String(value.to_string()),
OptionKind::Retry(value) => value.to_json(),
OptionKind::RetryInterval(value) => value.to_json(),
OptionKind::Skip(value) => value.to_json(),
OptionKind::UnixSocket(value) => JValue::String(value.to_string()),
OptionKind::User(value) => JValue::String(value.to_string()),
OptionKind::Variable(value) => {
JValue::String(format!("{}={}", value.name, value.value))
OptionKind::Verbose(value) => value.to_json(),
OptionKind::VeryVerbose(value) => value.to_json(),
// If the value contains the unit such as `{ "value": 10, "unit": "second" }`
// The JSON for this option should still have one level
// for example: { "name": "delay", "value": 10, "unit", "second" }
let attributes = if let JValue::Object(mut attributes) = value {
attributes.push((
"name".to_string(),
JValue::String(self.kind.name().to_string()),
));
attributes
} else {
vec![
("value".to_string(), value),
]
impl ToJson for BooleanOption {
BooleanOption::Literal(value) => JValue::Boolean(*value),
BooleanOption::Placeholder(placeholder) => placeholder.to_json(),
impl ToJson for CountOption {
CountOption::Literal(value) => value.to_json(),
CountOption::Placeholder(placeholder) => placeholder.to_json(),
impl ToJson for Count {
Count::Finite(n) => JValue::Number(n.to_string()),
Count::Infinite => JValue::Number("-1".to_string()),
impl ToJson for DurationOption {
DurationOption::Literal(value) => value.to_json(),
DurationOption::Placeholder(placeholder) => placeholder.to_json(),
impl ToJson for Duration {
if let Some(unit) = self.unit {
let mut attributes =
vec![("value".to_string(), JValue::Number(self.value.to_string()))];
attributes.push(("unit".to_string(), JValue::String(unit.to_string())));
JValue::Number(self.value.to_string())
impl ToJson for Capture {
("query".to_string(), self.query.to_json()),
if !self.filters.is_empty() {
let filters = JValue::List(self.filters.iter().map(|(_, f)| f.to_json()).collect());
attributes.push(("filters".to_string(), filters));
impl ToJson for Assert {
let mut attributes = vec![("query".to_string(), self.query.to_json())];
attributes.push(("predicate".to_string(), self.predicate.to_json()));
impl ToJson for Query {
let attributes = query_value_attributes(&self.value);
fn query_value_attributes(query_value: &QueryValue) -> Vec<(String, JValue)> {
match query_value {
QueryValue::Status => {
attributes.push(("type".to_string(), JValue::String("status".to_string())));
QueryValue::Url => {
attributes.push(("type".to_string(), JValue::String("url".to_string())));
QueryValue::Body => {
attributes.push(("type".to_string(), JValue::String("body".to_string())));
QueryValue::Jsonpath { expr, .. } => {
attributes.push(("type".to_string(), JValue::String("jsonpath".to_string())));
attributes.push(("expr".to_string(), JValue::String(expr.to_string())));
QueryValue::Header { name, .. } => {
attributes.push(("type".to_string(), JValue::String("header".to_string())));
attributes.push(("name".to_string(), JValue::String(name.to_string())));
QueryValue::Cookie { expr, .. } => {
attributes.push(("type".to_string(), JValue::String("cookie".to_string())));
QueryValue::Xpath { expr, .. } => {
attributes.push(("type".to_string(), JValue::String("xpath".to_string())));
QueryValue::Regex { value, .. } => {
attributes.push(("type".to_string(), JValue::String("regex".to_string())));
attributes.push(("expr".to_string(), value.to_json()));
QueryValue::Variable { name, .. } => {
attributes.push(("type".to_string(), JValue::String("variable".to_string())));
QueryValue::Duration => {
attributes.push(("type".to_string(), JValue::String("duration".to_string())));
QueryValue::Bytes => {
attributes.push(("type".to_string(), JValue::String("bytes".to_string())));
QueryValue::Sha256 => {
attributes.push(("type".to_string(), JValue::String("sha256".to_string())));
QueryValue::Md5 => {
attributes.push(("type".to_string(), JValue::String("md5".to_string())));
QueryValue::Certificate {
attribute_name: field,
} => {
"type".to_string(),
JValue::String("certificate".to_string()),
attributes.push(("expr".to_string(), field.to_json()));
impl ToJson for RegexValue {
RegexValue::Template(template) => JValue::String(template.to_string()),
RegexValue::Regex(regex) => regex.to_json(),
impl ToJson for Regex {
("type".to_string(), JValue::String("regex".to_string())),
("value".to_string(), JValue::String(self.to_string())),
impl ToJson for CertificateAttributeName {
let value = match self {
CertificateAttributeName::Subject => "Subject",
CertificateAttributeName::Issuer => "Issuer",
CertificateAttributeName::StartDate => "Start-Date",
CertificateAttributeName::ExpireDate => "Expire-Date",
CertificateAttributeName::SerialNumber => "Serial-Number",
JValue::String(value.to_string())
impl ToJson for Predicate {
if self.not {
attributes.push(("not".to_string(), JValue::Boolean(true)));
match self.predicate_func.value.clone() {
PredicateFuncValue::Equal { value, .. } => {
attributes.push(("type".to_string(), JValue::String("equal".to_string())));
add_predicate_value(&mut attributes, value);
PredicateFuncValue::NotEqual { value, .. } => {
attributes.push(("type".to_string(), JValue::String("not-equal".to_string())));
PredicateFuncValue::GreaterThan { value, .. } => {
attributes.push(("type".to_string(), JValue::String("greater".to_string())));
PredicateFuncValue::GreaterThanOrEqual { value, .. } => {
JValue::String("greater-or-equal".to_string()),
PredicateFuncValue::LessThan { value, .. } => {
attributes.push(("type".to_string(), JValue::String("less".to_string())));
PredicateFuncValue::LessThanOrEqual { value, .. } => {
JValue::String("less-or-equal".to_string()),
PredicateFuncValue::StartWith { value, .. } => {
attributes.push(("type".to_string(), JValue::String("start-with".to_string())));
PredicateFuncValue::EndWith { value, .. } => {
attributes.push(("type".to_string(), JValue::String("end-with".to_string())));
PredicateFuncValue::Contain { value, .. } => {
attributes.push(("type".to_string(), JValue::String("contain".to_string())));
PredicateFuncValue::Include { value, .. } => {
attributes.push(("type".to_string(), JValue::String("include".to_string())));
PredicateFuncValue::Match { value, .. } => {
attributes.push(("type".to_string(), JValue::String("match".to_string())));
PredicateFuncValue::IsInteger => {
attributes.push(("type".to_string(), JValue::String("isInteger".to_string())));
PredicateFuncValue::IsFloat => {
attributes.push(("type".to_string(), JValue::String("isFloat".to_string())));
PredicateFuncValue::IsBoolean => {
attributes.push(("type".to_string(), JValue::String("isBoolean".to_string())));
PredicateFuncValue::IsString => {
attributes.push(("type".to_string(), JValue::String("isString".to_string())));
PredicateFuncValue::IsCollection => {
JValue::String("isCollection".to_string()),
PredicateFuncValue::IsDate => {
attributes.push(("type".to_string(), JValue::String("isDate".to_string())));
PredicateFuncValue::IsIsoDate => {
attributes.push(("type".to_string(), JValue::String("isIsoDate".to_string())));
PredicateFuncValue::Exist => {
attributes.push(("type".to_string(), JValue::String("exist".to_string())));
PredicateFuncValue::IsEmpty => {
attributes.push(("type".to_string(), JValue::String("isEmpty".to_string())));
PredicateFuncValue::IsNumber => {
attributes.push(("type".to_string(), JValue::String("isNumber".to_string())));
fn add_predicate_value(attributes: &mut Vec<(String, JValue)>, predicate_value: PredicateValue) {
let (value, encoding) = json_predicate_value(predicate_value);
attributes.push(("value".to_string(), value));
if let Some(encoding) = encoding {
attributes.push(("encoding".to_string(), JValue::String(encoding)));
fn json_predicate_value(predicate_value: PredicateValue) -> (JValue, Option<String>) {
match predicate_value {
PredicateValue::String(value) => (JValue::String(value.to_string()), None),
PredicateValue::MultilineString(value) => (JValue::String(value.value().to_string()), None),
PredicateValue::Bool(value) => (JValue::Boolean(value), None),
PredicateValue::Null => (JValue::Null, None),
PredicateValue::Number(value) => (JValue::Number(value.to_string()), None),
PredicateValue::File(value) => (value.to_json(), None),
PredicateValue::Hex(value) => {
let base64_string = general_purpose::STANDARD.encode(value.value);
(JValue::String(base64_string), Some("base64".to_string()))
PredicateValue::Base64(value) => {
PredicateValue::Placeholder(value) => (JValue::String(value.to_string()), None),
PredicateValue::Regex(value) => {
(JValue::String(value.to_string()), Some("regex".to_string()))
impl ToJson for JsonValue {
JsonValue::Null => JValue::Null,
JsonValue::Number(s) => JValue::Number(s.to_string()),
JsonValue::String(s) => JValue::String(s.to_string()),
JsonValue::Boolean(v) => JValue::Boolean(*v),
JsonValue::List { elements, .. } => {
JValue::List(elements.iter().map(|e| e.to_json()).collect())
JsonValue::Object { elements, .. } => JValue::Object(
elements
.map(|elem| (elem.name.to_string(), elem.value.to_json()))
.collect(),
JsonValue::Placeholder(exp) => JValue::String(format!("{{{{{exp}}}}}")),
impl ToJson for JsonListElement {
impl ToJson for Filter {
impl ToJson for FilterValue {
FilterValue::Count => {
attributes.push(("type".to_string(), JValue::String("count".to_string())));
FilterValue::DaysAfterNow => {
JValue::String("daysAfterNow".to_string()),
FilterValue::DaysBeforeNow => {
JValue::String("daysBeforeNow".to_string()),
FilterValue::Decode { encoding, .. } => {
attributes.push(("type".to_string(), JValue::String("decode".to_string())));
attributes.push(("encoding".to_string(), JValue::String(encoding.to_string())));
FilterValue::Format { fmt, .. } => {
attributes.push(("type".to_string(), JValue::String("format".to_string())));
attributes.push(("fmt".to_string(), JValue::String(fmt.to_string())));
FilterValue::JsonPath { expr, .. } => {
FilterValue::Nth { n, .. } => {
attributes.push(("type".to_string(), JValue::String("nth".to_string())));
attributes.push(("n".to_string(), JValue::Number(n.to_string())));
FilterValue::HtmlEscape => {
attributes.push(("type".to_string(), JValue::String("htmlEscape".to_string())));
FilterValue::HtmlUnescape => {
JValue::String("htmlUnescape".to_string()),
FilterValue::Regex { value, .. } => {
FilterValue::Replace {
old_value,
new_value,
attributes.push(("type".to_string(), JValue::String("replace".to_string())));
attributes.push(("old_value".to_string(), old_value.to_json()));
"new_value".to_string(),
JValue::String(new_value.to_string()),
FilterValue::UrlEncode => {
attributes.push(("type".to_string(), JValue::String("urlEncode".to_string())));
FilterValue::UrlDecode => {
attributes.push(("type".to_string(), JValue::String("urlDecode".to_string())));
FilterValue::Split { sep, .. } => {
attributes.push(("type".to_string(), JValue::String("split".to_string())));
attributes.push(("sep".to_string(), JValue::String(sep.to_string())));
FilterValue::ToDate { fmt, .. } => {
attributes.push(("type".to_string(), JValue::String("toDate".to_string())));
FilterValue::ToFloat => {
attributes.push(("type".to_string(), JValue::String("toFloat".to_string())));
FilterValue::ToInt => {
attributes.push(("type".to_string(), JValue::String("toInt".to_string())));
FilterValue::XPath { expr, .. } => {
impl ToJson for Placeholder {
JValue::String(format!("{{{{{}}}}}", self))
impl ToJson for Comment {
JValue::String(self.value.to_string())
impl ToJson for NaturalOption {
NaturalOption::Literal(value) => JValue::Number(value.to_string()),
NaturalOption::Placeholder(placeholder) => placeholder.to_json(),
#[cfg(test)]
pub mod tests {
use hurl_core::reader::Pos;
use super::*;
fn whitespace() -> Whitespace {
Whitespace {
value: String::new(),
source_info: SourceInfo::new(Pos::new(0, 0), Pos::new(0, 0)),
fn line_terminator() -> LineTerminator {
LineTerminator {
space0: whitespace(),
comment: None,
newline: whitespace(),
#[test]
pub fn test_request() {
assert_eq!(
Request {
line_terminators: vec![],
method: Method("GET".to_string()),
space1: whitespace(),
url: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "http://example.com".to_string(),
encoded: "not_used".to_string(),
}],
},
line_terminator0: line_terminator(),
headers: vec![KeyValue {
key: Template {
value: "Foo".to_string(),
encoded: "unused".to_string(),
space2: whitespace(),
value: Template {
value: "Bar".to_string(),
sections: vec![],
body: None,
.to_json(),
("method".to_string(), JValue::String("GET".to_string())),
"url".to_string(),
JValue::String("http://example.com".to_string())
"headers".to_string(),
JValue::List(vec![JValue::Object(vec![
("name".to_string(), JValue::String("Foo".to_string())),
("value".to_string(), JValue::String("Bar".to_string()))
])])
)
);
pub fn test_response() {
Response {
version: Version {
value: VersionValue::Version11,
status: Status {
value: StatusValue::Specific(200),
headers: vec![],
"version".to_string(),
JValue::String("HTTP/1.1".to_string())
("status".to_string(), JValue::Number("200".to_string()))
value: VersionValue::VersionAny,
value: StatusValue::Any,
JValue::Object(vec![])
fn header_query() -> Query {
Query {
value: QueryValue::Header {
name: Template {
value: "Content-Length".to_string(),
encoded: "10".to_string(),
fn header_capture() -> Capture {
Capture {
value: "size".to_string(),
query: header_query(),
filters: vec![],
fn header_assert() -> Assert {
Assert {
predicate: equal_int_predicate(10),
fn equal_int_predicate(value: i64) -> Predicate {
Predicate {
not: false,
predicate_func: PredicateFunc {
value: PredicateFuncValue::Equal {
value: PredicateValue::Number(Number::Integer(value)),
operator: false,
pub fn test_query() {
header_query().to_json(),
("type".to_string(), JValue::String("header".to_string())),
JValue::String("Content-Length".to_string())
pub fn test_capture() {
header_capture().to_json(),
("name".to_string(), JValue::String("size".to_string())),
"query".to_string(),
pub fn test_predicate() {
equal_int_predicate(10).to_json(),
("type".to_string(), JValue::String("equal".to_string())),
("value".to_string(), JValue::Number("10".to_string()))
pub fn test_assert() {
header_assert().to_json(),
"predicate".to_string(),