Merge pull request #22 from Orange-OpenSource/feature/refacto-http

Clean/Refacto http module
This commit is contained in:
Fabrice Reix 2020-09-18 21:53:44 +02:00 committed by GitHub
commit 7fbedb5a7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 834 additions and 742 deletions

View File

@ -30,7 +30,7 @@ use clap::{AppSettings, ArgMatches};
use hurl::cli;
use hurl::core::common::FormatError;
use hurl::html;
use hurl::http::libcurl;
use hurl::http;
use hurl::parser;
use hurl::runner;
use hurl::runner::core::*;
@ -109,7 +109,7 @@ fn execute(filename: &str,
let proxy = cli_options.proxy;
let no_proxy = cli_options.no_proxy;
let cookie_input_file = cli_options.cookie_input_file;
let options = libcurl::client::ClientOptions {
let options = http::ClientOptions {
follow_location,
max_redirect,
cookie_input_file,
@ -118,7 +118,7 @@ fn execute(filename: &str,
verbose,
insecure,
};
let mut client = libcurl::client::Client::init(options);
let mut client = http::Client::init(options);
let context_dir = match file_root {

View File

@ -16,15 +16,26 @@
*
*/
use std::io::Read;
use std::str;
use curl::easy;
use encoding::all::ISO_8859_1;
use encoding::{DecoderTrap, Encoding};
use super::core::*;
use std::io::Read;
use encoding::{Encoding, DecoderTrap};
use encoding::all::ISO_8859_1;
use super::request::*;
use super::response::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HttpError {
CouldNotResolveProxyName,
CouldNotResolveHost,
FailToConnect,
TooManyRedirect,
CouldNotParseResponse,
SSLCertificate,
}
#[derive(Debug)]
pub struct Client {
@ -50,9 +61,7 @@ pub struct ClientOptions {
pub insecure: bool,
}
impl Client {
///
/// Init HTTP hurl client
///
@ -61,7 +70,13 @@ impl Client {
// Activate cookie storage
// with or without persistence (empty string)
h.cookie_file(options.cookie_input_file.unwrap_or_else(|| "".to_string()).as_str()).unwrap();
h.cookie_file(
options
.cookie_input_file
.unwrap_or_else(|| "".to_string())
.as_str(),
)
.unwrap();
if let Some(proxy) = options.proxy {
h.proxy(proxy.as_str()).unwrap();
@ -93,7 +108,11 @@ impl Client {
///
/// Execute an http request
///
pub fn execute(&mut self, request: &Request, redirect_count: usize) -> Result<Response, HttpError> {
pub fn execute(
&mut self,
request: &Request,
redirect_count: usize,
) -> Result<Response, HttpError> {
self.set_url(&request.url, &request.querystring);
self.set_method(&request.method);
@ -101,16 +120,13 @@ impl Client {
self.set_form(&request.form);
self.set_multipart(&request.multipart);
let b = request.body.clone();
let mut data: &[u8] = b.as_ref();
self.set_body(data);
self.set_headers(&request);
self.handle.debug_function(|info_type, data|
match info_type {
self.handle
.debug_function(|info_type, data| match info_type {
// return all request headers (not one by one)
easy::InfoType::HeaderOut => {
let lines = split_lines(data);
@ -124,30 +140,34 @@ impl Client {
}
}
_ => {}
}
).unwrap();
})
.unwrap();
let mut lines = vec![];
let mut body = Vec::<u8>::new();
{
let mut transfer = self.handle.transfer();
if !data.is_empty() {
transfer.read_function(|buf| {
Ok(data.read(buf).unwrap_or(0))
}).unwrap();
transfer
.read_function(|buf| Ok(data.read(buf).unwrap_or(0)))
.unwrap();
}
transfer.header_function(|h| {
if let Some(s) = decode_header(h) {
lines.push(s)
}
true
}).unwrap();
transfer
.header_function(|h| {
if let Some(s) = decode_header(h) {
lines.push(s)
}
true
})
.unwrap();
transfer.write_function(|data| {
body.extend(data);
Ok(data.len())
}).unwrap();
transfer
.write_function(|data| {
body.extend(data);
Ok(data.len())
})
.unwrap();
if let Err(e) = transfer.perform() {
match e.code() {
@ -161,11 +181,10 @@ impl Client {
}
let status = self.handle.response_code().unwrap();
let first_line = lines.remove(0); // remove the status line
let first_line = lines.remove(0); // remove the status line
let version = self.parse_response_version(first_line)?;
let headers = self.parse_response_headers(&mut lines);
if let Some(url) = self.get_follow_location(headers.clone()) {
let request = Request {
method: Method::Get,
@ -219,7 +238,6 @@ impl Client {
self.handle.url(url.as_str()).unwrap();
}
///
/// set method
///
@ -237,7 +255,6 @@ impl Client {
}
}
///
/// set request headers
///
@ -245,29 +262,31 @@ impl Client {
let mut list = easy::List::new();
for header in request.headers.clone() {
list.append(format!("{}: {}", header.name, header.value).as_str()).unwrap();
list.append(format!("{}: {}", header.name, header.value).as_str())
.unwrap();
}
if get_header_values(request.headers.clone(), "Content-Type".to_string()).is_empty() {
if let Some(s) = request.content_type.clone() {
list.append(format!("Content-Type: {}", s).as_str()).unwrap();
list.append(format!("Content-Type: {}", s).as_str())
.unwrap();
} else {
list.append("Content-Type:").unwrap(); // remove header Content-Type
list.append("Content-Type:").unwrap(); // remove header Content-Type
}
}
// if request.form.is_empty() && request.multipart.is_empty() && request.body.is_empty() {
// list.append("Content-Length:").unwrap();
// }
// if request.form.is_empty() && request.multipart.is_empty() && request.body.is_empty() {
// list.append("Content-Length:").unwrap();
// }
if get_header_values(request.headers.clone(), "User-Agent".to_string()).is_empty() {
list.append(format!("User-Agent: hurl/{}", clap::crate_version!()).as_str()).unwrap();
list.append(format!("User-Agent: hurl/{}", clap::crate_version!()).as_str())
.unwrap();
}
self.handle.http_headers(list).unwrap();
}
///
/// set request cookies
///
@ -277,7 +296,6 @@ impl Client {
//}
}
///
/// set form
///
@ -289,7 +307,6 @@ impl Client {
}
}
///
/// set form
///
@ -299,18 +316,19 @@ impl Client {
for param in params {
match param {
MultipartParam::Param(Param { name, value }) => {
form.part(name)
.contents(value.as_bytes())
.add()
.unwrap()
}
MultipartParam::FileParam(FileParam { name, filename, data, content_type }) => {
form.part(name)
.buffer(filename, data.clone())
.content_type(content_type)
.add()
.unwrap()
form.part(name).contents(value.as_bytes()).add().unwrap()
}
MultipartParam::FileParam(FileParam {
name,
filename,
data,
content_type,
}) => form
.part(name)
.buffer(filename, data.clone())
.content_type(content_type)
.add()
.unwrap(),
}
}
self.handle.httppost(form).unwrap();
@ -341,7 +359,6 @@ impl Client {
.join("&")
}
///
/// parse response version
///
@ -357,13 +374,12 @@ impl Client {
}
}
///
/// parse headers from libcurl responses
///
fn parse_response_headers(&mut self, lines: &mut Vec<String>) -> Vec<Header> {
let mut headers: Vec<Header> = vec![];
lines.pop(); // remove the blank line between headers and body
lines.pop(); // remove the blank line between headers and body
for line in lines {
if let Some(header) = Header::parse(line.to_string()) {
headers.push(header);
@ -372,7 +388,6 @@ impl Client {
headers
}
///
/// retrieve an optional location to follow
/// You need:
@ -402,7 +417,6 @@ impl Client {
}
}
///
/// get cookie storage
///
@ -420,7 +434,15 @@ impl Client {
let expires = fields.get(4).unwrap().to_string();
let name = fields.get(5).unwrap().to_string();
let value = fields.get(6).unwrap().to_string();
cookies.push(Cookie { domain, include_subdomain, path, https, expires, name, value });
cookies.push(Cookie {
domain,
include_subdomain,
path,
https,
expires,
name,
value,
});
}
cookies
}
@ -433,11 +455,12 @@ impl Client {
eprintln!("* add to cookie store: {}", cookie);
//self.handle.cookie_list(format!("Set-Cookie: {}={}", cookie.name, cookie.value).as_str()).unwrap();
}
self.handle.cookie_list(cookie.to_string().as_str()).unwrap();
self.handle
.cookie_list(cookie.to_string().as_str())
.unwrap();
}
}
impl Header {
///
/// Parse an http header line received from the server
@ -457,7 +480,6 @@ impl Header {
}
}
///
/// Split an array of bytes into http lines (\r\n separator)
///
@ -479,42 +501,42 @@ fn split_lines(data: &[u8]) -> Vec<String> {
lines
}
///
/// Decode optionally header value as text with utf8 or iso-8859-1 encoding
///
pub fn decode_header(data: &[u8]) -> Option<String> {
match str::from_utf8(data) {
Ok(s) => Some(s.to_string()),
Err(_) => {
match ISO_8859_1.decode(data, DecoderTrap::Strict) {
Ok(s) => Some(s),
Err(_) => {
println!("Error decoding header both utf8 and iso-8859-1 {:?}", data);
None
}
Err(_) => match ISO_8859_1.decode(data, DecoderTrap::Strict) {
Ok(s) => Some(s),
Err(_) => {
println!("Error decoding header both utf8 and iso-8859-1 {:?}", data);
None
}
}
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_header() {
assert_eq!(Header::parse("Foo: Bar\r\n".to_string()).unwrap(),
Header {
name: "Foo".to_string(),
value: "Bar".to_string(),
});
assert_eq!(Header::parse("Location: http://localhost:8000/redirected\r\n".to_string()).unwrap(),
Header {
name: "Location".to_string(),
value: "http://localhost:8000/redirected".to_string(),
});
assert_eq!(
Header::parse("Foo: Bar\r\n".to_string()).unwrap(),
Header {
name: "Foo".to_string(),
value: "Bar".to_string(),
}
);
assert_eq!(
Header::parse("Location: http://localhost:8000/redirected\r\n".to_string()).unwrap(),
Header {
name: "Location".to_string(),
value: "http://localhost:8000/redirected".to_string(),
}
);
assert!(Header::parse("Foo".to_string()).is_none());
}
@ -527,4 +549,4 @@ mod tests {
assert_eq!(lines.get(1).unwrap().as_str(), "Host: localhost:8000");
assert_eq!(lines.get(2).unwrap().as_str(), "");
}
}
}

74
src/http/core.rs Normal file
View File

@ -0,0 +1,74 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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 core::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Header {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cookie {
pub domain: String,
pub include_subdomain: String,
pub path: String,
pub https: String,
pub expires: String,
pub name: String,
pub value: String,
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
impl fmt::Display for Cookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}\t{}\t{}\t{}\t{}\t{}\t{}",
self.domain,
self.include_subdomain,
self.path,
self.https,
self.expires,
self.name,
self.value
)
}
}
///
/// return a list of headers values for the given header name
///
pub fn get_header_values(headers: Vec<Header>, expected_name: String) -> Vec<String> {
headers
.iter()
.filter_map(|Header { name, value }| {
if name.clone() == expected_name {
Some(value.to_string())
} else {
None
}
})
.collect()
}

View File

@ -1,423 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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 core::fmt;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
pub method: Method,
pub url: String,
pub headers: Vec<Header>,
pub querystring: Vec<Param>,
pub form: Vec<Param>,
pub multipart: Vec<MultipartParam>,
pub cookies: Vec<RequestCookie>,
pub body: Vec<u8>,
pub content_type: Option<String>,
}
//impl Request {
//
// ///
// /// Get implicit content-type from request
// /// Note that for multipart, the content-type is not returned because it is generated at runtime by the client
// ///
// pub fn content_type(&self) -> Option<String> {
// if self.form.is_empty() {
// Some("application/x-www-form-urlencoded".to_string())
// // } else if self..mform.is_empty() {
// // Some("application/x-www-form-urlencoded".to_string())
// } else {
// None
// }
// }
//}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Response {
pub version: Version,
pub status: u32,
pub headers: Vec<Header>,
pub body: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH"
};
write!(f, "{}", value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Version {
Http10,
Http11,
Http2,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match self {
Version::Http10 => "1.0",
Version::Http11 => "1.1",
Version::Http2 => "2",
};
write!(f, "{}", value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Header {
pub name: String,
pub value: String,
}
impl fmt::Display for Header {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Param {
pub name: String,
pub value: String,
}
impl fmt::Display for Param {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultipartParam {
Param(Param),
FileParam(FileParam),
}
impl fmt::Display for MultipartParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MultipartParam::Param(param) => write!(f, "{}", param.to_string()),
MultipartParam::FileParam(param) => write!(f, "{}", param.to_string()),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileParam {
pub name: String,
pub filename: String,
pub data: Vec<u8>,
pub content_type: String,
}
impl fmt::Display for FileParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: file,{}; {}", self.name, self.filename, self.content_type)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestCookie {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cookie {
pub domain: String,
pub include_subdomain: String,
pub path: String,
pub https: String,
pub expires: String,
pub name: String,
pub value: String,
}
impl fmt::Display for Cookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}\t{}\t{}\t{}\t{}\t{}\t{}", self.domain, self.include_subdomain, self.path, self.https, self.expires, self.name, self.value)
}
}
impl fmt::Display for RequestCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}", self.name, self.value)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HttpError {
CouldNotResolveProxyName,
CouldNotResolveHost,
FailToConnect,
TooManyRedirect,
CouldNotParseResponse,
SSLCertificate,
}
impl Response {
///
/// return a list of headers values for the given header name
///
pub fn get_header_values(&self, expected_name: String) -> Vec<String> {
get_header_values(self.headers.clone(), expected_name)
}
}
///
/// return a list of headers values for the given header name
///
pub fn get_header_values(headers: Vec<Header>, expected_name: String) -> Vec<String> {
headers
.iter()
.filter_map(|Header { name, value }| if name.clone() == expected_name { Some(value.to_string()) } else { None })
.collect()
}
#[cfg(test)]
pub mod tests {
use super::*;
#[test]
fn get_header_values() {
let response = Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: "Content-Length".to_string(), value: "12".to_string() }
],
body: vec![],
};
assert_eq!(response.get_header_values("Content-Length".to_string()), vec!["12".to_string()]);
assert!(response.get_header_values("Unknown".to_string()).is_empty());
}
pub fn hello_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost:8000/hello".to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn custom_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost/custom".to_string(),
querystring: vec![],
headers: vec![
Header { name: String::from("User-Agent"), value: String::from("iPhone") },
Header { name: String::from("Foo"), value: String::from("Bar") },
],
cookies: vec![
RequestCookie {
name: String::from("theme"),
value: String::from("light"),
},
RequestCookie {
name: String::from("sessionToken"),
value: String::from("abc123"),
}
],
body: vec![],
multipart: vec![],
form: vec![],
content_type: None,
}
}
// GET http://localhost:8000/querystring-params?param1=value1&param2
pub fn query_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost:8000/querystring-params".to_string(),
querystring: vec![
Param { name: String::from("param1"), value: String::from("value1") },
Param { name: String::from("param2"), value: String::from("a b") },
],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn form_http_request() -> Request {
Request {
method: Method::Post,
url: "http://localhost/form-params".to_string(),
querystring: vec![],
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("application/x-www-form-urlencoded") },
],
cookies: vec![],
body: "param1=value1&param2=&param3=a%3db&param4=a%253db".to_string().into_bytes(),
multipart: vec![],
form: vec![],
content_type: Some("multipart/form-data".to_string()),
}
}
pub fn hello_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(String::from("Hello World!")),
}
}
pub fn html_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
],
body: String::into_bytes(String::from("<html><head><meta charset=\"UTF-8\"></head><body><br></body></html>")),
}
}
pub fn xml_invalid_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
xxx
"#.to_string()),
}
}
pub fn xml_two_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
</users>
"#.to_string()),
}
}
pub fn xml_three_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("text/html; charset=utf-8") },
Header { name: String::from("Content-Length"), value: String::from("12") },
],
body: String::into_bytes(r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
<user id="3">Bruce</user>
</users>
"#.to_string()),
}
}
pub fn json_http_response() -> Response {
Response {
version: Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(r#"
{
"success":false,
"errors": [
{ "id": "error1"},
{"id": "error2"}
],
"duration": 1.5
}
"#.to_string()),
}
}
pub fn bytes_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header { name: String::from("Content-Type"), value: String::from("application/octet-stream") },
Header { name: String::from("Content-Length"), value: String::from("1") },
],
body: vec![255],
}
}
}

