Add JSONPath functions

This commit is contained in:
Fabrice Reix 2025-11-30 17:42:46 +01:00
parent ea87045f7c
commit 6b1ff56bc1
No known key found for this signature in database
GPG Key ID: 71B8BAD935E3190A
17 changed files with 817 additions and 1 deletions

View File

@ -16,6 +16,7 @@
*
*/
use crate::jsonpath2::ast::function::functions::ValueTypeFunction;
use crate::jsonpath2::ast::literal::Literal;
use crate::jsonpath2::ast::singular_query::SingularQuery;
@ -52,6 +53,7 @@ impl ComparisonExpr {
pub enum Comparable {
Literal(Literal),
SingularQuery(SingularQuery),
Function(ValueTypeFunction),
}
#[allow(dead_code)]

View File

@ -0,0 +1,48 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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 super::functions::LogicalTypeFunction;
use super::functions::ValueTypeFunction;
use crate::jsonpath2::ast::expr::LogicalExpr;
use crate::jsonpath2::ast::literal::Literal;
use crate::jsonpath2::ast::query::Query;
use crate::jsonpath2::ast::singular_query::SingularQuery;
/// Arguments with ValueType
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)]
pub enum ValueTypeArgument {
Literal(Literal),
SingularQuery(SingularQuery),
Function(Box<ValueTypeFunction>),
}
/// Arguments with LogicalType
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LogicalTypeArgument {
LogicalExpr(LogicalExpr),
LogicalTypeFunction(Box<LogicalTypeFunction>),
}
/// Arguments with NodesType
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum NodesTypeArgument {
FilterQuery(Query),
}

View File

@ -0,0 +1,36 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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 super::argument::{NodesTypeArgument, ValueTypeArgument};
/// Functions returning a LogicalType
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LogicalTypeFunction {
Match(ValueTypeArgument, ValueTypeArgument),
Search(ValueTypeArgument, ValueTypeArgument),
}
/// Functions returning a ValueType
#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ValueTypeFunction {
Length(ValueTypeArgument),
Count(NodesTypeArgument),
Value(NodesTypeArgument),
}

View File

@ -0,0 +1,30 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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.
*
*/
/// Function extensions:
/// <https://www.rfc-editor.org/rfc/rfc9535.html#name-function-extensions>
/// This module implements the functions defined in the RFC.
/// It defines the function types and their arguments.
pub mod argument;
pub mod functions;
use crate::jsonpath2::eval::NodeList;
pub type ValueType = Option<serde_json::Value>;
pub type LogicalType = bool;
pub type NodesType = NodeList;

View File

@ -18,6 +18,7 @@
pub(crate) mod comparison;
pub(crate) mod expr;
pub(crate) mod function;
pub(crate) mod literal;
pub(crate) mod query;
pub(crate) mod segment;

View File

