diff --git a/docs/spec/hurl.grammar b/docs/spec/hurl.grammar
index 7377f6921..d93d14d70 100644
--- a/docs/spec/hurl.grammar
+++ b/docs/spec/hurl.grammar
@@ -418,6 +418,8 @@ variable-name: [A-Za-z] [A-Za-z_-0-9]*
filter:
count-filter
+ | days-after-now
+ | days-before-now
| format-filter
| html-escape-filter
| html-unescape-filter
@@ -432,6 +434,10 @@ filter:
count-filter: "count"
+days-after-now: "daysAfterNow"
+
+days-before-now: "daysBeforeNow"
+
format-filter: "format"
html-escape-filter: "htmlEscape"
diff --git a/integration/ssl/letsencrypt.hurl b/integration/ssl/letsencrypt.hurl
index a684a1498..da6e5c607 100644
--- a/integration/ssl/letsencrypt.hurl
+++ b/integration/ssl/letsencrypt.hurl
@@ -1,2 +1,7 @@
GET https://hurl.dev
HTTP 200
+[Asserts]
+certificate "Subject" == "CN=hurl.dev"
+certificate "Issuer" == "C=US, O=Let's Encrypt, CN=R3"
+certificate "Expire-Date" daysAfterNow > 30
+certificate "Serial-Number" == "03:db:c4:f3:c8:5e:13:66:cc:52:a3:69:2c:fa:04:a0:7b:b1"
diff --git a/integration/tests_ok/assert_header.html b/integration/tests_ok/assert_header.html
index 2c44b1f91..7ea330fa1 100644
--- a/integration/tests_ok/assert_header.html
+++ b/integration/tests_ok/assert_header.html
@@ -11,6 +11,7 @@
header "ETag" == "\"33a64df551425fcc55e4d42a148795d9f25f89d4\""
header "Expires" == "Wed, 21 Oct 2015 07:28:00 GMT"
header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" format "%Y" == "2015"
+header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000
header "Set-Cookie" exists
header "Set-Cookie" count == 3
header "Set-Cookie" includes "cookie1=value1; Path=/"
diff --git a/integration/tests_ok/assert_header.hurl b/integration/tests_ok/assert_header.hurl
index a8bc8f222..38b2f18d4 100644
--- a/integration/tests_ok/assert_header.hurl
+++ b/integration/tests_ok/assert_header.hurl
@@ -11,6 +11,7 @@ header "Header1" == "value1"
header "ETag" == "\"33a64df551425fcc55e4d42a148795d9f25f89d4\""
header "Expires" == "Wed, 21 Oct 2015 07:28:00 GMT"
header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" format "%Y" == "2015"
+header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000
header "Set-Cookie" exists
header "Set-Cookie" count == 3
header "Set-Cookie" includes "cookie1=value1; Path=/"
diff --git a/integration/tests_ok/assert_header.json b/integration/tests_ok/assert_header.json
index 1a439988c..cfac9f1d1 100644
--- a/integration/tests_ok/assert_header.json
+++ b/integration/tests_ok/assert_header.json
@@ -1 +1 @@
-{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/assert-header"},"response":{"status":200,"headers":[{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"}],"asserts":[{"query":{"type":"header","name":"Custom"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"header","name":"Content-Type"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Header1"},"predicate":{"type":"equal","value":"value1"}},{"query":{"type":"header","name":"ETag"},"predicate":{"type":"equal","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""}},{"query":{"type":"header","name":"Expires"},"predicate":{"type":"equal","value":"Wed, 21 Oct 2015 07:28:00 GMT"}},{"query":{"type":"header","name":"Expires"},"filters":[{"type":"toDate","fmt":"%a, %d %b %Y %H:%M:%S GMT"},{"type":"format","fmt":"%Y"}],"predicate":{"type":"equal","value":"2015"}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Set-Cookie"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"include","value":"cookie1=value1; Path=/"}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"not":true,"type":"include","value":"cookie4=value4; Path=/"}}]}}]}
+{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/assert-header"},"response":{"status":200,"headers":[{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"}],"asserts":[{"query":{"type":"header","name":"Custom"},"predicate":{"not":true,"type":"exist"}},{"query":{"type":"header","name":"Content-Type"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Header1"},"predicate":{"type":"equal","value":"value1"}},{"query":{"type":"header","name":"ETag"},"predicate":{"type":"equal","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""}},{"query":{"type":"header","name":"Expires"},"predicate":{"type":"equal","value":"Wed, 21 Oct 2015 07:28:00 GMT"}},{"query":{"type":"header","name":"Expires"},"filters":[{"type":"toDate","fmt":"%a, %d %b %Y %H:%M:%S GMT"},{"type":"format","fmt":"%Y"}],"predicate":{"type":"equal","value":"2015"}},{"query":{"type":"header","name":"Expires"},"filters":[{"type":"toDate","fmt":"%a, %d %b %Y %H:%M:%S GMT"},{"type":"daysBeforeNow"}],"predicate":{"type":"greater","value":1000}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"exist"}},{"query":{"type":"header","name":"Set-Cookie"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":3}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"type":"include","value":"cookie1=value1; Path=/"}},{"query":{"type":"header","name":"Set-Cookie"},"predicate":{"not":true,"type":"include","value":"cookie4=value4; Path=/"}}]}}]}
diff --git a/integration/tests_ok/assert_header.out.pattern b/integration/tests_ok/assert_header.out.pattern
index e34602609..fac28e14b 100644
--- a/integration/tests_ok/assert_header.out.pattern
+++ b/integration/tests_ok/assert_header.out.pattern
@@ -1 +1 @@
-{"cookies":[{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie1","path":"/","value":"value1"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie2","path":"/","value":"value2"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie3","path":"/","value":"value3"}],"entries":[{"asserts":[{"line":3,"success":true},{"line":3,"success":true},{"line":4,"success":true},{"line":5,"success":true},{"line":6,"success":true},{"line":8,"success":true},{"line":9,"success":true},{"line":10,"success":true},{"line":11,"success":true},{"line":12,"success":true},{"line":13,"success":true},{"line":14,"success":true},{"line":15,"success":true},{"line":16,"success":true},{"line":17,"success":true}],"calls":[{"request":{"cookies":[],"headers":[{"name":"Host","value":"localhost:8000"},{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"hurl/~~~"}],"method":"GET","queryString":[],"url":"http://localhost:8000/assert-header"},"response":{"cookies":[{"name":"cookie1","path":"/","value":"value1"},{"name":"cookie2","path":"/","value":"value2"},{"name":"cookie3","path":"/","value":"value3"}],"headers":[{"name":"Server","value":"Werkzeug/~~~ Python/~~~"},{"name":"Date","value":"~~~"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Header1","value":"value1"},{"name":"ETag","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""},{"name":"Expires","value":"~~~"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"},{"name":"Set-Cookie","value":"cookie3=value3; Path=/"},{"name":"Server","value":"Flask Server"},{"name":"Content-Length","value":"0"},{"name":"Connection","value":"close"}],"httpVersion":"HTTP/1.1","status":200},"timings":{"app_connect":~~~,"begin_call":"~~~","connect":~~~,"end_call":"~~~","name_lookup":~~~,"pre_transfert":~~~,"start_transfert":~~~,"total":~~~}}],"captures":[],"index":1,"time":~~~}],"filename":"tests_ok/assert_header.hurl","success":true,"time":~~~}
+{"cookies":[{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie1","path":"/","value":"value1"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie2","path":"/","value":"value2"},{"domain":"localhost","expires":"0","https":"FALSE","include_subdomain":"FALSE","name":"cookie3","path":"/","value":"value3"}],"entries":[{"asserts":[{"line":3,"success":true},{"line":3,"success":true},{"line":4,"success":true},{"line":5,"success":true},{"line":6,"success":true},{"line":8,"success":true},{"line":9,"success":true},{"line":10,"success":true},{"line":11,"success":true},{"line":12,"success":true},{"line":13,"success":true},{"line":14,"success":true},{"line":15,"success":true},{"line":16,"success":true},{"line":17,"success":true},{"line":18,"success":true}],"calls":[{"request":{"cookies":[],"headers":[{"name":"Host","value":"localhost:8000"},{"name":"Accept","value":"*/*"},{"name":"User-Agent","value":"hurl/~~~"}],"method":"GET","queryString":[],"url":"http://localhost:8000/assert-header"},"response":{"cookies":[{"name":"cookie1","path":"/","value":"value1"},{"name":"cookie2","path":"/","value":"value2"},{"name":"cookie3","path":"/","value":"value3"}],"headers":[{"name":"Server","value":"Werkzeug/~~~ Python/~~~"},{"name":"Date","value":"~~~"},{"name":"Content-Type","value":"text/html; charset=utf-8"},{"name":"Header1","value":"value1"},{"name":"ETag","value":"\"33a64df551425fcc55e4d42a148795d9f25f89d4\""},{"name":"Expires","value":"~~~"},{"name":"Set-Cookie","value":"cookie1=value1; Path=/"},{"name":"Set-Cookie","value":"cookie2=value2; Path=/"},{"name":"Set-Cookie","value":"cookie3=value3; Path=/"},{"name":"Server","value":"Flask Server"},{"name":"Content-Length","value":"0"},{"name":"Connection","value":"close"}],"httpVersion":"HTTP/1.1","status":200},"timings":{"app_connect":~~~,"begin_call":"~~~","connect":~~~,"end_call":"~~~","name_lookup":~~~,"pre_transfert":~~~,"start_transfert":~~~,"total":~~~}}],"captures":[],"index":1,"time":~~~}],"filename":"tests_ok/assert_header.hurl","success":true,"time":~~~}
diff --git a/packages/hurl/src/runner/filter.rs b/packages/hurl/src/runner/filter.rs
index d6d302865..c1cb33839 100644
--- a/packages/hurl/src/runner/filter.rs
+++ b/packages/hurl/src/runner/filter.rs
@@ -17,7 +17,7 @@
*/
use std::collections::HashMap;
-use chrono::NaiveDateTime;
+use chrono::{NaiveDateTime, Utc};
use hurl_core::ast::{Filter, FilterValue, RegexValue, SourceInfo, Template};
use percent_encoding::AsciiSet;
@@ -49,6 +49,8 @@ fn eval_filter(
) -> Result {
match &filter.value {
FilterValue::Count => eval_count(value, &filter.source_info, in_assert),
+ FilterValue::DaysAfterNow => eval_days_after_now(value, &filter.source_info, in_assert),
+ FilterValue::DaysBeforeNow => eval_days_before_now(value, &filter.source_info, in_assert),
FilterValue::Format { fmt, .. } => {
eval_format(value, fmt, variables, &filter.source_info, in_assert)
}
@@ -133,6 +135,42 @@ fn eval_count(value: &Value, source_info: &SourceInfo, assert: bool) -> Result Result {
+ match value {
+ Value::Date(value) => {
+ let diff = value.signed_duration_since(Utc::now());
+ Ok(Value::Integer(diff.num_days()))
+ }
+ v => Err(Error {
+ source_info: source_info.clone(),
+ inner: RunnerError::FilterInvalidInput(v._type()),
+ assert,
+ }),
+ }
+}
+
+fn eval_days_before_now(
+ value: &Value,
+ source_info: &SourceInfo,
+ assert: bool,
+) -> Result {
+ match value {
+ Value::Date(value) => {
+ let diff = Utc::now().signed_duration_since(*value);
+ Ok(Value::Integer(diff.num_days()))
+ }
+ v => Err(Error {
+ source_info: source_info.clone(),
+ inner: RunnerError::FilterInvalidInput(v._type()),
+ assert,
+ }),
+ }
+}
+
fn eval_format(
value: &Value,
fmt: &Template,
@@ -347,6 +385,7 @@ fn eval_to_int(value: &Value, source_info: &SourceInfo, assert: bool) -> Result<
pub mod tests {
use chrono::offset::Utc;
use chrono::prelude::*;
+ use chrono::Duration;
use hurl_core::ast::{FilterValue, SourceInfo, Template, TemplateElement, Whitespace};
use super::*;
@@ -407,6 +446,54 @@ pub mod tests {
);
}
+ #[test]
+ pub fn eval_filter_days_after_before_now() {
+ let variables = HashMap::new();
+
+ let now = Utc::now();
+ assert_eq!(
+ eval_filter(
+ &Filter {
+ source_info: SourceInfo::new(1, 1, 1, 1),
+ value: FilterValue::DaysAfterNow,
+ },
+ &Value::Date(now),
+ &variables,
+ false,
+ )
+ .unwrap(),
+ Value::Integer(0)
+ );
+
+ let now_plus_30hours = now + Duration::hours(30);
+ assert_eq!(
+ eval_filter(
+ &Filter {
+ source_info: SourceInfo::new(1, 1, 1, 1),
+ value: FilterValue::DaysAfterNow,
+ },
+ &Value::Date(now_plus_30hours),
+ &variables,
+ false,
+ )
+ .unwrap(),
+ Value::Integer(1)
+ );
+ assert_eq!(
+ eval_filter(
+ &Filter {
+ source_info: SourceInfo::new(1, 1, 1, 1),
+ value: FilterValue::DaysBeforeNow,
+ },
+ &Value::Date(now_plus_30hours),
+ &variables,
+ false,
+ )
+ .unwrap(),
+ Value::Integer(-1)
+ );
+ }
+
#[test]
pub fn eval_filter_format() {
// let naivedatetime_utc = NaiveDate::from_ymd_opt(2000, 1, 12).unwrap().and_hms_opt(2, 0, 0).unwrap();
diff --git a/packages/hurl_core/src/ast/core.rs b/packages/hurl_core/src/ast/core.rs
index 4b505a96c..cf6167262 100644
--- a/packages/hurl_core/src/ast/core.rs
+++ b/packages/hurl_core/src/ast/core.rs
@@ -887,6 +887,8 @@ pub struct Filter {
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FilterValue {
Count,
+ DaysAfterNow,
+ DaysBeforeNow,
Format {
space0: Whitespace,
fmt: Template,
diff --git a/packages/hurl_core/src/format/html.rs b/packages/hurl_core/src/format/html.rs
index a5c4cf146..2ebc8fac7 100644
--- a/packages/hurl_core/src/format/html.rs
+++ b/packages/hurl_core/src/format/html.rs
@@ -1148,6 +1148,12 @@ impl Htmlable for FilterValue {
fn to_html(&self) -> String {
match self {
FilterValue::Count => "count".to_string(),
+ FilterValue::DaysAfterNow => {
+ "daysAfterNow".to_string()
+ }
+ FilterValue::DaysBeforeNow => {
+ "daysBeforeNow".to_string()
+ }
FilterValue::Format { space0, fmt } => {
let mut buffer = "format".to_string();
buffer.push_str(space0.to_html().as_str());
diff --git a/packages/hurl_core/src/parser/filter.rs b/packages/hurl_core/src/parser/filter.rs
index b15ef3012..701c465af 100644
--- a/packages/hurl_core/src/parser/filter.rs
+++ b/packages/hurl_core/src/parser/filter.rs
@@ -52,6 +52,8 @@ pub fn filter(reader: &mut Reader) -> ParseResult<'static, Filter> {
let value = choice(
&[
count_filter,
+ days_after_now_filter,
+ days_before_now_filter,
format_filter,
html_decode_filter,
html_encode_filter,
@@ -89,6 +91,16 @@ fn count_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
Ok(FilterValue::Count)
}
+fn days_after_now_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
+ try_literal("daysAfterNow", reader)?;
+ Ok(FilterValue::DaysAfterNow)
+}
+
+fn days_before_now_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
+ try_literal("daysBeforeNow", reader)?;
+ Ok(FilterValue::DaysBeforeNow)
+}
+
fn format_filter(reader: &mut Reader) -> ParseResult<'static, FilterValue> {
try_literal("format", reader)?;
let space0 = one_or_more_spaces(reader)?;
diff --git a/packages/hurlfmt/src/format/json.rs b/packages/hurlfmt/src/format/json.rs
index d351efea7..03f34218b 100644
--- a/packages/hurlfmt/src/format/json.rs
+++ b/packages/hurlfmt/src/format/json.rs
@@ -553,6 +553,18 @@ impl ToJson for FilterValue {
FilterValue::Count => {
attributes.push(("type".to_string(), JValue::String("count".to_string())));
}
+ FilterValue::DaysAfterNow => {
+ attributes.push((
+ "type".to_string(),
+ JValue::String("daysAfterNow".to_string()),
+ ));
+ }
+ FilterValue::DaysBeforeNow => {
+ attributes.push((
+ "type".to_string(),
+ JValue::String("daysBeforeNow".to_string()),
+ ));
+ }
FilterValue::Format { fmt, .. } => {
attributes.push(("type".to_string(), JValue::String("format".to_string())));
attributes.push(("fmt".to_string(), JValue::String(fmt.to_string())));
diff --git a/packages/hurlfmt/src/format/token.rs b/packages/hurlfmt/src/format/token.rs
index f0d956cb4..bf177ac7f 100644
--- a/packages/hurlfmt/src/format/token.rs
+++ b/packages/hurlfmt/src/format/token.rs
@@ -1167,6 +1167,8 @@ impl Tokenizable for Filter {
fn tokenize(&self) -> Vec {
match self.value.clone() {
FilterValue::Count => vec![Token::FilterType(String::from("count"))],
+ FilterValue::DaysAfterNow => vec![Token::FilterType(String::from("daysAfterNow"))],
+ FilterValue::DaysBeforeNow => vec![Token::FilterType(String::from("daysBeforeNow"))],
FilterValue::Format { space0, fmt } => {
let mut tokens: Vec = vec![Token::FilterType(String::from("format"))];
tokens.append(&mut space0.tokenize());