View File

@ -1,20 +0,0 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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.
*
*/
pub mod client;
pub mod core;

View File

@ -16,4 +16,16 @@
*
*/
pub mod libcurl;
pub use self::client::{Client, ClientOptions, HttpError};
pub use self::core::{Cookie, Header};
#[cfg(test)]
pub use self::request::tests::*;
pub use self::request::{FileParam, Method, MultipartParam, Param, Request, RequestCookie};
#[cfg(test)]
pub use self::response::tests::*;
pub use self::response::{Response, Version};
mod client;
mod core;
mod request;
mod response;

214
src/http/request.rs Normal file
View File

@ -0,0 +1,214 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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 core::fmt;
use super::core::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Request {
pub method: Method,
pub url: String,
pub headers: Vec<Header>,
pub querystring: Vec<Param>,
pub form: Vec<Param>,
pub multipart: Vec<MultipartParam>,
pub cookies: Vec<RequestCookie>,
pub body: Vec<u8>,
pub content_type: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Method {
Get,
Head,
Post,
Put,
Delete,
Connect,
Options,
Trace,
Patch,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Param {
pub name: String,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MultipartParam {
Param(Param),
FileParam(FileParam),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FileParam {
pub name: String,
pub filename: String,
pub data: Vec<u8>,
pub content_type: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RequestCookie {
pub name: String,
pub value: String,
}
impl fmt::Display for Method {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match self {
Method::Get => "GET",
Method::Head => "HEAD",
Method::Post => "POST",
Method::Put => "PUT",
Method::Delete => "DELETE",
Method::Connect => "CONNECT",
Method::Options => "OPTIONS",
Method::Trace => "TRACE",
Method::Patch => "PATCH",
};
write!(f, "{}", value)
}
}
impl fmt::Display for Param {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.name, self.value)
}
}
impl fmt::Display for MultipartParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MultipartParam::Param(param) => write!(f, "{}", param.to_string()),
MultipartParam::FileParam(param) => write!(f, "{}", param.to_string()),
}
}
}
impl fmt::Display for FileParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}: file,{}; {}",
self.name, self.filename, self.content_type
)
}
}
impl fmt::Display for RequestCookie {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}", self.name, self.value)
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn hello_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost:8000/hello".to_string(),
querystring: vec![],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn custom_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost/custom".to_string(),
querystring: vec![],
headers: vec![
Header {
name: String::from("User-Agent"),
value: String::from("iPhone"),
},
Header {
name: String::from("Foo"),
value: String::from("Bar"),
},
],
cookies: vec![
RequestCookie {
name: String::from("theme"),
value: String::from("light"),
},
RequestCookie {
name: String::from("sessionToken"),
value: String::from("abc123"),
},
],
body: vec![],
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn query_http_request() -> Request {
Request {
method: Method::Get,
url: "http://localhost:8000/querystring-params".to_string(),
querystring: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
headers: vec![],
cookies: vec![],
body: vec![],
multipart: vec![],
form: vec![],
content_type: None,
}
}
pub fn form_http_request() -> Request {
Request {
method: Method::Post,
url: "http://localhost/form-params".to_string(),
querystring: vec![],
headers: vec![Header {
name: String::from("Content-Type"),
value: String::from("application/x-www-form-urlencoded"),
}],
cookies: vec![],
body: "param1=value1&param2=&param3=a%3db&param4=a%253db"
.to_string()
.into_bytes(),
multipart: vec![],
form: vec![],
content_type: Some("multipart/form-data".to_string()),
}
}
}

