Support template in keys

This commit is contained in:
Fabrice Reix 2023-10-04 16:15:25 +02:00
parent af3f78e637
commit aeca943cf7
No known key found for this signature in database
GPG Key ID: BF5213154B2E7155
26 changed files with 701 additions and 338 deletions

View File

@ -0,0 +1,7 @@
error: Undefined variable
--> tests_failed/key_template.hurl:4:3
|
4 | {{name}}: value
| ^^^^ you must set the variable name
|

View File

@ -0,0 +1 @@
4

View File

@ -0,0 +1,5 @@
# Variable not found
GET http://localhost:8000/error-key-template
HTTP 200
{{name}}: value

View File

@ -0,0 +1,3 @@
Set-StrictMode -Version latest
$ErrorActionPreference = 'Stop'
hurl tests_failed/key_template.hurl

View File

@ -0,0 +1,7 @@
from app import app
from flask import Response
@app.route("/error-key-template")
def error_key_template():
return ""

View File

@ -0,0 +1,3 @@
#!/bin/bash
set -Eeuo pipefail
hurl tests_failed/key_template.hurl

View File

@ -0,0 +1,47 @@
# Test template in the different Hurl "keys"
GET http://localhost:8000/key-template/header
{{key}}: value
[Options]
variable: key=name
HTTP 200
GET http://localhost:8000/key-template/querystring
[QueryStringParams]
{{key}}: value
[Options]
variable: key=name
HTTP 200
POST http://localhost:8000/key-template/form
[FormParams]
{{key}}: value
[Options]
variable: key=name
HTTP 200
POST http://localhost:8000/key-template/multipart-form-data
[MultipartFormData]
{{key1}}: value
{{key2}}: file,data.txt;
[Options]
variable: key1=name
variable: key2=file
HTTP 200
GET http://localhost:8000/key-template/cookie
[Cookies]
{{key}}: value
[Options]
variable: key=name
HTTP 200
GET http://localhost:8000/key-template/capture
[Options]
variable: key=name
HTTP 200
[Captures]
{{key}}: body
[Asserts]
variable "name" == "Hello"

View File

@ -0,0 +1,3 @@
Set-StrictMode -Version latest
$ErrorActionPreference = 'Stop'
hurl tests_ok/key_template.hurl

View File

@ -0,0 +1,43 @@
# coding=utf-8
from flask import request
from app import app
@app.route("/key-template/header")
def key_template_header():
print(request.headers)
assert request.headers["name"] == "value"
return ""
@app.route("/key-template/querystring")
def key_template_querystring():
assert request.args.get("name") == "value"
return ""
@app.route("/key-template/form", methods=["POST"])
def key_template_form():
assert request.form["name"] == "value"
return ""
@app.route("/key-template/multipart-form-data", methods=["POST"])
def key_template_multipart_form_data():
assert request.form["name"] == "value"
upload = request.files["file"]
assert upload.filename == "data.txt"
assert upload.content_type == "text/plain"
assert upload.read() == b"Hello World!"
return ""
@app.route("/key-template/cookie")
def key_template_cookie():
assert request.cookies["name"] == "value"
return ""
@app.route("/key-template/capture")
def key_template_capture():
return "Hello"

View File

@ -0,0 +1,3 @@
#!/bin/bash
set -Eeuo pipefail
hurl tests_ok/key_template.hurl

View File

