Release: http-rest-file Parser
Aug 3 2023
I just released version v0.5.0
of a recursive descent parser I wrote in Rust for the .http
and .rest
htp client file format.
You can find the repository here on github.
Why did I start this project? I am currently working on a larger project for sending http requests and I wanted to store the requests locally on the file system. I thought why not use an existing format for specifying requests and already had some experience with the Jetbrains editor that has an inbuilt editor for sending HTTP requests. Therefore I wrote this recursive descent parser that can parse the request model from the file format as well as serialize an existing model to a file.
The http rest file format describes a request with target, headers and body that can be performed by a api client for testing purposes.
JetBrains has an inbuilt http client within their editor that can perform these specified requests. See here for more information: Intellij Http Syntax
What is the .http/.rest format
With the http file format you can specify requests given
- an HTTP method such as (GET, POST, PUT, ..)
- an URL
- optionally the HTTP version
- headers
- body
Such a request could look like this:
GET https://httpbin.org/get
or using a POST and some additional meta information in the comments:
### Some comment describing the request
# @name=Request Name
# @no-log @no-follow
POST https://httpbin.org/post
Content-Type: application/json
< path/to/json/file.json
>>! save_response_output.json
JetBrains also specified the request in editor in the following github project. http-request-in-editor-spec The content of the specification seems to be somewhat outdated but still describes the format quite well.
Add Library Using Cargo
cargo add http-rest-file
Usage
If you are using this crate you want to either parse the content of a .http/.rest file into a model or do the vice versa and create the file content from an existing model.
Model
The request model looks like this:
pub struct Request {
pub name: Option<String>,
pub comments: Vec<Comment>,
pub request_line: RequestLine,
pub headers: Vec<Header>,
pub body: RequestBody,
pub settings: RequestSettings,
pub pre_request_script: Option<PreRequestScript>,
pub response_handler: Option<ResponseHandler>,
pub save_response: Option<SaveResponse>,
}
Some parts of it are contained within the comments (meta information) such as the name
as well as the settings
(@no-log
, @no-cookie-jar
, @no-follow
).
The request_line
contains the http method type, url as well as optionally the http version.
The pre-request scripts and response handler are optional and do not need to be present.
Optionally, the result of a respones can be sent to a file specified by the SaveResponse
type.
Parsing Text -> Model
Parse a file given a file path:
use http_rest_file::Parser;
use std::path::PathBuf;
fn main() {
let model = Parser::parse_file(PathBuf::from("./your/path/request.http")).expect(jj)
}
Parse string content
let str = r#####"
POST http://example.com/api/add
Content-Type: application/json
< ./input.json
###
GET https://example.com/first
###
GET https://example.com/second
###
"#####;
let FileParseResult { requests, errs } = dbg!(Parser::parse(str, false));
println!("errs: {:?}", errs);
assert_eq!(errs.len(), 1);
assert_eq!(requests.len(), 3);
// @TODO check content
assert_eq!(
requests,
vec![
model::Request {
name: None,
comments: vec![],
headers: vec![Header {
key: "Content-Type".to_string(),
value: "application/json".to_string()
}],
body: model::RequestBody::Raw {
data: DataSource::FromFilepath("./input.json".to_string())
},
request_line: model::RequestLine {
http_version: WithDefault::default(),
method: WithDefault::Some(HttpMethod::POST),
target: model::RequestTarget::Absolute {
uri: "http://example.com/api/add".to_string()
}
},
settings: RequestSettings::default(),
pre_request_script: None,
response_handler: None,
save_response: None,
},
model::Request {
name: None,
comments: vec![],
headers: vec![],
body: model::RequestBody::None,
request_line: model::RequestLine {
http_version: WithDefault::default(),
method: WithDefault::Some(HttpMethod::GET),
target: model::RequestTarget::Absolute {
uri: "https://example.com/first".to_string()
}
},
settings: RequestSettings::default(),
pre_request_script: None,
response_handler: None,
save_response: None,
},
model::Request {
name: None,
comments: vec![],
headers: vec![],
body: model::RequestBody::None,
request_line: model::RequestLine {
http_version: WithDefault::default(),
method: WithDefault::Some(HttpMethod::GET),
target: model::RequestTarget::Absolute {
uri: "https://example.com/second".to_string()
}
},
settings: RequestSettings::default(),
pre_request_script: None,
response_handler: None,
save_response: None
}
],
);
}
Serialization Model -> Text
Either serialize to a file given a HttpRestFile
model:
http_rest_file::Serializer::serialize_to_file(rest_file_model)
Or serialize to a string given a list of request models, each will be serialized and requests are separated by the request
separator which are three tags: ###
http_rest_file::Serializer::serialize_requests(requests)
Full example:
#[test]
pub fn serialize_with_headers() {
let request = Request {
name: None,
headers: vec![Header::new("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")
, Header::new("Accept-Language", "en-US,en;q=0.9,es;q=0.8"),
// fake token
Header::new("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"),
Header::new("Cache-Control", "max-age=3600")
],
comments: vec![],
settings: RequestSettings {
no_redirect: None,
no_log: None,
no_cookie_jar: None,
},
request_line: RequestLine {
method: WithDefault::Some(HttpMethod::POST),
target: RequestTarget::from("https://httpbin.org/post"),
http_version: WithDefault::default(),
},
body: RequestBody::None,
pre_request_script: None,
response_handler: None,
save_response: None,
};
// we expect a newline after the headers
let expected = r"POST https://httpbin.org/post
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
Accept-Language: en-US,en;q=0.9,es;q=0.8
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Cache-Control: max-age=3600
";
let serialized = Serializer::serialize_requests(&[&request]);
assert_eq!(serialized, expected);
}
Error Handling And Recovery
During parsing errors can occur. See the ParseError
enum type for a display of the kind of errors that can occur and what kind
of messages are displayed if they do.
If a request can be parsed partially then the ErrorWithPartial
will be returned which contains a PartialRequest
with all
parts that could be parsed before the error occurred. From a PartialRequest
you can if you want create a Request
model where
all successfully parsed parts are present and the rest is filled up with the defaults.