251
src/http/response.rs Normal file
View File

@ -0,0 +1,251 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 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 core::fmt;
use super::core::*;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Response {
pub version: Version,
pub status: u32,
pub headers: Vec<Header>,
pub body: Vec<u8>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Version {
Http10,
Http11,
Http2,
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let value = match self {
Version::Http10 => "1.0",
Version::Http11 => "1.1",
Version::Http2 => "2",
};
write!(f, "{}", value)
}
}
impl Response {
///
/// return a list of headers values for the given header name
///
pub fn get_header_values(&self, expected_name: String) -> Vec<String> {
get_header_values(self.headers.clone(), expected_name)
}
///
///
///
pub fn get_header(&self, name: String) -> Vec<String> {
self.headers
.iter()
.filter(|&h| h.name.to_lowercase() == name.to_lowercase())
.map(|h| h.value.clone())
.collect()
}
///
/// Return optional Content-type header value
///
pub fn content_type(&self) -> Option<String> {
for header in self.headers.clone() {
if header.name.to_lowercase().as_str() == "content-type" {
return Some(header.value);
}
}
None
}
}
#[cfg(test)]
pub mod tests {
use super::*;
pub fn hello_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header {
name: String::from("Content-Type"),
value: String::from("text/html; charset=utf-8"),
},
Header {
name: String::from("Content-Length"),
value: String::from("12"),
},
],
body: String::into_bytes(String::from("Hello World!")),
}
}
pub fn html_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![Header {
name: String::from("Content-Type"),
value: String::from("text/html; charset=utf-8"),
}],
body: String::into_bytes(String::from(
"<html><head><meta charset=\"UTF-8\"></head><body><br></body></html>",
)),
}
}
pub fn xml_invalid_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header {
name: String::from("Content-Type"),
value: String::from("text/html; charset=utf-8"),
},
Header {
name: String::from("Content-Length"),
value: String::from("12"),
},
],
body: String::into_bytes(
r#"
xxx
"#
.to_string(),
),
}
}
pub fn xml_two_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header {
name: String::from("Content-Type"),
value: String::from("text/html; charset=utf-8"),
},
Header {
name: String::from("Content-Length"),
value: String::from("12"),
},
],
body: String::into_bytes(
r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
</users>
"#
.to_string(),
),
}
}
pub fn xml_three_users_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header {
name: String::from("Content-Type"),
value: String::from("text/html; charset=utf-8"),
},
Header {
name: String::from("Content-Length"),
value: String::from("12"),
},
],
body: String::into_bytes(
r#"
<?xml version="1.0"?>
<users>
<user id="1">Bob</user>
<user id="2">Bill</user>
<user id="3">Bruce</user>
</users>
"#
.to_string(),
),
}
}
pub fn json_http_response() -> Response {
Response {
version: Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(
r#"
{
"success":false,
"errors": [
{ "id": "error1"},
{"id": "error2"}
],
"duration": 1.5
}
"#
.to_string(),
),
}
}
pub fn bytes_http_response() -> Response {
Response {
version: Version::Http10,
status: 200,
headers: vec![
Header {
name: String::from("Content-Type"),
value: String::from("application/octet-stream"),
},
Header {
name: String::from("Content-Length"),
value: String::from("1"),
},
],
body: vec![255],
}
}
#[test]
fn get_header_values() {
let response = Response {
version: Version::Http10,
status: 200,
headers: vec![Header {
name: "Content-Length".to_string(),
value: "12".to_string(),
}],
body: vec![],
};
assert_eq!(
response.get_header_values("Content-Length".to_string()),
vec!["12".to_string()]
);
assert!(response.get_header_values("Unknown".to_string()).is_empty());
}
}

