class JsonPath::Parser

Parser parses and evaluates an expression passed to @_current_node.

Constants

REGEX

Public Class Methods

new(node, options) click to toggle source
# File lib/jsonpath/parser.rb, line 12
def initialize(node, options)
  @_current_node = node
  @_expr_map = {}
  @options = options
end

Public Instance Methods

construct_expression_map(exps) click to toggle source

Construct a map for which the keys are the expressions  and the values are the corresponding parsed results. Exp.: {“(@['author'] =~ /herman|lukyanenko/i)”=>0} {“@['isTrue']”=>true}

# File lib/jsonpath/parser.rb, line 44
def construct_expression_map(exps)
  exps.each_with_index do |item, _index|
    next if item == '&&' || item == '||'

    item = item.strip.gsub(/\)*$/, '').gsub(/^\(*/, '')
    @_expr_map[item] = parse_exp(item)
  end
end
parse(exp) click to toggle source

parse will parse an expression in the following way. Split the expression up into an array of legs for && and || operators. Parse this array into a map for which the keys are the parsed legs  of the split. This map is then used to replace the expression with their corresponding boolean or numeric value. This might look something like this: ((false || false) && (false || true))  Once this string is assembled… we proceed to evaluate from left to right.  The above string is broken down like this: (false && (false || true)) (false && true)  false

# File lib/jsonpath/parser.rb, line 29
def parse(exp)
  exps = exp.split(/(&&)|(\|\|)/)
  construct_expression_map(exps)
  @_expr_map.each { |k, v| exp.sub!(k, v.to_s) }
  raise ArgumentError, "unmatched parenthesis in expression: #{exp}" unless check_parenthesis_count(exp)

  exp = parse_parentheses(exp) while exp.include?('(')
  bool_or_exp(exp)
end
parse_exp(exp) click to toggle source

Using a scanner break down the individual expressions and determine if there is a match in the JSON for it or not.

# File lib/jsonpath/parser.rb, line 55
def parse_exp(exp)
  exp = exp.sub(/@/, '').gsub(/^\(/, '').gsub(/\)$/, '').tr('"', '\'').strip
  exp.scan(/^\[(\d+)\]/) do |i|
    next if i.empty?

    index = Integer(i[0])
    raise ArgumentError, 'Node does not appear to be an array.' unless @_current_node.is_a?(Array)
    raise ArgumentError, "Index out of bounds for nested array. Index: #{index}" if @_current_node.size < index

    @_current_node = @_current_node[index]
    # Remove the extra '' and the index.
    exp = exp.gsub(/^\[\d+\]|\[''\]/, '')
  end
  scanner = StringScanner.new(exp)
  elements = []
  until scanner.eos?
    if (t = scanner.scan(/\['[a-zA-Z@&*\/$%^?_]+'\]|\.[a-zA-Z0-9_]+[?!]?/))
      elements << t.gsub(/[\[\]'.]|\s+/, '')
    elsif (t = scanner.scan(/(\s+)?[<>=!\-+][=~]?(\s+)?/))
      operator = t
    elsif (t = scanner.scan(/(\s+)?'?.*'?(\s+)?/))
      # If we encounter a node which does not contain `'` it means
      #  that we are dealing with a boolean type.
      operand =
        if t == 'true'
          true
        elsif t == 'false'
          false
        elsif operator.to_s.strip == '=~'
          parse_regex(t)
        else
          t.gsub(%r{^'|'$}, '').strip
        end
    elsif (t = scanner.scan(/\/\w+\//))
    elsif (t = scanner.scan(/.*/))
      raise "Could not process symbol: #{t}"
    end
  end

  el = if elements.empty?
         @_current_node
       elsif @_current_node.is_a?(Hash)
         dig(@_current_node, *elements)
       else
         elements.inject(@_current_node, &:__send__)
       end

  return (el ? true : false) if el.nil? || operator.nil?

  el = Float(el) rescue el
  operand = Float(operand) rescue operand

  el.__send__(operator.strip, operand)
end

Private Instance Methods

bool_or_exp(b) click to toggle source

 This is convoluted and I should probably refactor it somehow.  The map that is created will contain strings since essentially I'm constructing a string like `true || true && false`. With eval the need for this would disappear but never the less, here  it is. The fact is that the results can be either boolean, or a number in case there is only indexing happening like give me the 3rd item… or it also can be nil in case of regexes or things that aren't found. Hence, I have to be clever here to see what kind of variable I need to provide back.

# File lib/jsonpath/parser.rb, line 187
def bool_or_exp(b)
  if b.to_s == 'true'
    return true
  elsif b.to_s == 'false'
    return false
  elsif b.to_s == ''
    return nil
  end

  b = Float(b) rescue b
  b
end
check_parenthesis_count(exp) click to toggle source

this simply makes sure that we aren't getting into the whole  parenthesis parsing business without knowing that every parenthesis has its pair.

# File lib/jsonpath/parser.rb, line 203
def check_parenthesis_count(exp)
  return true unless exp.include?('(')

  depth = 0
  exp.chars.each do |c|
    if c == '('
      depth += 1
    elsif c == ')'
      depth -= 1
    end
  end
  depth == 0
end
parse_parentheses(str) click to toggle source

 This will break down a parenthesis from the left to the right  and replace the given expression with it's returned value. It does this in order to make it easy to eliminate groups one-by-one.

# File lib/jsonpath/parser.rb, line 141
def parse_parentheses(str)
  opening_index = 0
  closing_index = 0

  (0..str.length - 1).step(1) do |i|
    opening_index = i if str[i] == '('
    if str[i] == ')'
      closing_index = i
      break
    end
  end

  to_parse = str[opening_index + 1..closing_index - 1]

  #  handle cases like (true && true || false && true) in
  # one giant parenthesis.
  top = to_parse.split(/(&&)|(\|\|)/)
  top = top.map(&:strip)
  res = bool_or_exp(top.shift)
  top.each_with_index do |item, index|
    if item == '&&'
      res &&= top[index + 1]
    elsif item == '||'
      res ||= top[index + 1]
    end
  end

  #  if we are at the last item, the opening index will be 0
  # and the closing index will be the last index. To avoid
  # off-by-one errors we simply return the result at that point.
  if closing_index + 1 >= str.length && opening_index == 0
    res.to_s
  else
    "#{str[0..opening_index - 1]}#{res}#{str[closing_index + 1..str.length]}"
  end
end
parse_regex(t) click to toggle source

/foo/i -> Regex.new(“foo”, Regexp::IGNORECASE) without using eval also supports %r{foo}i following github.com/seamusabshere/to_regexp/blob/master/lib/to_regexp.rb

# File lib/jsonpath/parser.rb, line 115
def parse_regex(t)
  t =~ REGEX
  content = $1 || $3
  options = $2 || $4

  raise ArgumentError, "unsupported regex #{t} use /foo/ style" if !content || !options

  content = content.gsub '\\/', '/'

  flags = 0
  flags |= Regexp::IGNORECASE if options.include?('i')
  flags |= Regexp::MULTILINE if options.include?('m')
  flags |= Regexp::EXTENDED if options.include?('x')

  # 'n' = none, 'e' = EUC, 's' = SJIS, 'u' = UTF-8
  lang = options.scan(/[nes]/).join.downcase # ignores u since that is default and causes a warning

  args = [content, flags]
  args << lang unless lang.empty? # avoid warning
  Regexp.new(*args)
end