Module: Janeway::Functions
- Included in:
- Parser
- Defined in:
- lib/janeway/functions.rb,
lib/janeway/functions/count.rb,
lib/janeway/functions/match.rb,
lib/janeway/functions/value.rb,
lib/janeway/functions/length.rb,
lib/janeway/functions/search.rb
Overview
Mixin to provide JSONPath function handlers for Parser
Instance Method Summary collapse
-
#parse_function_count ⇒ Object
The count() function extension provides a way to obtain the number of nodes in a nodelist and make that available for further processing in the filter expression:.
-
#parse_function_length ⇒ Object
The length() function extension provides a way to compute the length of a value and make that available for further processing in the filter expression:.
-
#parse_function_match ⇒ Object
The match() function extension provides a way to check whether (the entirety of; see Section 2.4.7) a given string matches a given regular expression, which is in the form described in [RFC9485].
-
#parse_function_parameter ⇒ String, ...
All jsonpath function parameters are one of these accepted types.
-
#parse_function_search ⇒ Object
2.4.7.
-
#parse_function_value ⇒ Object
Parameters: 1.
-
#translate_iregex_to_ruby_regex(iregex, anchor: true) ⇒ Regexp
Convert IRegexp format to ruby regexp equivalent, following the instructions in rfc9485.
Instance Method Details
#parse_function_count ⇒ Object
The count() function extension provides a way to obtain the number of nodes in a nodelist and make that available for further processing in the filter expression:
Its only argument is a nodelist. The result is a value (an unsigned integer) that gives the number of nodes in the nodelist.
Notes:
* There is no deduplication of the nodelist.
* The number of nodes in the nodelist is counted independent of
their values or any children they may have, e.g., the count of a
non-empty singular nodelist such as count(@) is always 1.
20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
# File 'lib/janeway/functions/count.rb', line 20 def parse_function_count consume # function raise "expect group_start token, found #{current}" unless current.type == :group_start consume # ( # Read parameter arg = parse_function_parameter parameters = [arg] raise Error, "Invalid parameter - count() expects node list, got #{arg.value.inspect}" if arg.literal? unless current.type == :group_end raise Error, 'Too many parameters for count() function call' end # Define function body AST::Function.new('count', parameters) do |node_list| if node_list.is_a?(Array) node_list.size else 1 # The count of a non-empty singulare noselist such as count(@) is always 1 end end end |
#parse_function_length ⇒ Object
The length() function extension provides a way to compute the length of a value and make that available for further processing in the filter expression:
JSONPath return type: ValueType
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/janeway/functions/length.rb', line 9 def parse_function_length consume # function raise "expect group_start token, found #{current}" unless current.type == :group_start consume # ( # Read parameter arg = parse_function_parameter parameters = [arg] unless arg.singular_query? || arg.literal? raise Error, "Invalid parameter - length() expects literal value or singular query, got #{arg.value.inspect}" end unless current.type == :group_end raise Error, 'Too many parameters for length() function call' end # Meaning of return value depends on the JSON type: # * string - number of Unicode scalar values in the string. # * array - number of elements in the array. # * object - number of members in the object. # For any other argument value, the result is the special result Nothing. AST::Function.new('length', parameters) do |value| if [Array, Hash, String].include?(value.class) value.size else :nothing end end end |
#parse_function_match ⇒ Object
The match() function extension provides a way to check whether (the entirety of; see Section 2.4.7) a given string matches a given regular expression, which is in the form described in [RFC9485].
Its arguments are instances of ValueType (possibly taken from a singular query, as for the first argument in the example above). If the first argument is not a string or the second argument is not a string conforming to [RFC9485], the result is LogicalFalse. Otherwise, the string that is the first argument is matched against the I-Regexp contained in the string that is the second argument; the result is LogicalTrue if the string matches the I-Regexp and is LogicalFalse otherwise.
The regexp dialect is called “I-Regexp” and is defined in RFC9485.
Fortunately a shortcut is availalble, that RFC contains instructions for converting an I-Regexp to ruby’s regexp format. The instructions are:
* For any unescaped dots (.) outside character classes (first
alternative of charClass production), replace the dot with [^\n\r].
* Enclose the regexp in \A(?: and )\z.
tl;dr: How is this different from the search function? “match” must match the entire string, “search” matches a substring.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/janeway/functions/match.rb', line 35 def parse_function_match consume # function raise "expect group_start token, found #{current}" unless current.type == :group_start consume # ( # Read first parameter parameters = [] parameters << parse_function_parameter # Consume comma if current.type == :union consume # , else raise Error, 'Not enough parameters for match() function call' end # Read second parameter (the regexp) # This could be a string, in which case it is available now. # Otherwise it is an expression that takes the regexp from the input document, # and the iregexp will not be available until interpretation. parameters << parse_function_parameter unless current.type == :group_end raise Error, 'Too many parameters for match() function call' end AST::Function.new('match', parameters) do |str, str_iregexp| if str.is_a?(String) && str_iregexp.is_a?(String) regexp = translate_iregex_to_ruby_regex(str_iregexp) regexp.match?(str) else false # result defined by RFC9535 end end end |
#parse_function_parameter ⇒ String, ...
All jsonpath function parameters are one of these accepted types. Parse the function parameter and return the result.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/janeway/functions.rb', line 42 def parse_function_parameter result = case current.type when :string then parse_string when :current_node then parse_current_node when :root then parse_root when :group_end then raise Error, 'Function call is missing parameter' else # Invalid, no function uses this. # Instead of crashing here, accept it and let the function return an empty result. parse_expr end consume result end |
#parse_function_search ⇒ Object
2.4.7. search() Function Extension Parameters:
1. ValueType (string)
2. ValueType (string conforming to [RFC9485])
Result:
LogicalType
The search() function extension provides a way to check whether a given string contains a substring that matches a given regular expression, which is in the form described in [RFC9485].
$[?search(@.author, “[BR]ob”)]
Its arguments are instances of ValueType (possibly taken from a singular query, as for the first argument in the example above). If the first argument is not a string or the second argument is not a string conforming to [RFC9485], the result is LogicalFalse. Otherwise, the string that is the first argument is searched for a substring that matches the I-Regexp contained in the string that is the second argument; the result is LogicalTrue if at least one such substring exists and is LogicalFalse otherwise.
tl;dr: How is this different from the match function? “match” must match the entire string, “search” matches a substring.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/janeway/functions/search.rb', line 29 def parse_function_search consume # function raise "expect group_start token, found #{current}" unless current.type == :group_start consume # ( # Read first parameter parameters = [] parameters << parse_function_parameter # Consume comma if current.type == :union consume # , else raise Error, 'Insufficient parameters for search() function call' end # Read second parameter (the regexp) # This could be a string, in which case it is available now. # Otherwise it is an expression that takes the regexp from the input document, # and the iregexp will not be available until interpretation. parameters << parse_function_parameter unless current.type == :group_end raise Error, 'Too many parameters for match() function call' end AST::Function.new('search', parameters) do |str, str_iregexp| if str.is_a?(String) && str_iregexp.is_a?(String) regexp = translate_iregex_to_ruby_regex(str_iregexp, anchor: false) regexp.match?(str) else false # result defined by RFC9535 end end end |
#parse_function_value ⇒ Object
Parameters:
1. NodesType
Result:
ValueType
The value() function extension provides a way to convert an instance of NodesType to a value and make that available for further processing in the filter expression:
Its only argument is an instance of NodesType (possibly taken from a filter-query, as in the example above). The result is an instance of ValueType.
If the argument contains a single node, the result is the value of the node.
If the argument is the empty nodelist or contains multiple nodes, the result is Nothing.
Note: A singular query may be used anywhere where a ValueType is expected, so there is no need to use the value() function extension with a singular query.
28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/janeway/functions/value.rb', line 28 def parse_function_value consume # function raise "expect group_start token, found #{current}" unless current.type == :group_start consume # ( # Read parameter parameters = [parse_function_parameter] unless current.type == :group_end raise Error, 'Too many parameters for value() function call' end AST::Function.new('value', parameters) do |nodes| if nodes.is_a?(Array) && nodes.size == 1 nodes.first else :nothing end end end |
#translate_iregex_to_ruby_regex(iregex, anchor: true) ⇒ Regexp
Convert IRegexp format to ruby regexp equivalent, following the instructions in rfc9485.
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/janeway/functions.rb', line 13 def translate_iregex_to_ruby_regex(iregex, anchor: true) # * For any unescaped dots (.) outside character classes (first # alternative of charClass production), replace the dot with [^\n\r]. chars = iregex.chars in_char_class = false indexes = [] chars.each_with_index do |char, i| case char when '[' then in_char_class = true when ']' in_char_class = false unless chars[i - 1] == '\\' # escaped ] does not close char class when '.' next if in_char_class || chars[i - 1] == '\\' # escaped dot indexes << i # replace this dot end end indexes.reverse_each do |i| chars[i] = '[^\n\r]' end # * Enclose the regexp in \A(?: and )\z. regex_str = anchor ? format('\A(?:%s)\z', chars.join) : chars.join Regexp.new(regex_str) end |