diff --git a/docs/filters.md b/docs/filters.md index 417e62bd9..95ff8182d 100644 --- a/docs/filters.md +++ b/docs/filters.md @@ -238,6 +238,17 @@ HTTP 200 jsonpath "$.id" toInt == 123 ``` +### toFloat + +Converts to float number. + +```hurl +GET https://example.org/foo +HTTP 200 +[Asserts] +jsonpath "$.pi" toFloat == 3.14 +``` + ### urlDecode Replaces %xx escapes with their single-character equivalent. diff --git a/docs/grammar.md b/docs/grammar.md index b73c8cab3..d98f7b47d 100644 --- a/docs/grammar.md +++ b/docs/grammar.md @@ -293,6 +293,7 @@ Short description:
split-filter(used by filter)
split sp quoted-string
to-date-filter(used by filter)
toDate
to-int-filter(used by filter)
toInt
+
to-float-filter(used by filter)
toFloat
url-decode-filter(used by filter)
urlDecode
url-encode-filter(used by filter)
urlEncode
xpath-filter(used by filter)
xpath sp quoted-string
diff --git a/docs/spec/grammar/hurl.grammar b/docs/spec/grammar/hurl.grammar index 789076adc..71ab63fb1 100644 --- a/docs/spec/grammar/hurl.grammar +++ b/docs/spec/grammar/hurl.grammar @@ -539,6 +539,8 @@ to-date-filter: "toDate" to-int-filter: "toInt" +to-float-filter: "toFloat" + url-decode-filter: "urlDecode" url-encode-filter: "urlEncode" diff --git a/integration/hurlfmt/tests_export/filter.html b/integration/hurlfmt/tests_export/filter.html index b2a1069b3..0334b9cf7 100644 --- a/integration/hurlfmt/tests_export/filter.html +++ b/integration/hurlfmt/tests_export/filter.html @@ -18,6 +18,7 @@ jsonpath "$.ips" split ", " count == 3 # split header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000 # toDate jsonpath "$.id" toInt == 123 # toInt +jsonpath "$.pi" toFloat == 3.14 # toFloat jsonpath "$.encoded_url" urlDecode == "https://mozilla.org/?x=шеллы" # urlDecode jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" # urlEncode bytes decode "iso-8859-1" xpath "string(//p)" == "Hello" # xpath diff --git a/integration/hurlfmt/tests_export/filter.hurl b/integration/hurlfmt/tests_export/filter.hurl index 5ab09d8d9..7c0c0b9e9 100644 --- a/integration/hurlfmt/tests_export/filter.hurl +++ b/integration/hurlfmt/tests_export/filter.hurl @@ -18,6 +18,7 @@ jsonpath "$.ips" replace ", " "|" == "192.168.2.1|10.0.0.20|10.0.0.10" jsonpath "$.ips" split ", " count == 3 # split header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000 # toDate jsonpath "$.id" toInt == 123 # toInt +jsonpath "$.pi" toInt == 3.14 # toFloat jsonpath "$.encoded_url" urlDecode == "https://mozilla.org/?x=шеллы" # urlDecode jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" # urlEncode bytes decode "iso-8859-1" xpath "string(//p)" == "Hello" # xpath diff --git a/integration/hurlfmt/tests_export/filter.json b/integration/hurlfmt/tests_export/filter.json index 3736804d0..44110328c 100644 --- a/integration/hurlfmt/tests_export/filter.json +++ b/integration/hurlfmt/tests_export/filter.json @@ -1 +1 @@ -{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/dummy"},"response":{"status":200,"captures":[{"name":"count","query":{"type":"jsonpath","expr":"$.books"},"filters":[{"type":"count"}]}],"asserts":[{"query":{"type":"jsonpath","expr":"$.books"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":12}},{"query":{"type":"certificate","expr":"Expire-Date"},"filters":[{"type":"daysAfterNow"}],"predicate":{"type":"greater","value":15}},{"query":{"type":"certificate","expr":"Start-Date"},"filters":[{"type":"daysBeforeNow"}],"predicate":{"type":"less","value":100}},{"query":{"type":"bytes"},"filters":[{"type":"decode","encoding":"iso-8859-1"}],"predicate":{"type":"equal","value":"café"}},{"query":{"type":"cookie","expr":"LSID[Expires]"},"filters":[{"type":"format","fmt":"%a, %d %b %Y %H:%M:%S"}],"predicate":{"type":"equal","value":"Wed, 13 Jan 2021 22:23:01"}},{"query":{"type":"jsonpath","expr":"$.text"},"filters":[{"type":"htmlEscape"}],"predicate":{"type":"equal","value":"a > b"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[1]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"

Hello