@ -88,6 +88,9 @@ impl Comparable {
Comparable::SingularQuery(singular_query) => {
singular_query.eval(current_value, root_value)
}
Comparable::Function(value_type_function) => {
value_type_function.eval(current_value, root_value)
}
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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::jsonpath2::ast::function::argument::{NodesTypeArgument, ValueTypeArgument};
use crate::jsonpath2::ast::function::{NodesType, ValueType};
impl ValueTypeArgument {
pub fn eval(
&self,
current_value: &serde_json::Value,
root_value: &serde_json::Value,
) -> ValueType {
match self {
ValueTypeArgument::SingularQuery(filter_query) => {
filter_query.eval(current_value, root_value)
}
ValueTypeArgument::Function(value_type_function) => {
value_type_function.eval(current_value, root_value)
}
ValueTypeArgument::Literal(value) => Some(value.eval()),
}
}
}
impl NodesTypeArgument {
pub fn eval(
&self,
current_value: &serde_json::Value,
root_value: &serde_json::Value,
) -> NodesType {
match self {
NodesTypeArgument::FilterQuery(query) => query.eval(current_value, root_value),
}
}
}
#[cfg(test)]
mod tests {
use crate::jsonpath2::ast::literal::{Literal, Number};
use crate::jsonpath2::ast::query::{Query, RelativeQuery};
use crate::jsonpath2::ast::segment::{ChildSegment, Segment};
use crate::jsonpath2::ast::selector::{NameSelector, Selector};
use crate::jsonpath2::ast::singular_query::{
RelativeSingularQuery, SingularQuery, SingularQuerySegment,
};
use crate::jsonpath2::eval::function::argument::{NodesTypeArgument, ValueTypeArgument};
use serde_json::json;
#[test]
fn test_value_type_argument() {
// "hello"
let argument = ValueTypeArgument::Literal(Literal::String("hello".to_string()));
assert_eq!(argument.eval(&json!({}), &json!({})), Some(json!("hello")));
// 1
let argument = ValueTypeArgument::Literal(Literal::Number(Number::Integer(1)));
assert_eq!(argument.eval(&json!({}), &json!({})), Some(json!(1)));
// @.authors
let argument = ValueTypeArgument::SingularQuery(SingularQuery::Relative(
RelativeSingularQuery::new(vec![SingularQuerySegment::Name(NameSelector::new(
"authors".to_string(),
))]),
));
assert_eq!(
argument.eval(&json!({"authors": ["a", "b", "c"]}), &json!({})),
Some(json!(["a", "b", "c"]))
);
}
#[test]
fn test_nodes_type_argument() {
// @.books
let argument =
NodesTypeArgument::FilterQuery(Query::RelativeQuery(RelativeQuery::new(vec![
Segment::Child(ChildSegment::new(vec![Selector::Name(NameSelector::new(
"books".to_string(),
))])),
])));
assert_eq!(
argument.eval(&json!({"books": ["book1", "book2"]}), &json!({})),
vec![json!(["book1", "book2"])]
);
assert!(argument.eval(&json!({}), &json!({})).is_empty());
}
}

View File

@ -0,0 +1,251 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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 regex::Regex;
use serde_json::Value;
use crate::jsonpath2::ast::function::functions::{LogicalTypeFunction, ValueTypeFunction};
use crate::jsonpath2::ast::function::LogicalType;
impl ValueTypeFunction {
#[allow(dead_code)]
pub fn eval(
&self,
current_value: &serde_json::Value,
root_value: &serde_json::Value,
) -> Option<serde_json::Value> {
match self {
ValueTypeFunction::Length(argument) => {
let argument = argument.eval(current_value, root_value)?;
length(&argument).map(|n| {
let number = serde_json::Number::from_i128(n as i128).unwrap();
serde_json::Value::Number(number)
})
}
ValueTypeFunction::Count(argument) => {
let nodelist = argument.eval(current_value, root_value);
Some(serde_json::Value::Number(serde_json::Number::from(
nodelist.len(),
)))
}
ValueTypeFunction::Value(argument) => {
let nodelist = argument.eval(current_value, root_value);
if nodelist.len() == 1 {
Some(nodelist[0].clone())
} else {
None
}
}
}
}
}
impl LogicalTypeFunction {
#[allow(dead_code)]
pub fn eval(
&self,
current_value: &serde_json::Value,
root_value: &serde_json::Value,
) -> LogicalType {
match self {
LogicalTypeFunction::Match(string_argument, pattern_argument) => {
let string = string_argument.eval(current_value, root_value);
let pattern = pattern_argument.eval(current_value, root_value);
let s = if let Some(Value::String(value)) = string {
value
} else {
return false;
};
let p = if let Some(Value::String(value)) = pattern {
value
} else {
return false;
};
// use total match
let p = format!("^{}$", p);
if let Ok(regex) = Regex::new(&p) {
regex.is_match(&s)
} else {
false
}
}
LogicalTypeFunction::Search(string_argument, pattern_argument) => {
let string = string_argument.eval(current_value, root_value);
let pattern = pattern_argument.eval(current_value, root_value);
let s = if let Some(Value::String(value)) = string {
value
} else {
return false;
};
let p = if let Some(Value::String(value)) = pattern {
value
} else {
return false;
};
if let Ok(regex) = Regex::new(&p) {
regex.is_match(&s)
} else {
false
}
}
}
}
}
#[allow(dead_code)]
pub fn length(value: &serde_json::Value) -> Option<usize> {
match value {
serde_json::Value::Array(arr) => Some(arr.len()),
serde_json::Value::Object(map) => Some(map.len()),
serde_json::Value::String(s) => Some(s.chars().count()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jsonpath2::ast::function::argument::{NodesTypeArgument, ValueTypeArgument};
use crate::jsonpath2::ast::literal::{Literal, Number};
use crate::jsonpath2::ast::query::{Query, RelativeQuery};
use crate::jsonpath2::ast::segment::{ChildSegment, DescendantSegment, Segment};
use crate::jsonpath2::ast::selector::{NameSelector, Selector, WildcardSelector};
use crate::jsonpath2::ast::singular_query::{
RelativeSingularQuery, SingularQuery, SingularQuerySegment,
};
use serde_json::json;
#[test]
fn test_length() {
// length("abc")
let length_function = ValueTypeFunction::Length(ValueTypeArgument::Literal(
Literal::String("abc".to_string()),
));
assert_eq!(
length_function.eval(&json!({}), &json!({})).unwrap(),
json!(3)
);
// length(1)
let length_function = ValueTypeFunction::Length(ValueTypeArgument::Literal(
Literal::Number(Number::Integer(1)),
));
assert!(length_function.eval(&json!({}), &json!({})).is_none());
// length(@.authors)
let length_function = ValueTypeFunction::Length(ValueTypeArgument::SingularQuery(
SingularQuery::Relative(RelativeSingularQuery::new(vec![
SingularQuerySegment::Name(NameSelector::new("authors".to_string())),
])),
));
assert_eq!(
length_function
.eval(&json!({"authors": ["a", "b", "c"]}), &json!({}))
.unwrap(),
json!(3)
);
assert_eq!(
length_function
.eval(
&json!({"authors": {"a":"a", "b": "b", "c": "c"}}),
&json!({})
)
.unwrap(),
json!(3)
);
assert!(length_function.eval(&json!({}), &json!({})).is_none());
}
#[test]
fn test_count() {
// count(@.*.author)
let count_function = ValueTypeFunction::Count(NodesTypeArgument::FilterQuery(
Query::RelativeQuery(RelativeQuery::new(vec![
Segment::Child(ChildSegment::new(vec![Selector::Wildcard(
WildcardSelector,
)])),
Segment::Child(ChildSegment::new(vec![Selector::Name(NameSelector::new(
"author".to_string(),
))])),
])),
));
assert_eq!(
count_function
.eval(&json!([{"author": "Bob"},{"author": "Bill"}]), &json!({}))
.unwrap(),
json!(2)
);
}
#[test]
fn test_match() {
// match(@.date, "1974-05-..")
let string_argument = ValueTypeArgument::SingularQuery(SingularQuery::Relative(
RelativeSingularQuery::new(vec![SingularQuerySegment::Name(NameSelector::new(
"date".to_string(),
))]),
));
let pattern_argument =
ValueTypeArgument::Literal(Literal::String("1974-05-..".to_string()));
let match_function = LogicalTypeFunction::Match(string_argument, pattern_argument);
assert!(match_function.eval(&json!({"date": "1974-05-01"}), &json!({})));
assert!(!match_function.eval(&json!({"date": "74-05-01"}), &json!({})));
assert!(!match_function.eval(&json!({"date": "1974-04-01"}), &json!({})));
assert!(!match_function.eval(&json!({"_date": "1974-05-01"}), &json!({})));
}
#[test]
fn test_search() {
// search(@.author, "[BR]ob")
let string_argument = ValueTypeArgument::SingularQuery(SingularQuery::Relative(
RelativeSingularQuery::new(vec![SingularQuerySegment::Name(NameSelector::new(
"author".to_string(),
))]),
));
let pattern_argument = ValueTypeArgument::Literal(Literal::String("[BR]ob".to_string()));
let search_function = LogicalTypeFunction::Search(string_argument, pattern_argument);
assert!(search_function.eval(&json!({"author": "Bob Dylan"}), &json!({})));
assert!(search_function.eval(&json!({"author": "Robert De Niro"}), &json!({})));
assert!(!search_function.eval(&json!({"author": "Christiano Ronaldo"}), &json!({})));
}
#[test]
fn test_value() {
// value(@..color)
let value_function =
ValueTypeFunction::Value(NodesTypeArgument::FilterQuery(Query::RelativeQuery(
RelativeQuery::new(vec![Segment::Descendant(DescendantSegment::new(vec![
Selector::Name(NameSelector::new("color".to_string())),
]))]),
)));
assert_eq!(
value_function
.eval(&json!({"color": "red"}), &json!({}))
.unwrap(),
json!("red")
);
assert!(value_function.eval(&json!({}), &json!({})).is_none());
}
}

View File

@ -0,0 +1,20 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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.
*
*/
mod argument;
mod functions;

View File

@ -18,6 +18,7 @@
mod comparison;
mod expr;
mod function;
mod literal;
mod query;
mod segment;

View File

@ -19,6 +19,7 @@
use crate::jsonpath2::ast::comparison::Comparable;
use crate::jsonpath2::ast::comparison::ComparisonExpr;
use crate::jsonpath2::ast::comparison::ComparisonOp;
use crate::jsonpath2::parser::function::functions::try_value_type_function;
use crate::jsonpath2::parser::literal;
use crate::jsonpath2::parser::primitives::{match_str, skip_whitespace};
use crate::jsonpath2::parser::singular_query::try_parse as try_singular_query;
@ -66,6 +67,8 @@ fn try_comparable(reader: &mut Reader) -> ParseResult<Option<Comparable>> {
Ok(Some(Comparable::Literal(literal)))
} else if let Some(singular_query) = try_singular_query(reader)? {
Ok(Some(Comparable::SingularQuery(singular_query)))
} else if let Some(function) = try_value_type_function(reader)? {
Ok(Some(Comparable::Function(function)))
} else {
Ok(None)
}

View File

@ -0,0 +1,90 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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::jsonpath2::ast::function::argument::{NodesTypeArgument, ValueTypeArgument};
use crate::jsonpath2::parser::literal;
use crate::jsonpath2::parser::query::try_filter_query;
use crate::jsonpath2::parser::singular_query;
use crate::jsonpath2::parser::{ParseError, ParseErrorKind, ParseResult};
use hurl_core::reader::Reader;
/// Parse an argument with ValueType
#[allow(dead_code)]
pub fn value_type(reader: &mut Reader) -> ParseResult<ValueTypeArgument> {
if let Some(v) = literal::try_parse(reader)? {
Ok(ValueTypeArgument::Literal(v))
} else if let Some(query) = singular_query::try_parse(reader)? {
Ok(ValueTypeArgument::SingularQuery(query))
} else {
Err(ParseError::new(
reader.cursor().pos,
ParseErrorKind::Expecting("a ValueType argument".to_string()),
))
}
}
/// Parse an argument with NodeType
#[allow(dead_code)]
pub fn nodes_type(reader: &mut Reader) -> ParseResult<NodesTypeArgument> {
if let Some(filter_query) = try_filter_query(reader)? {
Ok(NodesTypeArgument::FilterQuery(filter_query))
} else {
Err(ParseError::new(
reader.cursor().pos,
ParseErrorKind::Expecting("a NodesType argument".to_string()),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jsonpath2::ast::literal::{Literal, Number};
use crate::jsonpath2::ast::query::{Query, RelativeQuery};
use crate::jsonpath2::ast::segment::{ChildSegment, Segment};
use crate::jsonpath2::ast::selector::{NameSelector, Selector, WildcardSelector};
use hurl_core::reader::CharPos;
#[test]
pub fn test_value_type() {
let mut reader = Reader::new("42");
assert_eq!(
value_type(&mut reader).unwrap(),
ValueTypeArgument::Literal(Literal::Number(Number::Integer(42)))
);
assert_eq!(reader.cursor().index, CharPos(2));
}
#[test]
pub fn test_nodes_type() {
let mut reader = Reader::new("@.*.author");
assert_eq!(
nodes_type(&mut reader).unwrap(),
NodesTypeArgument::FilterQuery(Query::RelativeQuery(RelativeQuery::new(vec![
Segment::Child(ChildSegment::new(vec![Selector::Wildcard(
WildcardSelector
)])),
Segment::Child(ChildSegment::new(vec![Selector::Name(NameSelector::new(
"author".to_string()
)),]))
])))
);
assert_eq!(reader.cursor().index, CharPos(10));
}
}

View File

@ -0,0 +1,193 @@
/*
* Hurl (https://hurl.dev)
* Copyright (C) 2025 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 super::argument;
use crate::jsonpath2::ast::function::functions::{LogicalTypeFunction, ValueTypeFunction};
use crate::jsonpath2::parser::primitives::{expect_str, match_str, skip_whitespace};
use crate::jsonpath2::parser::ParseResult;
use hurl_core::reader::Reader;
/// Try to parse a function return a ValueType
#[allow(dead_code)]
pub fn try_value_type_function(reader: &mut Reader) -> ParseResult<Option<ValueTypeFunction>> {
//let initial_state = reader.cursor();
if match_str("length", reader) {
expect_str("(", reader)?;
let argument = argument::value_type(reader)?;
expect_str(")", reader)?;
Ok(Some(ValueTypeFunction::Length(argument)))
} else if match_str("value", reader) {
expect_str("(", reader)?;
let argument = argument::nodes_type(reader)?;
expect_str(")", reader)?;
Ok(Some(ValueTypeFunction::Value(argument)))
} else if match_str("count", reader) {
expect_str("(", reader)?;
let argument = argument::nodes_type(reader)?;
expect_str(")", reader)?;
Ok(Some(ValueTypeFunction::Count(argument)))
} else {
Ok(None)
}
}
/// Try to parse a function return a LogicalType
#[allow(dead_code)]
pub fn try_logical_type_function(reader: &mut Reader) -> ParseResult<Option<LogicalTypeFunction>> {
//let initial_state = reader.cursor();
if match_str("match", reader) {
expect_str("(", reader)?;
let argument1 = argument::value_type(reader)?;
skip_whitespace(reader);
expect_str(",", reader)?;
skip_whitespace(reader);
let argument2 = argument::value_type(reader)?;
expect_str(")", reader)?;
Ok(Some(LogicalTypeFunction::Match(argument1, argument2)))
} else if match_str("search", reader) {
expect_str("(", reader)?;
let argument1 = argument::value_type(reader)?;
skip_whitespace(reader);
expect_str(",", reader)?;
skip_whitespace(reader);
let argument2 = argument::value_type(reader)?;
expect_str(")", reader)?;
Ok(Some(LogicalTypeFunction::Search(argument1, argument2)))
} else {
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jsonpath2::ast::function::argument::{NodesTypeArgument, ValueTypeArgument};
use crate::jsonpath2::ast::literal::Literal;
use crate::jsonpath2::ast::query::{Query, RelativeQuery};
use crate::jsonpath2::ast::segment::{ChildSegment, DescendantSegment, Segment};
use crate::jsonpath2::ast::selector::{NameSelector, Selector, WildcardSelector};
use crate::jsonpath2::ast::singular_query::{
RelativeSingularQuery, SingularQuery, SingularQuerySegment,
};
use crate::jsonpath2::parser::{ParseError, ParseErrorKind};
use hurl_core::reader::{CharPos, Pos};
//use serde_json::json;
#[test]
pub fn test_length_function() {
let mut reader = Reader::new("length(\"abc\")");
assert_eq!(
try_value_type_function(&mut reader).unwrap().unwrap(),
ValueTypeFunction::Length(ValueTypeArgument::Literal(Literal::String(
"abc".to_string()
)))
);
assert_eq!(reader.cursor().index, CharPos(13));
}
#[test]
pub fn test_length_function_error() {
// not well-typed since @.* is a non-singular query
let mut reader = Reader::new("length(@.*)");
assert_eq!(
try_value_type_function(&mut reader).unwrap_err(),
ParseError::new(Pos::new(1, 9), ParseErrorKind::Expecting(")".to_string()))
);
}
#[test]
pub fn test_count_function() {
let mut reader = Reader::new("count(@.*.author)");
assert_eq!(
try_value_type_function(&mut reader).unwrap().unwrap(),
ValueTypeFunction::Count(NodesTypeArgument::FilterQuery(Query::RelativeQuery(
RelativeQuery::new(vec![
Segment::Child(ChildSegment::new(vec![Selector::Wildcard(
WildcardSelector
)])),
Segment::Child(ChildSegment::new(vec![Selector::Name(NameSelector::new(
"author".to_string()
)),]))
])
)))
);
assert_eq!(reader.cursor().index, CharPos(17));
}
#[test]
pub fn test_count_function_error() {
// not well-typed since 1 is not a query or function expression
let mut reader = Reader::new("count(1)");
assert_eq!(
try_value_type_function(&mut reader).unwrap_err(),
ParseError::new(
Pos::new(1, 7),
ParseErrorKind::Expecting("a NodesType argument".to_string())
)
);
}
#[test]
pub fn test_match_function() {
let mut reader = Reader::new("match(@.date, \"1974-05-01\")");
assert_eq!(
try_logical_type_function(&mut reader).unwrap().unwrap(),
LogicalTypeFunction::Match(
ValueTypeArgument::SingularQuery(SingularQuery::Relative(
RelativeSingularQuery::new(vec![SingularQuerySegment::Name(
NameSelector::new("date".to_string())
)])
)),
ValueTypeArgument::Literal(Literal::String("1974-05-01".to_string()))
)
);
assert_eq!(reader.cursor().index, CharPos(27));
}
#[test]
pub fn test_search_function() {
let mut reader = Reader::new("search(@.author, \"[BR]ob\")");
assert_eq!(
try_logical_type_function(&mut reader).unwrap().unwrap(),
LogicalTypeFunction::Search(
ValueTypeArgument::SingularQuery(SingularQuery::Relative(
RelativeSingularQuery::new(vec![SingularQuerySegment::Name(
NameSelector::new("author".to_string())
)])
)),
ValueTypeArgument::Literal(Literal::String("[BR]ob".to_string()))
)
);
assert_eq!(reader.cursor().index, CharPos(26));
}
#[test]
pub fn test_value_function() {
let mut reader = Reader::new("value(@..color)");
assert_eq!(
try_value_type_function(&mut reader).unwrap().unwrap(),
ValueTypeFunction::Value(NodesTypeArgument::FilterQuery(Query::RelativeQuery(
RelativeQuery::new(vec![Segment::Descendant(DescendantSegment::new(vec![
Selector::Name(NameSelector::new("color".to_string()))
]))])
)))
);
assert_eq!(reader.cursor().index, CharPos(15));
}
}

View File

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

View File

@ -49,6 +49,21 @@ pub fn parse(reader: &mut Reader) -> ParseResult<Literal> {
}
}
#[allow(dead_code)]
pub fn try_parse(reader: &mut Reader) -> ParseResult<Option<Literal>> {
if try_null(reader) {
Ok(Some(Literal::Null))
} else if let Some(value) = try_boolean(reader) {
Ok(Some(Literal::Bool(value)))
} else if let Some(value) = try_number(reader)? {
Ok(Some(Literal::Number(value)))
} else if let Some(value) = try_string_literal(reader)? {
Ok(Some(Literal::String(value)))
} else {
Ok(None)
}
}
/// Try to parse a boolean literal
#[allow(dead_code)]
fn try_boolean(reader: &mut Reader) -> Option<bool> {

View File

@ -19,6 +19,7 @@
mod comparison;
mod error;
mod expr;
mod function;
mod literal;
mod primitives;
mod query;

View File

@ -273,7 +273,7 @@ fn load_testcases() -> Vec<TestCase> {
fn run() {
let testcases = load_testcases();
// TODO: Remove Limit when spec is fully implemented
let testcases = testcases.iter().take(190);
let testcases = testcases.iter().take(200);
let count_total = testcases.len();
let testcases = testcases.filter(|tc| !IGNORED_TESTS.contains(&tc.name.as_str()));
let count_ignored = count_total - testcases.clone().count();