View File

@ -19,7 +19,7 @@
use std::collections::HashMap;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use super::core::{Error, RunnerError};
use super::core::*;
@ -107,7 +107,7 @@ impl AssertResult {
}
impl Assert {
pub fn eval(self, http_response: libcurl::core::Response, variables: &HashMap<String, Value>) -> AssertResult {
pub fn eval(self, http_response: http::Response, variables: &HashMap<String, Value>) -> AssertResult {
let actual = self.query.eval(variables, http_response);
let source_info = self.predicate.clone().predicate_func.source_info;
let predicate_result = match actual.clone() {
@ -162,7 +162,7 @@ pub mod tests {
fn test_eval() {
let variables = HashMap::new();
assert_eq!(
assert_count_user().eval(libcurl::core::tests::xml_three_users_http_response(), &variables),
assert_count_user().eval(http::xml_three_users_http_response(), &variables),
AssertResult::Explicit {
actual: Ok(Some(Value::Nodeset(3))),
source_info: SourceInfo::init(1, 14, 1, 27),

View File

@ -20,7 +20,7 @@ use std::collections::HashMap;
use regex::Regex;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use super::core::{CaptureResult, Error};
use super::core::RunnerError;
@ -28,7 +28,7 @@ use super::super::core::ast::*;
impl Capture {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: libcurl::core::Response) -> Result<CaptureResult, Error> {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: http::Response) -> Result<CaptureResult, Error> {
let name = self.name.value;
let value = self.query.clone().eval(variables, http_response)?;
let value = match value {
@ -184,7 +184,7 @@ pub mod tests {
},
};
let error = capture.eval(&variables, libcurl::core::tests::xml_three_users_http_response()).err().unwrap();
let error = capture.eval(&variables, http::xml_three_users_http_response()).err().unwrap();
assert_eq!(error.source_info.start, Pos { line: 1, column: 7 });
assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval)
}
@ -239,13 +239,13 @@ pub mod tests {
#[test]
fn test_capture() {
let variables = HashMap::new();
assert_eq!(user_count_capture().eval(&variables, libcurl::core::tests::xml_three_users_http_response()).unwrap(),
assert_eq!(user_count_capture().eval(&variables, http::xml_three_users_http_response()).unwrap(),
CaptureResult {
name: "UserCount".to_string(),
value: Value::from_f64(3.0),
});
assert_eq!(duration_capture().eval(&variables, libcurl::core::tests::json_http_response()).unwrap(),
assert_eq!(duration_capture().eval(&variables, http::json_http_response()).unwrap(),
CaptureResult {
name: "duration".to_string(),
value: Value::from_f64(1.5),

View File

@ -25,17 +25,6 @@
/// and not by the http client.
///
use crate::http::libcurl::core::Response;
impl Response {
pub fn cookies(&self) -> Vec<ResponseCookie> {
self.headers
.iter()
.filter(|&h| h.name.to_lowercase() == "set-cookie")
.filter_map(|h| ResponseCookie::parse(h.value.clone()))
.collect()
}
}
///
/// Cookie return from HTTP Response

View File

@ -19,7 +19,7 @@ use std::collections::HashMap;
use crate::core::common::{FormatError, SourceInfo, Value};
use crate::http::libcurl;
use crate::http;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunnerOptions {
@ -35,7 +35,7 @@ pub struct HurlResult {
pub entries: Vec<EntryResult>,
pub time_in_ms: u128,
pub success: bool,
pub cookies: Vec<libcurl::core::Cookie>,
pub cookies: Vec<http::Cookie>,
}
impl HurlResult {
@ -50,8 +50,8 @@ impl HurlResult {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EntryResult {
pub request: Option<libcurl::core::Request>,
pub response: Option<libcurl::core::Response>,
pub request: Option<http::Request>,
pub response: Option<http::Response>,
//pub captures: Vec<(String, Value)>,
pub captures: Vec<CaptureResult>,
pub asserts: Vec<AssertResult>,

View File

@ -22,7 +22,7 @@ use std::time::Instant;
use crate::core::ast::*;
use crate::core::common::SourceInfo;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use super::core::*;
@ -49,7 +49,7 @@ use crate::format::logger::Logger;
//// });
/// ```
pub fn run(entry: Entry,
http_client: &mut libcurl::client::Client,
http_client: &mut http::Client,
entry_index: usize,
variables: &mut HashMap<String, Value>,
context_dir: String,
@ -80,7 +80,7 @@ pub fn run(entry: Entry,
use url::Url;
if let Ok(url) = Url::parse(http_request.url.as_str()) {
for c in http_request.cookies.clone() {
let cookie = libcurl::core::Cookie {
let cookie = http::Cookie {
domain: url.host_str().unwrap().to_string(),
include_subdomain: "FALSE".to_string(),
path: "/".to_string(),
@ -182,7 +182,7 @@ pub fn run(entry: Entry,
}
pub fn log_request(logger: &Logger, request: &libcurl::core::Request) {
pub fn log_request(logger: &Logger, request: &http::Request) {
logger.verbose("Request");
logger.verbose(format!("{} {}", request.method, request.url).as_str());
for header in request.headers.clone() {

View File

@ -20,7 +20,7 @@ use std::time::Instant;
use crate::core::ast::*;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use super::core::*;
@ -35,7 +35,7 @@ use crate::core::common::FormatError;
/// # Example
///
/// ```
/// use hurl::http::libcurl;
/// use hurl::http;
/// use hurl::runner;
/// use hurl::format;
///
@ -48,7 +48,7 @@ use crate::core::common::FormatError;
/// let hurl_file = hurl::parser::parse_hurl_file(s).unwrap();
///
/// // Create an http client
/// let options = libcurl::client::ClientOptions {
/// let options = http::ClientOptions {
/// follow_location: false,
/// max_redirect: None,
/// cookie_input_file: None,
@ -57,7 +57,7 @@ use crate::core::common::FormatError;
/// verbose: false,
/// insecure: false,
/// };
/// let mut client = libcurl::client::Client::init(options);
/// let mut client = http::Client::init(options);
///
/// // Define runner options
/// let variables = std::collections::HashMap::new();
@ -92,7 +92,7 @@ use crate::core::common::FormatError;
/// ```
pub fn run(
hurl_file: HurlFile,
http_client: &mut libcurl::client::Client,
http_client: &mut http::Client,
filename: String,
context_dir: String,
options: RunnerOptions,

View File

@ -16,33 +16,25 @@
*
*/
use crate::http::libcurl::core::Response;
use crate::http::Response;
use super::core::RunnerError;
use super::cookie::ResponseCookie;
use encoding::{EncodingRef, DecoderTrap};
///
/// get body content as text from http response
/// used by query
///
#[allow(dead_code)]
impl Response {
///
/// Return optional Content-type header value
///
fn content_type(&self) -> Option<String> {
for header in self.headers.clone() {
if header.name.to_lowercase() == "content-type" {
return Some(header.value);
}
}
None
pub fn cookies(&self) -> Vec<ResponseCookie> {
self.headers
.iter()
.filter(|&h| h.name.to_lowercase().as_str() == "set-cookie")
.filter_map(|h| ResponseCookie::parse(h.value.clone()))
.collect()
}
///
/// Return encoding of the response
///
@ -61,6 +53,9 @@ impl Response {
}
}
///
/// return response body as text
///
pub fn text(&self) -> Result<String, RunnerError> {
let encoding = self.encoding()?;
match encoding.decode(&self.body, DecoderTrap::Strict) {
@ -68,6 +63,30 @@ impl Response {
Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.name().to_string() })
}
}
///
/// return true if response is an html response
///
pub fn is_html(&self) -> bool {
match self.content_type() {
None => false,
Some(s) => s.starts_with("text/html")
}
}
///
/// Return option cookie from response
///
pub fn get_cookie(&self, name: String) -> Option<ResponseCookie> {
for cookie in self.cookies() {
if cookie.name == name {
return Some(cookie);
}
}
None
}
}
///
@ -86,7 +105,7 @@ fn mime_charset(mime_type: String) -> Option<String> {
#[cfg(test)]
pub mod tests {
use super::*;
use crate::http::libcurl::core::{Version, Header};
use crate::http::*;
#[test]
pub fn test_charset() {

View File

@ -17,8 +17,7 @@
*/
use crate::http::libcurl::core::*;
use crate::http::*;
use super::cookie::*;
use super::core::*;
@ -350,8 +349,6 @@ fn parse_version(s: String) -> Result<Version, ParseError> {
#[cfg(test)]
mod tests {
use crate::http::libcurl::core::tests::*;
use super::*;
#[test]

View File

@ -19,7 +19,8 @@
use serde::ser::{Serializer, SerializeStruct};
use serde::Serialize;
use crate::http::libcurl::core::*;
use crate::http::*;
use super::cookie::*;
use super::core::*;

View File

@ -28,7 +28,7 @@ use std::path::Path;
use crate::core::ast::*;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use super::core::{Error, RunnerError};
@ -38,23 +38,23 @@ impl MultipartParam {
pub fn eval(self,
variables: &HashMap<String, Value>,
context_dir: String,
) -> Result<libcurl::core::MultipartParam, Error> {
) -> Result<http::MultipartParam, Error> {
match self {
MultipartParam::Param(KeyValue { key, value, .. }) => {
let name = key.value;
let value = value.eval(variables)?;
Ok(libcurl::core::MultipartParam::Param(libcurl::core::Param { name, value }))
Ok(http::MultipartParam::Param(http::Param { name, value }))
}
MultipartParam::FileParam(param) => {
let file_param = param.eval(context_dir)?;
Ok(libcurl::core::MultipartParam::FileParam(file_param))
Ok(http::MultipartParam::FileParam(file_param))
}
}
}
}
impl FileParam {
pub fn eval(self, context_dir: String) -> Result<libcurl::core::FileParam, Error> {
pub fn eval(self, context_dir: String) -> Result<http::FileParam, Error> {
let name = self.key.value;
let filename = self.value.filename.clone();
@ -93,7 +93,7 @@ impl FileParam {
}
let content_type = self.value.content_type();
Ok(libcurl::core::FileParam {
Ok(http::FileParam {
name,
filename: filename.value,
data,
@ -166,7 +166,7 @@ mod tests {
},
line_terminator0: line_terminator,
}.eval("integration/tests".to_string()).unwrap(),
libcurl::core::FileParam {
http::FileParam {
name: "upload1".to_string(),
filename: "hello.txt".to_string(),
data: b"Hello World!".to_vec(),

View File

@ -21,7 +21,7 @@ use std::collections::HashMap;
use regex::Regex;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use crate::jsonpath;
use super::cookie;
@ -32,29 +32,8 @@ use super::xpath;
pub type QueryResult = Result<Option<Value>, Error>;
impl libcurl::core::Response {
pub fn is_html(&self) -> bool {
for header in self.headers.clone() {
if header.name.to_lowercase() == "content-type" {
return header.value.contains("html");
}
}
false
}
pub fn get_cookie(&self, name: String) -> Option<cookie::ResponseCookie> {
for cookie in self.cookies() {
if cookie.name == name {
return Some(cookie);
}
}
None
}
}
impl Query {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: libcurl::core::Response) -> QueryResult {
pub fn eval(self, variables: &HashMap<String, Value>, http_response: http::Response) -> QueryResult {
match self.value {
QueryValue::Status {} => Ok(Some(Value::Integer(i64::from(http_response.status)))),
QueryValue::Header { name, .. } => {
@ -224,7 +203,6 @@ impl CookieAttributeName {
#[cfg(test)]
pub mod tests {
use crate::core::common::{Pos, SourceInfo};
use super::*;
pub fn xpath_invalid_query() -> Query {
@ -297,9 +275,9 @@ pub mod tests {
}
}
pub fn json_http_response() -> libcurl::core::Response {
libcurl::core::Response {
version: libcurl::core::Version::Http10,
pub fn json_http_response() -> http::Response {
http::Response {
version: http::Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(r#"
@ -436,7 +414,7 @@ pub mod tests {
fn test_query_status() {
let variables = HashMap::new();
assert_eq!(
Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Status {} }.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
Query { source_info: SourceInfo::init(0, 0, 0, 0), value: QueryValue::Status {} }.eval(&variables, http::hello_http_response()).unwrap().unwrap(),
Value::Integer(200)
);
}
@ -464,7 +442,7 @@ pub mod tests {
// let error = query_header.eval(http::hello_http_response()).err().unwrap();
// assert_eq!(error.source_info.start, Pos { line: 1, column: 8 });
// assert_eq!(error.inner, RunnerError::QueryHeaderNotFound);
assert_eq!(query_header.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap(), None);
assert_eq!(query_header.eval(&variables, http::hello_http_response()).unwrap(), None);
}
#[test]
@ -488,7 +466,7 @@ pub mod tests {
},
};
assert_eq!(
query_header.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
query_header.eval(&variables, http::hello_http_response()).unwrap().unwrap(),
Value::String(String::from("text/html; charset=utf-8"))
);
}
@ -500,11 +478,11 @@ pub mod tests {
value: String::from(""),
source_info: SourceInfo::init(0, 0, 0, 0),
};
let response = libcurl::core::Response {
version: libcurl::core::Version::Http10,
let response = http::Response {
version: http::Version::Http10,
status: 0,
headers: vec![
libcurl::core::Header {
http::Header {
name: "Set-Cookie".to_string(),
value: "LSID=DQAAAKEaem_vYg; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly".to_string(),
}
@ -649,22 +627,22 @@ pub mod tests {
Query {
source_info: SourceInfo::init(0, 0, 0, 0),
value: QueryValue::Body {},
}.eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
}.eval(&variables, http::hello_http_response()).unwrap().unwrap(),
Value::String(String::from("Hello World!"))
);
let error = Query {
source_info: SourceInfo::init(1, 1, 1, 2),
value: QueryValue::Body {},
}.eval(&variables, libcurl::core::tests::bytes_http_response()).err().unwrap();
}.eval(&variables, http::bytes_http_response()).err().unwrap();
assert_eq!(error.source_info, SourceInfo::init(1, 1, 1, 2));
assert_eq!(error.inner, RunnerError::InvalidDecoding { charset: "utf-8".to_string()});
assert_eq!(error.inner, RunnerError::InvalidDecoding { charset: "utf-8".to_string() });
}
#[test]
fn test_query_invalid_utf8() {
let variables = HashMap::new();
let http_response = libcurl::core::Response {
version: libcurl::core::Version::Http10,
let http_response = http::Response {
version: http::Version::Http10,
status: 0,
headers: vec![],
body: vec![200],
@ -697,7 +675,7 @@ pub mod tests {
},
},
};
let error = query.eval(&variables, libcurl::core::tests::xml_two_users_http_response()).err().unwrap();
let error = query.eval(&variables, http::xml_two_users_http_response()).err().unwrap();
assert_eq!(error.inner, RunnerError::QueryInvalidXpathEval);
assert_eq!(error.source_info.start, Pos { line: 1, column: 7 });
}
@ -706,8 +684,8 @@ pub mod tests {
fn test_query_xpath() {
let variables = HashMap::new();
assert_eq!(xpath_users().eval(&variables, libcurl::core::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Nodeset(2));
assert_eq!(xpath_count_user_query().eval(&variables, libcurl::core::tests::xml_two_users_http_response()).unwrap().unwrap(), Value::Float(2, 0));
assert_eq!(xpath_users().eval(&variables, http::xml_two_users_http_response()).unwrap().unwrap(), Value::Nodeset(2));
assert_eq!(xpath_count_user_query().eval(&variables, http::xml_two_users_http_response()).unwrap().unwrap(), Value::Float(2, 0));
}
#[cfg(test)]
@ -738,7 +716,7 @@ pub mod tests {
#[test]
fn test_query_xpath_with_html() {
let variables = HashMap::new();
assert_eq!(xpath_html_charset().eval(&variables, libcurl::core::tests::html_http_response()).unwrap().unwrap(), Value::String(String::from("UTF-8")));
assert_eq!(xpath_html_charset().eval(&variables, http::html_http_response()).unwrap().unwrap(), Value::String(String::from("UTF-8")));
}
#[test]
@ -775,8 +753,8 @@ pub mod tests {
#[test]
fn test_query_invalid_json() {
let variables = HashMap::new();
let http_response = libcurl::core::Response {
version: libcurl::core::Version::Http10,
let http_response = http::Response {
version: http::Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(String::from("xxx")),
@ -789,8 +767,8 @@ pub mod tests {
#[test]
fn test_query_json_not_found() {
let variables = HashMap::new();
let http_response = libcurl::core::Response {
version: libcurl::core::Version::Http10,
let http_response = http::Response {
version: http::Version::Http10,
status: 0,
headers: vec![],
body: String::into_bytes(String::from("{}")),
@ -819,11 +797,11 @@ pub mod tests {
fn test_query_regex() {
let variables = HashMap::new();
assert_eq!(
regex_name().eval(&variables, libcurl::core::tests::hello_http_response()).unwrap().unwrap(),
regex_name().eval(&variables, http::hello_http_response()).unwrap().unwrap(),
Value::String("World".to_string())
);
let error = regex_invalid().eval(&variables, libcurl::core::tests::hello_http_response()).err().unwrap();
let error = regex_invalid().eval(&variables, http::hello_http_response()).err().unwrap();
assert_eq!(error.source_info, SourceInfo::init(1, 7, 1, 10));
assert_eq!(error.inner, RunnerError::InvalidRegex());
}

View File

@ -25,7 +25,7 @@ use std::io::prelude::*;
use crate::core::ast::*;
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use super::core::Error;
@ -33,38 +33,38 @@ impl Request {
pub fn eval(self,
variables: &HashMap<String, Value>,
context_dir: String,
) -> Result<libcurl::core::Request, Error> {
) -> Result<http::Request, Error> {
let method = self.method.clone().eval();
let url = self.clone().url.eval(&variables)?;
// headers
let mut headers: Vec<libcurl::core::Header> = vec![];
let mut headers: Vec<http::Header> = vec![];
for header in self.clone().headers {
let name = header.key.value;
let value = header.value.eval(variables)?;
headers.push(libcurl::core::Header {
headers.push(http::Header {
name,
value,
});
}
let mut querystring: Vec<libcurl::core::Param> = vec![];
let mut querystring: Vec<http::Param> = vec![];
for param in self.clone().querystring_params() {
let name = param.key.value;
let value = param.value.eval(variables)?;
querystring.push(libcurl::core::Param { name, value });
querystring.push(http::Param { name, value });
}
let mut form: Vec<libcurl::core::Param> = vec![];
let mut form: Vec<http::Param> = vec![];
for param in self.clone().form_params() {
let name = param.key.value;
let value = param.value.eval(variables)?;
form.push(libcurl::core::Param { name, value });
form.push(http::Param { name, value });
}
// if !self.clone().form_params().is_empty() {
// headers.push(libcurl::core::Header {
// headers.push(http::core::Header {
// name: String::from("Content-Type"),
// value: String::from("application/x-www-form-urlencoded"),
// });
@ -72,7 +72,7 @@ impl Request {
let mut cookies = vec![];
for cookie in self.clone().cookies() {
let cookie = libcurl::core::RequestCookie {
let cookie = http::RequestCookie {
name: cookie.clone().name.value,
value: cookie.clone().value.value,
};
@ -104,7 +104,7 @@ impl Request {
// if self.content_type().is_none() {
// if let Some(body) = self.body {
// if let Bytes::Json { .. } = body.value {
// headers.push(libcurl::core::Header {
// headers.push(http::core::Header {
// name: String::from("Content-Type"),
// value: String::from("application/json"),
// });
@ -112,7 +112,7 @@ impl Request {
// }
// }
Ok(libcurl::core::Request {
Ok(http::Request {
method,
url,
querystring,
@ -136,42 +136,42 @@ impl Request {
}
impl Method {
fn eval(self) -> libcurl::core::Method {
fn eval(self) -> http::Method {
match self {
Method::Get => libcurl::core::Method::Get,
Method::Head => libcurl::core::Method::Head,
Method::Post => libcurl::core::Method::Post,
Method::Put => libcurl::core::Method::Put,
Method::Delete => libcurl::core::Method::Delete,
Method::Connect => libcurl::core::Method::Connect,
Method::Options => libcurl::core::Method::Options,
Method::Trace => libcurl::core::Method::Trace,
Method::Patch => libcurl::core::Method::Patch,
Method::Get => http::Method::Get,
Method::Head => http::Method::Head,
Method::Post => http::Method::Post,
Method::Put => http::Method::Put,
Method::Delete => http::Method::Delete,
Method::Connect => http::Method::Connect,
Method::Options => http::Method::Options,
Method::Trace => http::Method::Trace,
Method::Patch => http::Method::Patch,
}
}
}
pub fn split_url(url: String) -> (String, Vec<libcurl::core::Param>) {
match url.find('?') {
None => (url, vec![]),
Some(index) => {
let (url, params) = url.split_at(index);
let params: Vec<libcurl::core::Param> = params[1..].split('&')
.map(|s| {
match s.find('=') {
None => libcurl::core::Param { name: s.to_string(), value: String::from("") },
Some(index) => {
let (name, value) = s.split_at(index);
libcurl::core::Param { name: name.to_string(), value: value[1..].to_string() }
}
}
})
.collect();
(url.to_string(), params)
}
}
}
//pub fn split_url(url: String) -> (String, Vec<http::Param>) {
// match url.find('?') {
// None => (url, vec![]),
// Some(index) => {
// let (url, params) = url.split_at(index);
// let params: Vec<http::Param> = params[1..].split('&')
// .map(|s| {
// match s.find('=') {
// None => http::Param { name: s.to_string(), value: String::from("") },
// Some(index) => {
// let (name, value) = s.split_at(index);
// http::Param { name: name.to_string(), value: value[1..].to_string() }
// }
// }
// })
// .collect();
//
// (url.to_string(), params)
// }
// }
//}
#[cfg(test)]
@ -333,7 +333,7 @@ mod tests {
let mut variables = HashMap::new();
variables.insert(String::from("base_url"), Value::String(String::from("http://localhost:8000")));
let http_request = hello_request().eval(&variables, "current_dir".to_string()).unwrap();
assert_eq!(http_request, libcurl::core::tests::hello_http_request());
assert_eq!(http_request, http::hello_http_request());
}
#[test]
@ -341,6 +341,6 @@ mod tests {
let mut variables = HashMap::new();
variables.insert(String::from("param1"), Value::String(String::from("value1")));
let http_request = query_request().eval(&variables, "current_dir".to_string()).unwrap();
assert_eq!(http_request, libcurl::core::tests::query_http_request());
assert_eq!(http_request, http::query_http_request());
}
}

View File

@ -20,25 +20,17 @@ use std::collections::HashMap;
use crate::core::common::{Pos, SourceInfo};
use crate::core::common::Value;
use crate::http::libcurl;
use crate::http;
use crate::runner::core::RunnerError;
use super::core::*;
use super::core::Error;
use super::super::core::ast::*;
impl libcurl::core::Response {
pub fn get_header(&self, name: String) -> Vec<String> {
self.headers
.iter()
.filter(|&h| h.name.to_lowercase() == name.to_lowercase())
.map(|h| h.value.clone())
.collect()
}
}
impl Response {
pub fn eval_asserts(self, variables: &HashMap<String, Value>, http_response: libcurl::core::Response, context_dir: String) -> Vec<AssertResult> {
pub fn eval_asserts(self, variables: &HashMap<String, Value>, http_response: http::Response, context_dir: String) -> Vec<AssertResult> {
let mut asserts = vec![];
let version = self.clone().version;
@ -200,7 +192,7 @@ impl Response {
asserts
}
pub fn eval_captures(self, http_response: libcurl::core::Response, variables: &HashMap<String, Value>) -> Result<Vec<CaptureResult>, Error> {
pub fn eval_captures(self, http_response: http::Response, variables: &HashMap<String, Value>) -> Result<Vec<CaptureResult>, Error> {
let mut captures = vec![];
for capture in self.captures() {
let capture_result = capture.eval(variables, http_response.clone())?;
@ -268,7 +260,7 @@ mod tests {
let variables = HashMap::new();
let context_dir = "undefined".to_string();
assert_eq!(
user_response().eval_asserts(&variables, libcurl::core::tests::xml_two_users_http_response(), context_dir),
user_response().eval_asserts(&variables, http::xml_two_users_http_response(), context_dir),
vec![
AssertResult::Version {
actual: String::from("1.0"),
@ -301,7 +293,7 @@ mod tests {
pub fn test_eval_captures() {
let variables = HashMap::new();
assert_eq!(
user_response().eval_captures(libcurl::core::tests::xml_two_users_http_response(), &variables).unwrap(),
user_response().eval_captures(http::xml_two_users_http_response(), &variables).unwrap(),
vec![
CaptureResult {
name: "UserCount".to_string(),

View File

@ -3,9 +3,10 @@ use std::io::prelude::*;
use curl::easy::Easy;
use hurl::http::libcurl;
use hurl::http::libcurl::client::ClientOptions;
use hurl::http::libcurl::core::*;
use hurl::http::*;
use server::Server;
macro_rules! t {
@ -55,7 +56,7 @@ fn get_easy() {
}
fn default_client() -> libcurl::client::Client {
fn default_client() -> Client {
let options = ClientOptions {
follow_location: false,
max_redirect: None,
@ -65,7 +66,7 @@ fn default_client() -> libcurl::client::Client {
verbose: true,
insecure: false,
};
libcurl::client::Client::init(options)
Client::init(options)
}
fn default_get_request(url: String) -> Request {
@ -121,12 +122,10 @@ fn test_put() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
#[test]
fn test_patch() {
let mut client = default_client();
let request = Request {
method: Method::Patch,
@ -146,7 +145,6 @@ fn test_patch() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 204);
assert!(response.body.is_empty());
}
// endregion
@ -176,7 +174,6 @@ fn test_custom_headers() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
// endregion
@ -205,7 +202,6 @@ fn test_querystring_params() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
// endregion
@ -241,7 +237,6 @@ fn test_form_params() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.body, b"Hello World!".to_vec());
}
// endregion
@ -266,9 +261,9 @@ fn test_follow_location() {
proxy: None,
no_proxy: None,
verbose: false,
insecure: false
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = Client::init(options);
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.get_header_values("Content-Length".to_string()).get(0).unwrap(), "0");
@ -295,7 +290,7 @@ fn test_max_redirect() {
verbose: false,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/redirect".to_string());
let response = client.execute(&request, 5).unwrap();
assert_eq!(response.status, 200);
@ -303,7 +298,6 @@ fn test_max_redirect() {
let error = client.execute(&request, 11).err().unwrap();
assert_eq!(error, HttpError::TooManyRedirect);
}
// endregion
@ -312,7 +306,6 @@ fn test_max_redirect() {
#[test]
fn test_multipart_form_data() {
let mut client = default_client();
let request = Request {
method: Method::Post,
@ -321,27 +314,27 @@ fn test_multipart_form_data() {
querystring: vec![],
form: vec![],
multipart: vec![
MultipartParam::Param(Param{
MultipartParam::Param(Param {
name: "key1".to_string(),
value: "value1".to_string()
value: "value1".to_string(),
}),
MultipartParam::FileParam(FileParam{
MultipartParam::FileParam(FileParam {
name: "upload1".to_string(),
filename: "hello.txt".to_string(),
data: b"Hello World!".to_vec(),
content_type: "text/plain".to_string()
content_type: "text/plain".to_string(),
}),
MultipartParam::FileParam(FileParam{
MultipartParam::FileParam(FileParam {
name: "upload2".to_string(),
filename: "hello.html".to_string(),
data: b"Hello <b>World</b>!".to_vec(),
content_type: "text/html".to_string()
content_type: "text/html".to_string(),
}),
MultipartParam::FileParam(FileParam{
MultipartParam::FileParam(FileParam {
name: "upload3".to_string(),
filename: "hello.txt".to_string(),
data: b"Hello World!".to_vec(),
content_type: "text/html".to_string()
content_type: "text/html".to_string(),
}),
],
cookies: vec![],
@ -358,7 +351,6 @@ fn test_multipart_form_data() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert_eq!(response.body, b"Hello World!".to_vec());
}
// endregion
@ -367,7 +359,6 @@ fn test_multipart_form_data() {
#[test]
fn test_post_bytes() {
let mut client = default_client();
let request = Request {
method: Method::Post,
@ -378,12 +369,11 @@ fn test_post_bytes() {
multipart: vec![],
cookies: vec![],
body: b"Hello World!".to_vec(),
content_type: None
content_type: None,
};
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
// endregion
@ -394,7 +384,7 @@ fn test_post_bytes() {
fn test_error_could_not_resolve_host() {
let mut client = default_client();
let request = default_get_request("http://unknown".to_string());
let error = client.execute(&request, 0).err().unwrap();
let error = client.execute(&request, 0).err().unwrap();
assert_eq!(error, HttpError::CouldNotResolveHost);
}
@ -403,7 +393,7 @@ fn test_error_could_not_resolve_host() {
fn test_error_fail_to_connect() {
let mut client = default_client();
let request = default_get_request("http://localhost:9999".to_string());
let error = client.execute(&request, 0).err().unwrap();
let error = client.execute(&request, 0).err().unwrap();
assert_eq!(error, HttpError::FailToConnect);
@ -416,11 +406,10 @@ fn test_error_fail_to_connect() {
verbose: true,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
let error = client.execute(&request, 0).err().unwrap();
let error = client.execute(&request, 0).err().unwrap();
assert_eq!(error, HttpError::FailToConnect);
}
@ -435,7 +424,7 @@ fn test_error_could_not_resolve_proxy_name() {
verbose: false,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
let error = client.execute(&request, 0).err().unwrap();
assert_eq!(error, HttpError::CouldNotResolveProxyName);
@ -486,7 +475,6 @@ fn test_cookie() {
// let response = client.execute(&request, 0).unwrap();
// assert_eq!(response.status, 200);
// assert!(response.body.is_empty());
}
#[test]
@ -512,7 +500,6 @@ fn test_cookie_storage() {
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
@ -531,12 +518,11 @@ fn test_cookie_file() {
verbose: false,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/cookies/assert-that-cookie2-is-valueA".to_string());
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);
assert!(response.body.is_empty());
}
// endregion
@ -555,7 +541,7 @@ fn test_proxy() {
verbose: false,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = Client::init(options);
let request = default_get_request("http://localhost:8000/hello".to_string());
let response = client.execute(&request, 0).unwrap();
assert_eq!(response.status, 200);

View File

@ -21,7 +21,7 @@ use hurl::core::ast;
use hurl::core::common::{Pos, SourceInfo};
use hurl::runner;
use hurl::format;
use hurl::http::libcurl;
use hurl::http;
use std::collections::HashMap;
use hurl::core::ast::{Template, TemplateElement, EncodedString};
use hurl::runner::core::RunnerOptions;
@ -37,7 +37,7 @@ fn test_hurl_file() {
let content = std::fs::read_to_string(filename).expect("Something went wrong reading the file");
let hurl_file = hurl::parser::parse_hurl_file(content.as_str()).unwrap();
let variables = HashMap::new();
let options = libcurl::client::ClientOptions {
let options = http::ClientOptions {
follow_location: false,
max_redirect: None,
cookie_input_file: None,
@ -46,7 +46,7 @@ fn test_hurl_file() {
verbose: false,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = http::Client::init(options);
let mut lines: Vec<&str> = regex::Regex::new(r"\n|\r\n")
.unwrap()
.split(&content)
@ -144,7 +144,7 @@ fn hello_request() -> ast::Request {
#[test]
fn test_hello() {
let options = libcurl::client::ClientOptions {
let options = http::ClientOptions {
follow_location: false,
max_redirect: None,
cookie_input_file: None,
@ -153,7 +153,7 @@ fn test_hello() {
verbose: false,
insecure: false,
};
let mut client = libcurl::client::Client::init(options);
let mut client = http::Client::init(options);
let source_info = SourceInfo {
start: Pos { line: 1, column: 1 },
end: Pos { line: 1, column: 1 },