"}},{"query":{"type":"variable","name":"books"},"filters":[{"type":"jsonpath","expr":"$[0].name"}],"predicate":{"type":"equal","value":"Dune"}},{"query":{"type":"jsonpath","expr":"$.books"},"filters":[{"type":"nth","n":2}],"predicate":{"type":"equal","value":"Children of Dune"}},{"query":{"type":"body"},"filters":[{"type":"regex","expr":{"type":"regex","value":"Hello ([0-9]+)!"}}],"predicate":{"type":"equal","value":"Bob"}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"replace","old_value":", ","new_value":"|"}],"predicate":{"type":"equal","value":"192.168.2.1|10.0.0.20|10.0.0.10"}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"split","sep":", "},{"type":"count"}],"predicate":{"type":"equal","value":3}},{"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":"jsonpath","expr":"$.id"},"filters":[{"type":"toInt"}],"predicate":{"type":"equal","value":123}},{"query":{"type":"jsonpath","expr":"$.encoded_url"},"filters":[{"type":"urlDecode"}],"predicate":{"type":"equal","value":"https://mozilla.org/?x=шеллы"}},{"query":{"type":"jsonpath","expr":"$.url"},"filters":[{"type":"urlEncode"}],"predicate":{"type":"equal","value":"https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"}},{"query":{"type":"bytes"},"filters":[{"type":"decode","encoding":"iso-8859-1"},{"type":"xpath","expr":"string(//p)"}],"predicate":{"type":"equal","value":"Hello"}}]}}]} +{"entries":[{"request":{"method":"GET","url":"http://localhost:8000/dummy"},"response":{"status":200,"captures":[{"name":"count","query":{"type":"jsonpath","expr":"$.books"},"filters":[{"type":"count"}]}],"asserts":[{"query":{"type":"jsonpath","expr":"$.books"},"filters":[{"type":"count"}],"predicate":{"type":"equal","value":12}},{"query":{"type":"certificate","expr":"Expire-Date"},"filters":[{"type":"daysAfterNow"}],"predicate":{"type":"greater","value":15}},{"query":{"type":"certificate","expr":"Start-Date"},"filters":[{"type":"daysBeforeNow"}],"predicate":{"type":"less","value":100}},{"query":{"type":"bytes"},"filters":[{"type":"decode","encoding":"iso-8859-1"}],"predicate":{"type":"equal","value":"café"}},{"query":{"type":"cookie","expr":"LSID[Expires]"},"filters":[{"type":"format","fmt":"%a, %d %b %Y %H:%M:%S"}],"predicate":{"type":"equal","value":"Wed, 13 Jan 2021 22:23:01"}},{"query":{"type":"jsonpath","expr":"$.text"},"filters":[{"type":"htmlEscape"}],"predicate":{"type":"equal","value":"a > b"}},{"query":{"type":"jsonpath","expr":"$.escaped_html[1]"},"filters":[{"type":"htmlUnescape"}],"predicate":{"type":"equal","value":"

Hello