@ -66,29 +66,29 @@ pub fn pre_entry(entry: Entry) -> bool {
fn log_request(request: Request) {
eprintln!("\r\n{} {}", request.method, request.url);
for header in request.headers {
eprintln!("\r{}: {}", header.key.value, header.value);
eprintln!("\r{}: {}", header.key, header.value);
}
for section in request.sections {
eprintln!("\r[{}]", section.name());
match section.value {
SectionValue::QueryParams(key_values) => {
for value in key_values {
eprintln!("\r{}: {}", value.key.value, value.value);
eprintln!("\r{}: {}", value.key, value.value);
}
}
SectionValue::BasicAuth(Some(key_value)) => {
eprintln!("\r{}: {}", key_value.key.value, key_value.value);
eprintln!("\r{}: {}", key_value.key, key_value.value);
}
SectionValue::FormParams(key_values) => {
for value in key_values {
eprintln!("\r{}: {}", value.key.value, value.value);
eprintln!("\r{}: {}", value.key, value.value);
}
}
SectionValue::MultipartFormData(multipart_params) => {
for param in multipart_params {
match param {
MultipartParam::Param(value) => {
eprintln!("\r{}: {}", value.key.value, value.value)
eprintln!("\r{}: {}", value.key, value.value)
}
MultipartParam::FileParam(file_param) => {
let content_type =
@ -99,7 +99,7 @@ fn log_request(request: Request) {
};
eprintln!(
"\r{}: {}{}",
file_param.key.value, file_param.value.filename.value, content_type
file_param.key, file_param.value.filename.value, content_type
);
}
}
@ -107,7 +107,7 @@ fn log_request(request: Request) {
}
SectionValue::Cookies(cookies) => {
for cookie in cookies {
eprintln!("\r{}: {}", cookie.name.value, cookie.value);
eprintln!("\r{}: {}", cookie.name, cookie.value);
}
}
_ => {}

View File

@ -23,6 +23,7 @@ use crate::http;
use crate::runner::core::{CaptureResult, Error, RunnerError};
use crate::runner::filter::eval_filters;
use crate::runner::query::eval_query;
use crate::runner::template::eval_template;
use crate::runner::value::Value;
/// Evaluates a `capture` with `variables` map and `http_response`, returns a
@ -32,7 +33,7 @@ pub fn eval_capture(
variables: &HashMap<String, Value>,
http_response: &http::Response,
) -> Result<CaptureResult, Error> {
let name = &capture.name.value;
let name = eval_template(&capture.name, variables)?;
let value = eval_query(&capture.query, variables, http_response)?;
let value = match value {
None => {
@ -79,10 +80,12 @@ pub mod tests {
Capture {
line_terminators: vec![],
space0: whitespace.clone(),
name: EncodedString {
quotes: false,
value: "UserCount".to_string(),
encoded: "UserCount".to_string(),
name: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "UserCount".to_string(),
encoded: "UserCount".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space1: whitespace.clone(),
@ -108,10 +111,12 @@ pub mod tests {
Capture {
line_terminators: vec![],
space0: whitespace.clone(),
name: EncodedString {
quotes: false,
value: "duration".to_string(),
encoded: "duration".to_string(),
name: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "duration".to_string(),
encoded: "duration".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space1: whitespace.clone(),
@ -138,10 +143,12 @@ pub mod tests {
let capture = Capture {
line_terminators: vec![],
space0: whitespace.clone(),
name: EncodedString {
quotes: false,
value: "count".to_string(),
encoded: "count".to_string(),
name: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "count".to_string(),
encoded: "count".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
filters: vec![],
@ -173,10 +180,12 @@ pub mod tests {
let _capture = Capture {
line_terminators: vec![],
space0: whitespace.clone(),
name: EncodedString {
quotes: false,
value: "???".to_string(),
encoded: "???".to_string(),
name: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "???".to_string(),
encoded: "???".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space1: whitespace.clone(),

View File

@ -35,12 +35,12 @@ pub fn eval_multipart_param(
) -> Result<http::MultipartParam, Error> {
match multipart_param {
MultipartParam::Param(KeyValue { key, value, .. }) => {
let name = key.value.clone();
let name = eval_template(key, variables)?;
let value = eval_template(value, variables)?;
Ok(http::MultipartParam::Param(http::Param { name, value }))
}
MultipartParam::FileParam(param) => {
let file_param = eval_file_param(param, context_dir)?;
let file_param = eval_file_param(param, context_dir, variables)?;
Ok(http::MultipartParam::FileParam(file_param))
}
}
@ -49,8 +49,9 @@ pub fn eval_multipart_param(
pub fn eval_file_param(
file_param: &FileParam,
context_dir: &ContextDir,
variables: &HashMap<String, Value>,
) -> Result<http::FileParam, Error> {
let name = file_param.key.value.clone();
let name = eval_template(&file_param.key, variables)?;
let filename = file_param.value.filename.clone();
let data = eval_file(&filename, context_dir)?;
let content_type = file_value_content_type(&file_param.value);
@ -107,14 +108,17 @@ mod tests {
let current_dir = std::env::current_dir().unwrap();
let file_root = Path::new("tests");
let context_dir = ContextDir::new(current_dir.as_path(), file_root);
let variables = HashMap::default();
let param = eval_file_param(
&FileParam {
line_terminators: vec![],
space0: whitespace(),
key: EncodedString {
value: "upload1".to_string(),
encoded: "upload1".to_string(),
quotes: false,
key: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "upload1".to_string(),
encoded: "upload1".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space1: whitespace(),
@ -132,6 +136,7 @@ mod tests {
line_terminator0: line_terminator,
},
&context_dir,
&variables,
)
.unwrap();
assert_eq!(

View File

@ -41,16 +41,17 @@ pub fn eval_request(
// Headers
let mut headers: Vec<http::Header> = vec![];
for header in &request.headers {
let name = &header.key.value;
let name = eval_template(&header.key, variables)?;
let value = eval_template(&header.value, variables)?;
let header = http::Header::new(name, &value);
let header = http::Header::new(&name, &value);
headers.push(header);
}
// Basic auth
if let Some(kv) = &request.basic_auth() {
let name = eval_template(&kv.key, variables)?;
let value = eval_template(&kv.value, variables)?;
let user_password = format!("{}:{}", kv.key.value, value);
let user_password = format!("{}:{}", name, value);
let user_password = user_password.as_bytes();
let authorization = general_purpose::STANDARD.encode(user_password);
let value = format!("Basic {authorization}");
@ -61,7 +62,7 @@ pub fn eval_request(
// Query string params
let mut querystring: Vec<http::Param> = vec![];
for param in &request.querystring_params() {
let name = param.key.value.clone();
let name = eval_template(&param.key, variables)?;
let value = eval_template(&param.value, variables)?;
let param = http::Param { name, value };
querystring.push(param);
@ -70,7 +71,7 @@ pub fn eval_request(
// Form params
let mut form: Vec<http::Param> = vec![];
for param in &request.form_params() {
let name = param.key.value.clone();
let name = eval_template(&param.key, variables)?;
let value = eval_template(&param.value, variables)?;
let param = http::Param { name, value };
form.push(param);
@ -79,8 +80,8 @@ pub fn eval_request(
// Cookies
let mut cookies = vec![];
for cookie in &request.cookies() {
let name = eval_template(&cookie.name, variables)?;
let value = eval_template(&cookie.value, variables)?;
let name = cookie.name.value.clone();
let cookie = http::RequestCookie { name, value };
cookies.push(cookie);
}
@ -215,7 +216,7 @@ mod tests {
}
}
fn simple_key_value(key: EncodedString, value: Template) -> KeyValue {
fn simple_key_value(key: Template, value: Template) -> KeyValue {
let line_terminator = LineTerminator {
space0: whitespace(),
comment: None,
@ -259,10 +260,12 @@ mod tests {
line_terminator0: line_terminator,
value: SectionValue::QueryParams(vec![
simple_key_value(
EncodedString {
quotes: false,
value: "param1".to_string(),
encoded: "param1".to_string(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "param1".to_string(),
encoded: "param1".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
Template {
@ -279,10 +282,12 @@ mod tests {
},
),
simple_key_value(
EncodedString {
quotes: false,
value: "param2".to_string(),
encoded: "param2".to_string(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "param2".to_string(),
encoded: "param2".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
Template {

View File

@ -78,48 +78,58 @@ pub fn eval_asserts(
});
}
Ok(expected) => {
let header_name = &header.key.value;
let actuals = http_response.get_header_values(header_name);
if actuals.is_empty() {
asserts.push(AssertResult::Header {
actual: Err(Error {
source_info: header.key.source_info.clone(),
inner: RunnerError::QueryHeaderNotFound,
assert: false,
}),
expected,
source_info: header.key.source_info.clone(),
});
} else if actuals.len() == 1 {
let actual = actuals.first().unwrap().to_string();
asserts.push(AssertResult::Header {
actual: Ok(actual),
expected,
source_info: header.value.clone().source_info,
});
} else {
// failure by default
// expected value not found in the list
// actual is therefore the full list
let mut actual = format!(
"[{}]",
actuals
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<String>>()
.join(", ")
);
for value in actuals {
if value == expected {
actual = value;
break;
match eval_template(&header.key, variables) {
Ok(header_name) => {
let actuals = http_response.get_header_values(&header_name);
if actuals.is_empty() {
asserts.push(AssertResult::Header {
actual: Err(Error {
source_info: header.key.source_info.clone(),
inner: RunnerError::QueryHeaderNotFound,
assert: false,
}),
expected,
source_info: header.key.source_info.clone(),
});
} else if actuals.len() == 1 {
let actual = actuals.first().unwrap().to_string();
asserts.push(AssertResult::Header {
actual: Ok(actual),
expected,
source_info: header.value.clone().source_info,
});
} else {
// failure by default
// expected value not found in the list
// actual is therefore the full list
let mut actual = format!(
"[{}]",
actuals
.iter()
.map(|v| format!("\"{v}\""))
.collect::<Vec<String>>()
.join(", ")
);
for value in actuals {
if value == expected {
actual = value;
break;
}
}
asserts.push(AssertResult::Header {
actual: Ok(actual),
expected,
source_info: header.value.clone().source_info,
});
}
}
asserts.push(AssertResult::Header {
actual: Ok(actual),
expected,
source_info: header.value.clone().source_info,
});
Err(e) => {
asserts.push(AssertResult::Header {
actual: Err(e),
expected,
source_info: header.value.clone().source_info,
});
}
}
}
}

View File

@ -221,7 +221,7 @@ pub enum SectionValue {
pub struct Cookie {
pub line_terminators: Vec<LineTerminator>,
pub space0: Whitespace,
pub name: EncodedString,
pub name: Template,
pub space1: Whitespace,
pub space2: Whitespace,
pub value: Template,
@ -232,7 +232,7 @@ pub struct Cookie {
pub struct KeyValue {
pub line_terminators: Vec<LineTerminator>,
pub space0: Whitespace,
pub key: EncodedString,
pub key: Template,
pub space1: Whitespace,
pub space2: Whitespace,
pub value: Template,
@ -249,7 +249,7 @@ pub enum MultipartParam {
pub struct FileParam {
pub line_terminators: Vec<LineTerminator>,
pub space0: Whitespace,
pub key: EncodedString,
pub key: Template,
pub space1: Whitespace,
pub space2: Whitespace,
pub value: FileValue,
@ -269,7 +269,7 @@ pub struct FileValue {
pub struct Capture {
pub line_terminators: Vec<LineTerminator>,
pub space0: Whitespace,
pub name: EncodedString,
pub name: Template,
pub space1: Whitespace,
pub space2: Whitespace,
pub query: Query,

View File

@ -195,7 +195,7 @@ impl HtmlFormatter {
self.fmt_lts(&kv.line_terminators);
self.fmt_span_open("line");
self.fmt_space(&kv.space0);
self.fmt_string(&kv.key.encoded);
self.fmt_template(&kv.key);
self.fmt_space(&kv.space1);
self.buffer.push(':');
self.fmt_space(&kv.space2);
@ -274,7 +274,7 @@ impl HtmlFormatter {
self.fmt_lts(&param.line_terminators);
self.fmt_span_open("line");
self.fmt_space(&param.space0);
self.fmt_string(&param.key.encoded);
self.fmt_template(&param.key);
self.fmt_space(&param.space1);
self.buffer.push(':');
self.fmt_space(&param.space2);
@ -306,7 +306,7 @@ impl HtmlFormatter {
self.fmt_lts(&cookie.line_terminators);
self.fmt_span_open("line");
self.fmt_space(&cookie.space0);
self.fmt_span("name", &cookie.name.value);
self.fmt_template(&cookie.name);
self.fmt_space(&cookie.space1);
self.buffer.push(':');
self.fmt_space(&cookie.space2);
@ -319,7 +319,7 @@ impl HtmlFormatter {
self.fmt_lts(&capture.line_terminators);
self.fmt_span_open("line");
self.fmt_space(&capture.space0);
self.fmt_span("name", &capture.name.value);
self.fmt_template(&capture.name);
self.fmt_space(&capture.space1);
self.buffer.push(':');
self.fmt_space(&capture.space2);

View File

@ -0,0 +1,319 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2023 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use crate::ast::*;
use crate::parser::error::*;
use crate::parser::primitives::*;
use crate::parser::reader::Reader;
use crate::parser::template::template;
use crate::parser::{string, ParseResult};
pub fn parse(reader: &mut Reader) -> ParseResult<Template> {
let start = reader.state.clone();
let mut elements = vec![];
loop {
match template(reader) {
Ok(expr) => {
let element = TemplateElement::Expression(expr);
elements.push(element);
}
Err(e) => {
if e.recoverable {
let value = key_string_content(reader)?;
if value.is_empty() {
break;
}
let encoded: String = reader.buffer[start.cursor..reader.state.cursor]
.iter()
.collect();
let element = TemplateElement::String { value, encoded };
elements.push(element);
} else {
return Err(e);
}
}
}
}
if elements.is_empty() {
return Err(Error {
pos: start.pos,
recoverable: false,
inner: ParseError::Expecting {
value: "key-string".to_string(),
},
});
}
if let Some(TemplateElement::String { encoded, .. }) = elements.first() {
if encoded.starts_with('[') {
return Err(Error {
pos: start.pos,
recoverable: false,
inner: ParseError::Expecting {
value: "key-string".to_string(),
},
});
}
}
let end = reader.state.clone();
Ok(Template {
delimiter: None,
elements,
source_info: SourceInfo {
start: start.pos,
end: end.pos,
},
})
}
fn key_string_content(reader: &mut Reader) -> ParseResult<String> {
let mut s = String::new();
loop {
match key_string_escaped_char(reader) {
Ok(c) => {
s.push(c);
}
Err(e) => {
if e.recoverable {
let s2 = key_string_text(reader);
if s2.is_empty() {
break;
} else {
s.push_str(&s2);
}
} else {
return Err(e);
}
}
}
}
Ok(s)
}
fn key_string_text(reader: &mut Reader) -> String {
let mut s = String::new();
loop {
let save = reader.state.clone();
match reader.read() {
None => break,
Some(c) => {
if c.is_alphanumeric()
|| c == '_'
|| c == '-'
|| c == '.'
|| c == '['
|| c == ']'
|| c == '@'
|| c == '$'
{
s.push(c);
} else {
reader.state = save;
break;
}
}
}
}
s
}
fn key_string_escaped_char(reader: &mut Reader) -> ParseResult<char> {
try_literal("\\", reader)?;
let start = reader.state.clone();
match reader.read() {
Some('#') => Ok('#'),
Some(':') => Ok(':'),
Some('\\') => Ok('\\'),
Some('/') => Ok('/'),
Some('b') => Ok('\x08'),
Some('f') => Ok('\x0c'),
Some('n') => Ok('\n'),
Some('r') => Ok('\r'),
Some('t') => Ok('\t'),
Some('u') => string::unicode(reader),
_ => Err(Error {
pos: start.pos,
recoverable: false,
inner: ParseError::EscapeChar,
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_string() {
let mut reader = Reader::new("aaa\\: ");
assert_eq!(
parse(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "aaa:".to_string(),
encoded: "aaa\\:".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 6),
}
);
assert_eq!(reader.state.cursor, 5);
let mut reader = Reader::new("$top:");
assert_eq!(
parse(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "$top".to_string(),
encoded: "$top".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 5),
}
);
assert_eq!(reader.state.cursor, 4);
let mut reader = Reader::new("key\\u{20}\\u{3a} :");
assert_eq!(
parse(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "key :".to_string(),
encoded: "key\\u{20}\\u{3a}".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 16),
}
);
assert_eq!(reader.state.cursor, 15);
let mut reader = Reader::new("values\\u{5b}0\\u{5d} :");
assert_eq!(
parse(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "values[0]".to_string(),
encoded: "values\\u{5b}0\\u{5d}".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 20),
}
);
assert_eq!(reader.state.cursor, 19);
let mut reader = Reader::new("values[0] :");
assert_eq!(
parse(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "values[0]".to_string(),
encoded: "values[0]".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 10),
}
);
assert_eq!(reader.state.cursor, 9);
let mut reader = Reader::new("\\u{5b}0\\u{5d}");
assert_eq!(
parse(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "[0]".to_string(),
encoded: "\\u{5b}0\\u{5d}".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 14),
}
);
assert_eq!(reader.state.cursor, 13);
}
#[test]
fn test_key_string_error() {
let mut reader = Reader::new("");
let error = parse(&mut reader).err().unwrap();
assert!(!error.recoverable);
assert_eq!(error.pos, Pos { line: 1, column: 1 });
let mut reader = Reader::new("{{key");
let error = parse(&mut reader).err().unwrap();
assert!(!error.recoverable);
assert_eq!(error.pos, Pos { line: 1, column: 6 });
// key string can not start with a '[' (reserved for section)
let mut reader = Reader::new("[0]:");
let error = parse(&mut reader).err().unwrap();
assert!(!error.recoverable);
assert_eq!(reader.state.cursor, 3);
let mut reader = Reader::new("\\l");
let error = parse(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 2 });
assert_eq!(error.inner, ParseError::EscapeChar);
let mut reader = Reader::new(r#"{"id":1}"#);
let error = parse(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 1 });
assert_eq!(
error.inner,
ParseError::Expecting {
value: "key-string".to_string()
}
);
}
#[test]
fn test_key_string_content() {
let mut reader = Reader::new("aaa\\:");
assert_eq!(key_string_content(&mut reader).unwrap(), "aaa:");
}
#[test]
fn test_key_string_text() {
let mut reader = Reader::new("aaa\\:");
assert_eq!(key_string_text(&mut reader), "aaa");
assert_eq!(reader.state.cursor, 3);
}
#[test]
fn test_key_string_escaped_char() {
let mut reader = Reader::new("\\u{0a}");
assert_eq!(key_string_escaped_char(&mut reader).unwrap(), '\n');
assert_eq!(reader.state.cursor, 6);
let mut reader = Reader::new("\\:");
assert_eq!(key_string_escaped_char(&mut reader).unwrap(), ':');
assert_eq!(reader.state.cursor, 2);
let mut reader = Reader::new("x");
let error = key_string_escaped_char(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 1 });
assert_eq!(
error.inner,
ParseError::Expecting {
value: "\\".to_string()
}
);
assert!(error.recoverable);
assert_eq!(reader.state.cursor, 0);
}
}

View File

@ -41,6 +41,7 @@ mod expr;
mod filename;
mod filter;
mod json;
mod key_string;
mod multiline;
mod parsers;
mod predicate;

View File

@ -323,7 +323,8 @@ mod tests {
body: None,
source_info: SourceInfo::new(1, 1, 1, 21),
};
assert_eq!(request(&mut reader), Ok(default_request));
assert_eq!(request(&mut reader).unwrap(), default_request);
assert_eq!(reader.state.cursor, 20);
let mut reader = Reader::new("GET http://google.fr # comment");
let default_request = Request {
@ -363,11 +364,12 @@ mod tests {
body: None,
source_info: SourceInfo::new(1, 1, 1, 32),
};
assert_eq!(request(&mut reader), Ok(default_request));
assert_eq!(request(&mut reader).unwrap(), default_request);
assert_eq!(reader.state.cursor, 31);
let mut reader = Reader::new("GET http://google.fr\nGET http://google.fr");
let r = request(&mut reader);
assert_eq!(r.unwrap().method, Method("GET".to_string()));
let r = request(&mut reader).unwrap();
assert_eq!(r.method, Method("GET".to_string()));
assert_eq!(reader.state.cursor, 21);
let r = request(&mut reader).unwrap();
assert_eq!(r.method, Method("GET".to_string()));

View File

@ -20,7 +20,7 @@ use crate::parser::combinators::*;
use crate::parser::error::*;
use crate::parser::reader::Reader;
use crate::parser::string::*;
use crate::parser::{base64, filename, ParseResult};
use crate::parser::{base64, filename, key_string, ParseResult};
pub fn space(reader: &mut Reader) -> ParseResult<Whitespace> {
let start = reader.state.clone();
@ -268,7 +268,7 @@ pub fn newline(reader: &mut Reader) -> ParseResult<Whitespace> {
pub fn key_value(reader: &mut Reader) -> ParseResult<KeyValue> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;
let key = recover(unquoted_string_key, reader)?;
let key = recover(key_string::parse, reader)?;
let space1 = zero_or_more_spaces(reader)?;
recover(|reader1| literal(":", reader1), reader)?;
let space2 = zero_or_more_spaces(reader)?;
@ -759,10 +759,12 @@ mod tests {
value: String::new(),
source_info: SourceInfo::new(1, 1, 1, 1),
},
key: EncodedString {
quotes: false,
value: "message".to_string(),
encoded: "message".to_string(),
key: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "message".to_string(),
encoded: "message".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 8),
},
space1: Whitespace {
@ -818,6 +820,82 @@ mod tests {
);
}
#[test]
fn test_key_value_template() {
let mut reader = Reader::new("{{key}}: value");
assert_eq!(
key_value(&mut reader).unwrap(),
KeyValue {
line_terminators: vec![],
space0: Whitespace {
value: String::new(),
source_info: SourceInfo::new(1, 1, 1, 1),
},
key: Template {
delimiter: None,
elements: vec![TemplateElement::Expression(Expr {
space0: Whitespace {
value: String::new(),
source_info: SourceInfo::new(1, 3, 1, 3),
},
variable: Variable {
name: "key".to_string(),
source_info: SourceInfo::new(1, 3, 1, 6)
},
space1: Whitespace {
value: String::new(),
source_info: SourceInfo::new(1, 6, 1, 6),
},
})],
source_info: SourceInfo::new(1, 1, 1, 8),
},
space1: Whitespace {
value: String::new(),
source_info: SourceInfo::new(1, 8, 1, 8),
},
space2: Whitespace {
value: " ".to_string(),
source_info: SourceInfo::new(1, 9, 1, 10),
},
value: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "value".to_string(),
encoded: "value".to_string(),
}],
source_info: SourceInfo::new(1, 10, 1, 15),
},
line_terminator0: LineTerminator {
space0: Whitespace {
value: "".to_string(),
source_info: SourceInfo::new(1, 15, 1, 15),
},
comment: None,
newline: Whitespace {
value: String::new(),
source_info: SourceInfo::new(1, 15, 1, 15),
},
},
}
);
assert_eq!(reader.state.cursor, 14);
}
#[test]
fn test_key_value_recover() {
let mut reader = Reader::new("{{key");
let error = key_value(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 6 });
assert!(error.recoverable);
assert_eq!(reader.state.cursor, 5); // does not reset cursor
let mut reader = Reader::new("GET http://google.fr");
let error = key_value(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 5 });
assert!(error.recoverable);
assert_eq!(reader.state.cursor, 5); // does not reset cursor
}
#[test]
fn test_boolean() {
let mut reader = Reader::new("true");

View File

@ -24,7 +24,7 @@ use crate::parser::primitives::*;
use crate::parser::query::query;
use crate::parser::reader::Reader;
use crate::parser::string::*;
use crate::parser::{filename, ParseResult};
use crate::parser::{filename, key_string, ParseResult};
pub fn request_sections(reader: &mut Reader) -> ParseResult<Vec<Section>> {
let sections = zero_or_more(request_section, reader)?;
@ -170,7 +170,7 @@ fn cookie(reader: &mut Reader) -> ParseResult<Cookie> {
// let start = reader.state.clone();
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;
let name = unquoted_string_key(reader)?;
let name = recover(key_string::parse, reader)?;
let space1 = zero_or_more_spaces(reader)?;
recover(|p1| literal(":", p1), reader)?;
let space2 = zero_or_more_spaces(reader)?;
@ -206,7 +206,7 @@ fn multipart_param(reader: &mut Reader) -> ParseResult<MultipartParam> {
fn file_param(reader: &mut Reader) -> ParseResult<FileParam> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;
let key = recover(unquoted_string_key, reader)?;
let key = recover(key_string::parse, reader)?;
let space1 = zero_or_more_spaces(reader)?;
recover(|reader1| literal(":", reader1), reader)?;
let space2 = zero_or_more_spaces(reader)?;
@ -291,7 +291,7 @@ fn file_content_type(reader: &mut Reader) -> ParseResult<String> {
fn capture(reader: &mut Reader) -> ParseResult<Capture> {
let line_terminators = optional_line_terminators(reader)?;
let space0 = zero_or_more_spaces(reader)?;
let name = unquoted_string_key(reader)?;
let name = recover(key_string::parse, reader)?;
let space1 = zero_or_more_spaces(reader)?;
recover(|p1| literal(":", p1), reader)?;
let space2 = zero_or_more_spaces(reader)?;
@ -808,7 +808,7 @@ mod tests {
fn test_cookie() {
let mut reader = Reader::new("Foo: Bar");
let c = cookie(&mut reader).unwrap();
assert_eq!(c.name.value, String::from("Foo"));
assert_eq!(c.name.to_string(), String::from("Foo"));
assert_eq!(
c.value,
Template {
@ -1153,12 +1153,14 @@ mod tests {
assert_eq!(
capture0.name,
EncodedString {
quotes: false,
value: String::from("url"),
encoded: String::from("url"),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "url".to_string(),
encoded: "url".to_string(),
}],
source_info: SourceInfo::new(1, 1, 1, 4),
}
},
);
assert_eq!(
capture0.query,
@ -1330,10 +1332,12 @@ mod tests {
value: String::new(),
source_info: SourceInfo::new(2, 1, 2, 1)
},
key: EncodedString {
value: "user".to_string(),
encoded: "user".to_string(),
quotes: false,
key: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "user".to_string(),
encoded: "user".to_string()
}],
source_info: SourceInfo::new(2, 1, 2, 5),
},
space1: Whitespace {

View File

@ -23,7 +23,7 @@ use crate::parser::reader::Reader;
use crate::parser::{template, ParseResult};
/// Steps:
/// 1- parse String until end of stream or end of line or #
/// 1- parse String until end of stream, end of line
/// the string does not contain trailing space
/// 2- templatize
pub fn unquoted_template(reader: &mut Reader) -> ParseResult<Template> {
@ -77,69 +77,6 @@ pub fn unquoted_template(reader: &mut Reader) -> ParseResult<Template> {
})
}
pub fn unquoted_string_key(reader: &mut Reader) -> ParseResult<EncodedString> {
let start = reader.state.pos.clone();
let mut value = String::new();
let mut encoded = String::new();
loop {
let save = reader.state.clone();
match escape_char(reader) {
Ok(c) => {
value.push(c);
encoded.push_str(reader.peek_back(save.cursor).as_str())
}
Err(e) => {
if e.recoverable {
reader.state = save.clone();
match reader.read() {
None => break,
Some(c) => {
if c.is_alphanumeric()
|| c == '_'
|| c == '-'
|| c == '.'
|| c == '['
|| c == ']'
|| c == '@'
|| c == '$'
{
value.push(c);
encoded.push_str(reader.peek_back(save.cursor).as_str())
} else {
reader.state = save;
break;
}
}
}
} else {
return Err(e);
}
}
}
}
// check nonempty/ starts with [
if value.is_empty() || encoded.starts_with('[') {
return Err(Error {
pos: start,
recoverable: true,
inner: ParseError::Expecting {
value: "key string".to_string(),
},
});
}
let quotes = false;
let end = reader.state.pos.clone();
let source_info = SourceInfo { start, end };
Ok(EncodedString {
value,
encoded,
quotes,
source_info,
})
}
// TODO: should return an EncodedString
// (decoding escape sequence)
pub fn quoted_oneline_string(reader: &mut Reader) -> ParseResult<String> {
@ -295,7 +232,7 @@ fn escape_char(reader: &mut Reader) -> ParseResult<char> {
}
}
fn unicode(reader: &mut Reader) -> ParseResult<char> {
pub(crate) fn unicode(reader: &mut Reader) -> ParseResult<char> {
literal("{", reader)?;
let v = hex_value(reader)?;
let c = match std::char::from_u32(v) {
@ -432,147 +369,6 @@ mod tests {
assert_eq!(reader.state.cursor, 20);
}
#[test]
fn test_unquoted_template_trailing_space() {
let mut reader = Reader::new("hello world # comment");
assert_eq!(
unquoted_template(&mut reader).unwrap(),
Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "hello world".to_string(),
encoded: "hello world".to_string(),
},],
source_info: SourceInfo::new(1, 1, 1, 12),
}
);
assert_eq!(reader.state.cursor, 11);
assert_eq!(
reader.state.pos,
Pos {
line: 1,
column: 12
}
);
}
#[test]
fn test_unquoted_key() {
let mut reader = Reader::new("key");
assert_eq!(
unquoted_string_key(&mut reader).unwrap(),
EncodedString {
value: "key".to_string(),
encoded: "key".to_string(),
quotes: false,
source_info: SourceInfo::new(1, 1, 1, 4),
}
);
assert_eq!(reader.state.cursor, 3);
let mut reader = Reader::new("key\\u{20}\\u{3a} :");
assert_eq!(
unquoted_string_key(&mut reader).unwrap(),
EncodedString {
value: "key :".to_string(),
encoded: "key\\u{20}\\u{3a}".to_string(),
quotes: false,
source_info: SourceInfo::new(1, 1, 1, 16),
}
);
assert_eq!(reader.state.cursor, 15);
let mut reader = Reader::new("$top:");
assert_eq!(
unquoted_string_key(&mut reader).unwrap(),
EncodedString {
value: "$top".to_string(),
encoded: "$top".to_string(),
quotes: false,
source_info: SourceInfo::new(1, 1, 1, 5),
}
);
assert_eq!(reader.state.cursor, 4);
}
#[test]
fn test_unquoted_key_with_square_bracket() {
let mut reader = Reader::new("values\\u{5b}0\\u{5d} :");
assert_eq!(
unquoted_string_key(&mut reader).unwrap(),
EncodedString {
value: "values[0]".to_string(),
encoded: "values\\u{5b}0\\u{5d}".to_string(),
quotes: false,
source_info: SourceInfo::new(1, 1, 1, 20),
}
);
assert_eq!(reader.state.cursor, 19);
let mut reader = Reader::new("values[0] :");
assert_eq!(
unquoted_string_key(&mut reader).unwrap(),
EncodedString {
value: "values[0]".to_string(),
encoded: "values[0]".to_string(),
quotes: false,
source_info: SourceInfo::new(1, 1, 1, 10),
}
);
assert_eq!(reader.state.cursor, 9);
}
#[test]
fn test_unquoted_keys_ignore_start_square_bracket() {
let mut reader = Reader::new("[0]:");
let error = unquoted_string_key(&mut reader).err().unwrap();
assert!(error.recoverable);
assert_eq!(reader.state.cursor, 3);
}
#[test]
fn test_unquoted_keys_accept_start_escape_square_bracket() {
let mut reader = Reader::new("\\u{5b}0\\u{5d}");
assert_eq!(
unquoted_string_key(&mut reader).unwrap(),
EncodedString {
value: "[0]".to_string(),
encoded: "\\u{5b}0\\u{5d}".to_string(),
quotes: false,
source_info: SourceInfo::new(1, 1, 1, 14),
}
);
assert_eq!(reader.state.cursor, 13);
}
#[test]
fn test_unquoted_key_error() {
let mut reader = Reader::new("");
let error = unquoted_string_key(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 1 });
assert_eq!(
error.inner,
ParseError::Expecting {
value: "key string".to_string()
}
);
let mut reader = Reader::new("\\l");
let error = unquoted_string_key(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 2 });
assert_eq!(error.inner, ParseError::EscapeChar);
let mut reader = Reader::new(r#"{"id":1}"#);
let error = unquoted_string_key(&mut reader).err().unwrap();
assert_eq!(error.pos, Pos { line: 1, column: 1 });
assert_eq!(
error.inner,
ParseError::Expecting {
value: "key string".to_string()
}
);
}
#[test]
fn test_quoted_template() {
let mut reader = Reader::new("\"\"");

View File

@ -15,7 +15,8 @@
* limitations under the License.
*
*/
use crate::ast::{Pos, SourceInfo, TemplateElement};
use crate::ast::{Expr, Pos, SourceInfo, TemplateElement};
use crate::parser::primitives::{literal, try_literal};
use crate::parser::reader::*;
use crate::parser::{error, expr, ParseResult};
@ -24,6 +25,13 @@ pub struct EncodedString {
pub chars: Vec<(char, String, Pos)>,
}
pub fn template(reader: &mut Reader) -> ParseResult<Expr> {
try_literal("{{", reader)?;
let expression = expr::parse2(reader)?;
literal("}}", reader)?;
Ok(expression)
}
pub fn templatize(encoded_string: EncodedString) -> ParseResult<Vec<TemplateElement>> {
enum State {
String,

View File

@ -262,7 +262,7 @@ mod tests {
// A valid complete XML
let input = xml;
let output = xml;
let mut reader = Reader::new(&input);
let mut reader = Reader::new(input);
assert_eq!(parse(&mut reader).unwrap(), String::from(output),);
assert_eq!(reader.state.cursor, 520);

View File

@ -227,7 +227,7 @@ fn get_json_version(version_value: &VersionValue) -> Option<String> {
impl ToJson for KeyValue {
fn to_json(&self) -> JValue {
let attributes = vec![
("name".to_string(), JValue::String(self.key.value.clone())),
("name".to_string(), JValue::String(self.key.to_string())),
("value".to_string(), JValue::String(self.value.to_string())),
];
JValue::Object(attributes)
@ -246,7 +246,7 @@ impl ToJson for MultipartParam {
impl ToJson for FileParam {
fn to_json(&self) -> JValue {
let mut attributes = vec![
("name".to_string(), JValue::String(self.key.value.clone())),
("name".to_string(), JValue::String(self.key.to_string())),
(
"filename".to_string(),
JValue::String(self.value.filename.value.clone()),
@ -262,7 +262,7 @@ impl ToJson for FileParam {
impl ToJson for Cookie {
fn to_json(&self) -> JValue {
let attributes = vec![
("name".to_string(), JValue::String(self.name.value.clone())),
("name".to_string(), JValue::String(self.name.to_string())),
("value".to_string(), JValue::String(self.value.to_string())),
];
JValue::Object(attributes)
@ -311,7 +311,7 @@ impl ToJson for EntryOption {
impl ToJson for Capture {
fn to_json(&self) -> JValue {
let mut attributes = vec![
("name".to_string(), JValue::String(self.name.value.clone())),
("name".to_string(), JValue::String(self.name.to_string())),
("query".to_string(), self.query.to_json()),
];
if !self.filters.is_empty() {
@ -709,10 +709,12 @@ pub mod tests {
headers: vec![KeyValue {
line_terminators: vec![],
space0: whitespace(),
key: EncodedString {
value: "Foo".to_string(),
encoded: "unused".to_string(),
quotes: false,
key: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "Foo".to_string(),
encoded: "unused".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space1: whitespace(),
@ -824,10 +826,12 @@ pub mod tests {
Capture {
line_terminators: vec![],
space0: whitespace(),
name: EncodedString {
value: "size".to_string(),
encoded: "unused".to_string(),
quotes: false,
name: Template {
delimiter: None,
elements: vec![TemplateElement::String {
value: "size".to_string(),
encoded: "unused".to_string(),
}],
source_info: SourceInfo::new(0, 0, 0, 0),
},
space1: whitespace(),