"}},{"query":{"type":"variable","name":"books"},"filters":[{"type":"jsonpath","expr":"$[0].name"}],"predicate":{"type":"equal","value":"Dune"}},{"query":{"type":"jsonpath","expr":"$.books"},"filters":[{"type":"nth","n":2}],"predicate":{"type":"equal","value":"Children of Dune"}},{"query":{"type":"body"},"filters":[{"type":"regex","expr":{"type":"regex","value":"Hello ([0-9]+)!"}}],"predicate":{"type":"equal","value":"Bob"}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"replace","old_value":", ","new_value":"|"}],"predicate":{"type":"equal","value":"192.168.2.1|10.0.0.20|10.0.0.10"}},{"query":{"type":"jsonpath","expr":"$.ips"},"filters":[{"type":"split","sep":", "},{"type":"count"}],"predicate":{"type":"equal","value":3}},{"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":"jsonpath","expr":"$.id"},"filters":[{"type":"toInt"}],"predicate":{"type":"equal","value":123}},{"query":{"type":"jsonpath","expr":"$.pi"},"filters":[{"type": "toFloat"}],"predicate":{"type":"equal","value":3.14}},{"query":{"type":"jsonpath","expr":"$.encoded_url"},"filters":[{"type":"urlDecode"}],"predicate":{"type":"equal","value":"https://mozilla.org/?x=шеллы"}},{"query":{"type":"jsonpath","expr":"$.url"},"filters":[{"type":"urlEncode"}],"predicate":{"type":"equal","value":"https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"}},{"query":{"type":"bytes"},"filters":[{"type":"decode","encoding":"iso-8859-1"},{"type":"xpath","expr":"string(//p)"}],"predicate":{"type":"equal","value":"Hello"}}]}}]} diff --git a/integration/hurlfmt/tests_export/filter.lint.hurl b/integration/hurlfmt/tests_export/filter.lint.hurl index 5ab09d8d9..cab4f0e42 100644 --- a/integration/hurlfmt/tests_export/filter.lint.hurl +++ b/integration/hurlfmt/tests_export/filter.lint.hurl @@ -18,6 +18,7 @@ jsonpath "$.ips" replace ", " "|" == "192.168.2.1|10.0.0.20|10.0.0.10" jsonpath "$.ips" split ", " count == 3 # split header "Expires" toDate "%a, %d %b %Y %H:%M:%S GMT" daysBeforeNow > 1000 # toDate jsonpath "$.id" toInt == 123 # toInt +jsonpath "$.pi" toFloat == 3.14 # toFloat jsonpath "$.encoded_url" urlDecode == "https://mozilla.org/?x=шеллы" # urlDecode jsonpath "$.url" urlEncode == "https%3A//mozilla.org/%3Fx%3D%D1%88%D0%B5%D0%BB%D0%BB%D1%8B" # urlEncode bytes decode "iso-8859-1" xpath "string(//p)" == "Hello" # xpath diff --git a/packages/hurl/src/runner/filter/eval.rs b/packages/hurl/src/runner/filter/eval.rs index 5ea117481..22f9e5595 100644 --- a/packages/hurl/src/runner/filter/eval.rs +++ b/packages/hurl/src/runner/filter/eval.rs @@ -33,6 +33,7 @@ use crate::runner::filter::replace::eval_replace; use crate::runner::filter::split::eval_split; use crate::runner::filter::to_date::eval_to_date; use crate::runner::filter::to_int::eval_to_int; +use crate::runner::filter::to_float::eval_to_float; use crate::runner::filter::url_decode::eval_url_decode; use crate::runner::filter::url_encode::eval_url_encode; use crate::runner::filter::xpath::eval_xpath; @@ -108,6 +109,7 @@ pub fn eval_filter( eval_to_date(value, fmt, variables, filter.source_info, in_assert) } FilterValue::ToInt => eval_to_int(value, filter.source_info, in_assert), + FilterValue::ToFloat => eval_to_float(value, filter.source_info, in_assert), FilterValue::UrlDecode => eval_url_decode(value, filter.source_info, in_assert), FilterValue::UrlEncode => eval_url_encode(value, filter.source_info, in_assert), FilterValue::XPath { expr, .. } => { diff --git a/packages/hurl/src/runner/filter/mod.rs b/packages/hurl/src/runner/filter/mod.rs index a3124decd..3dbcdb11c 100644 --- a/packages/hurl/src/runner/filter/mod.rs +++ b/packages/hurl/src/runner/filter/mod.rs @@ -38,3 +38,4 @@ mod to_int; mod url_decode; mod url_encode; mod xpath; +mod to_float; diff --git a/packages/hurl/src/runner/filter/to_float.rs b/packages/hurl/src/runner/filter/to_float.rs new file mode 100644 index 000000000..1f9d234ef --- /dev/null +++ b/packages/hurl/src/runner/filter/to_float.rs @@ -0,0 +1,137 @@ +/* + * Hurl (https://hurl.dev) + * Copyright (C) 2024 Orange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +use hurl_core::ast::SourceInfo; + +use crate::runner::{Error, Number, RunnerError, Value}; + + pub fn eval_to_float( + value: &Value, + source_info: SourceInfo, + assert: bool, + ) -> Result, Error> { + match value { + Value::Number(Number::Float(v)) => Ok(Some(Value::Number(Number::Float(*v)))), + Value::Number(Number::Integer(v)) => Ok(Some(Value::Number(Number::Float(*v as f64)))), + Value::String(v) => match v.parse::() { + Ok(f) => Ok(Some(Value::Number(Number::Float(f)))), + _ => { + let inner = RunnerError::FilterInvalidInput(value.display()); + Err(Error::new(source_info, inner, assert)) + } + }, + v => { + let inner = RunnerError::FilterInvalidInput(v.display()); + Err(Error::new(source_info, inner, assert)) + } + } + } + + #[cfg(test)] + pub mod tests { + + use crate::runner::filter::eval::eval_filter; + use crate::runner::{Number, RunnerError, Value}; + use hurl_core::ast::{Filter, FilterValue, Pos, SourceInfo}; + use std::collections::HashMap; + + #[test] + pub fn eval_filter_to_float() { + let variable = HashMap::new(); + let filter = Filter { + source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 1)), + value: FilterValue::ToFloat, + }; + assert_eq!( + eval_filter( + &filter, + &Value::String("3.1415".to_string()), + &variable, + false + ) + .unwrap() + .unwrap(), + Value::Number(Number::Float(3.1415)) + ); + assert_eq!( + eval_filter( + &filter, + &Value::Number(Number::Float(3.1415)), + &variable, + false + ) + .unwrap() + .unwrap(), + Value::Number(Number::Float(3.1415)) + ); + assert_eq!( + eval_filter( + &filter, + &Value::Number(Number::Float(3.0)), + &variable, + false + ) + .unwrap() + .unwrap(), + Value::Number(Number::Float(3.0)) + ); + assert_eq!( + eval_filter( + &filter, + &Value::Number(Number::Integer(3)), + &variable, + false + ) + .unwrap() + .unwrap(), + Value::Number(Number::Float(3.0)) + ); + } + + #[test] + pub fn eval_filter_to_float_error() { + let variable = HashMap::new(); + let filter = Filter { + source_info: SourceInfo::new(Pos::new(1, 1), Pos::new(1, 1)), + value: FilterValue::ToFloat, + }; + let err = eval_filter( + &filter, + &Value::String("3x.1415".to_string()), + &variable, + false, + ) + .err() + .unwrap(); + assert_eq!( + err.inner, + RunnerError::FilterInvalidInput("string <3x.1415>".to_string()) + ); + let err = eval_filter( + &filter, + &Value::Bool(true), + &variable, + false + ) + .err() + .unwrap(); + assert_eq!( + err.inner, + RunnerError::FilterInvalidInput("bool ".to_string()) + ); + } +} \ No newline at end of file diff --git a/packages/hurl_core/src/ast/core.rs b/packages/hurl_core/src/ast/core.rs index b1c01b49c..65b897a23 100644 --- a/packages/hurl_core/src/ast/core.rs +++ b/packages/hurl_core/src/ast/core.rs @@ -907,6 +907,7 @@ pub enum FilterValue { fmt: Template, }, ToInt, + ToFloat, UrlDecode, UrlEncode, XPath { diff --git a/packages/hurl_core/src/format/html.rs b/packages/hurl_core/src/format/html.rs index 3f343d316..0dff0375b 100644 --- a/packages/hurl_core/src/format/html.rs +++ b/packages/hurl_core/src/format/html.rs @@ -829,6 +829,7 @@ impl HtmlFormatter { self.fmt_template(fmt); } FilterValue::ToInt => self.fmt_span("filter-type", "toInt"), + FilterValue::ToFloat => self.fmt_span("filter-type", "toFloat"), FilterValue::UrlDecode => self.fmt_span("filter-type", "urlDecode"), FilterValue::UrlEncode => self.fmt_span("filter-type", "urlEncode"), FilterValue::XPath { space0, expr } => { diff --git a/packages/hurl_core/src/parser/filter.rs b/packages/hurl_core/src/parser/filter.rs index 2e351e9dc..a56abe3bb 100644 --- a/packages/hurl_core/src/parser/filter.rs +++ b/packages/hurl_core/src/parser/filter.rs @@ -65,6 +65,7 @@ pub fn filter(reader: &mut Reader) -> ParseResult { replace_filter, split_filter, to_int_filter, + to_float_filter, to_date_filter, url_decode_filter, url_encode_filter, @@ -180,6 +181,11 @@ fn to_int_filter(reader: &mut Reader) -> ParseResult { Ok(FilterValue::ToInt) } +fn to_float_filter(reader: &mut Reader) -> ParseResult { + try_literal("toFloat", reader)?; + Ok(FilterValue::ToFloat) +} + fn url_encode_filter(reader: &mut Reader) -> ParseResult { try_literal("urlEncode", reader)?; Ok(FilterValue::UrlEncode) diff --git a/packages/hurlfmt/src/format/json.rs b/packages/hurlfmt/src/format/json.rs index 3c988434b..0f8a6cd82 100644 --- a/packages/hurlfmt/src/format/json.rs +++ b/packages/hurlfmt/src/format/json.rs @@ -721,6 +721,9 @@ impl ToJson for FilterValue { FilterValue::ToInt => { attributes.push(("type".to_string(), JValue::String("toInt".to_string()))); } + FilterValue::ToFloat => { + attributes.push(("type".to_string(), JValue::String("toFloat".to_string()))); + } FilterValue::XPath { expr, .. } => { attributes.push(("type".to_string(), JValue::String("xpath".to_string()))); attributes.push(("expr".to_string(), JValue::String(expr.to_string()))); diff --git a/packages/hurlfmt/src/format/token.rs b/packages/hurlfmt/src/format/token.rs index 9e2a50c8a..16e594823 100644 --- a/packages/hurlfmt/src/format/token.rs +++ b/packages/hurlfmt/src/format/token.rs @@ -1042,6 +1042,7 @@ impl Tokenizable for Filter { tokens } FilterValue::ToInt => vec![Token::FilterType(String::from("toInt"))], + FilterValue::ToFloat => vec![Token::FilterType(String::from("toFloat"))], FilterValue::XPath { space0, expr } => { let mut tokens: Vec = vec![Token::FilterType(String::from("xpath"))]; tokens.append(&mut space0.tokenize());