From e9c732e76bad596e477479da2b93fc25397f981f Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 08:08:35 +0100 Subject: [PATCH 01/49] Make the AST interpret itself Replace the hash AST with one that consists of node objects. Each AST node can evaluate itself, which means that the job of TreeInterpreter is replaced by calling #visit on the top node in the AST. --- lib/jmespath.rb | 2 +- lib/jmespath/expr_node.rb | 15 - lib/jmespath/nodes.rb | 30 ++ lib/jmespath/nodes/comparator.rb | 48 +++ lib/jmespath/nodes/condition.rb | 25 ++ lib/jmespath/nodes/current.rb | 18 + lib/jmespath/nodes/expression.rb | 32 ++ lib/jmespath/nodes/field.rb | 27 ++ lib/jmespath/nodes/flatten.rb | 30 ++ lib/jmespath/nodes/function.rb | 342 ++++++++++++++++ lib/jmespath/nodes/index.rb | 27 ++ lib/jmespath/nodes/literal.rb | 23 ++ lib/jmespath/nodes/multi_select_hash.rb | 46 +++ lib/jmespath/nodes/multi_select_list.rb | 27 ++ lib/jmespath/nodes/or.rb | 28 ++ lib/jmespath/nodes/pipe.rb | 23 ++ lib/jmespath/nodes/projection.rb | 45 ++ lib/jmespath/nodes/slice.rb | 87 ++++ lib/jmespath/nodes/subexpression.rb | 23 ++ lib/jmespath/parser.rb | 142 ++----- lib/jmespath/runtime.rb | 8 +- lib/jmespath/tree_interpreter.rb | 523 ------------------------ 22 files changed, 928 insertions(+), 643 deletions(-) delete mode 100644 lib/jmespath/expr_node.rb create mode 100644 lib/jmespath/nodes.rb create mode 100644 lib/jmespath/nodes/comparator.rb create mode 100644 lib/jmespath/nodes/condition.rb create mode 100644 lib/jmespath/nodes/current.rb create mode 100644 lib/jmespath/nodes/expression.rb create mode 100644 lib/jmespath/nodes/field.rb create mode 100644 lib/jmespath/nodes/flatten.rb create mode 100644 lib/jmespath/nodes/function.rb create mode 100644 lib/jmespath/nodes/index.rb create mode 100644 lib/jmespath/nodes/literal.rb create mode 100644 lib/jmespath/nodes/multi_select_hash.rb create mode 100644 lib/jmespath/nodes/multi_select_list.rb create mode 100644 lib/jmespath/nodes/or.rb create mode 100644 lib/jmespath/nodes/pipe.rb create mode 100644 lib/jmespath/nodes/projection.rb create mode 100644 lib/jmespath/nodes/slice.rb create mode 100644 lib/jmespath/nodes/subexpression.rb delete mode 100644 lib/jmespath/tree_interpreter.rb diff --git a/lib/jmespath.rb b/lib/jmespath.rb index c59a656..ef3eccb 100644 --- a/lib/jmespath.rb +++ b/lib/jmespath.rb @@ -7,11 +7,11 @@ module JMESPath autoload :Errors, 'jmespath/errors' autoload :ExprNode, 'jmespath/expr_node' autoload :Lexer, 'jmespath/lexer' + autoload :Nodes, 'jmespath/nodes' autoload :Parser, 'jmespath/parser' autoload :Runtime, 'jmespath/runtime' autoload :Token, 'jmespath/token' autoload :TokenStream, 'jmespath/token_stream' - autoload :TreeInterpreter, 'jmespath/tree_interpreter' autoload :VERSION, 'jmespath/version' class << self diff --git a/lib/jmespath/expr_node.rb b/lib/jmespath/expr_node.rb deleted file mode 100644 index f014aca..0000000 --- a/lib/jmespath/expr_node.rb +++ /dev/null @@ -1,15 +0,0 @@ -module JMESPath - # @api private - class ExprNode - - def initialize(interpreter, node) - @interpreter = interpreter - @node = node - end - - attr_reader :interpreter - - attr_reader :node - - end -end diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb new file mode 100644 index 0000000..d337af7 --- /dev/null +++ b/lib/jmespath/nodes.rb @@ -0,0 +1,30 @@ +module JMESPath + # @api private + module Nodes + class Node + def visit(value) + end + + def hash_like?(value) + Hash === value || Struct === value + end + end + + autoload :Comparator, 'jmespath/nodes/comparator' + autoload :Condition, 'jmespath/nodes/condition' + autoload :Current, 'jmespath/nodes/current' + autoload :Expression, 'jmespath/nodes/expression' + autoload :Field, 'jmespath/nodes/field' + autoload :Flatten, 'jmespath/nodes/flatten' + autoload :Function, 'jmespath/nodes/function' + autoload :Index, 'jmespath/nodes/index' + autoload :Literal, 'jmespath/nodes/literal' + autoload :MultiSelectHash, 'jmespath/nodes/multi_select_hash' + autoload :MultiSelectList, 'jmespath/nodes/multi_select_list' + autoload :Or, 'jmespath/nodes/or' + autoload :Pipe, 'jmespath/nodes/pipe' + autoload :Projection, 'jmespath/nodes/projection' + autoload :Slice, 'jmespath/nodes/slice' + autoload :Subexpression, 'jmespath/nodes/subexpression' + end +end diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb new file mode 100644 index 0000000..6ed0afb --- /dev/null +++ b/lib/jmespath/nodes/comparator.rb @@ -0,0 +1,48 @@ +module JMESPath + # @api private + module Nodes + class Comparator < Node + attr_reader :children, :relation + + def initialize(children, relation) + @children = children + @relation = relation + end + + def visit(value) + left = @children[0].visit(value) + right = @children[1].visit(value) + case @relation + when '==' then compare_values(left, right) + when '!=' then !compare_values(left, right) + when '>' then is_int(left) && is_int(right) && left > right + when '>=' then is_int(left) && is_int(right) && left >= right + when '<' then is_int(left) && is_int(right) && left < right + when '<=' then is_int(left) && is_int(right) && left <= right + end + end + + def to_h + { + :type => :comparator, + :children => @children.map(&:to_h), + :relation => @relation, + } + end + + private + + def compare_values(a, b) + if a == b + true + else + false + end + end + + def is_int(value) + Integer === value + end + end + end +end diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb new file mode 100644 index 0000000..559bd96 --- /dev/null +++ b/lib/jmespath/nodes/condition.rb @@ -0,0 +1,25 @@ +module JMESPath + # @api private + module Nodes + class Condition < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + true == @children[0].visit(value) ? + @children[1].visit(value) : + nil + end + + def to_h + { + :type => :condition, + :children => @children.map(&:to_h), + } + end + end + end +end diff --git a/lib/jmespath/nodes/current.rb b/lib/jmespath/nodes/current.rb new file mode 100644 index 0000000..b6dd8d3 --- /dev/null +++ b/lib/jmespath/nodes/current.rb @@ -0,0 +1,18 @@ +module JMESPath + # @api private + module Nodes + class Current < Node + attr_reader :value + + def visit(value) + value + end + + def to_h + { + :type => :current, + } + end + end + end +end diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb new file mode 100644 index 0000000..cdaccce --- /dev/null +++ b/lib/jmespath/nodes/expression.rb @@ -0,0 +1,32 @@ +module JMESPath + # @api private + module Nodes + class Expression < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + ExprNode.new(@children[0]) + end + + def to_h + { + :type => :expression, + :children => @children.map(&:to_h), + } + end + + class ExprNode + attr_reader :node + + def initialize(node) + @node = node + end + end + end + end +end + diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb new file mode 100644 index 0000000..a4b7404 --- /dev/null +++ b/lib/jmespath/nodes/field.rb @@ -0,0 +1,27 @@ +module JMESPath + # @api private + module Nodes + class Field < Node + attr_reader :key + + def initialize(key) + @key = key + end + + def visit(value) + case value + when Hash then value.key?(@key) ? value[@key] : value[@key.to_sym] + when Struct then value.respond_to?(@key) ? value[@key] : nil + else nil + end + end + + def to_h + { + :type => :field, + :key => @key, + } + end + end + end +end diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb new file mode 100644 index 0000000..6e386bb --- /dev/null +++ b/lib/jmespath/nodes/flatten.rb @@ -0,0 +1,30 @@ +module JMESPath + # @api private + module Nodes + class Flatten < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + value = @children[0].visit(value) + if Array === value + value.inject([]) do |values, v| + values + (Array === v ? v : [v]) + end + else + nil + end + end + + def to_h + { + :type => :flatten, + :children => @children.map(&:to_h), + } + end + end + end +end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb new file mode 100644 index 0000000..5ad0edf --- /dev/null +++ b/lib/jmespath/nodes/function.rb @@ -0,0 +1,342 @@ +module JMESPath + # @api private + module Nodes + class Function < Node + attr_reader :children, :fn + + def initialize(children, fn) + @children = children + @fn = fn + end + + def visit(value) + args = @children.map { |child| child.visit(value) } + send("function_#{@fn}", *args) + end + + def to_h + { + :type => :function, + :children => @children.map(&:to_h), + :fn => @fn, + } + end + + private + + def method_missing(method_name, *args) + if matches = method_name.to_s.match(/^function_(.*)/) + raise Errors::UnknownFunctionError, "unknown function #{matches[1]}()" + else + super + end + end + + def get_type(value) + case + when Expression::ExprNode === value then 'expression' + when String === value then 'string' + when hash_like?(value) then 'object' + when Array === value then 'array' + when [true, false].include?(value) then 'boolean' + when value.nil? then 'null' + when Numeric === value then 'number' + end + end + + def number_compare(mode, *args) + if args.count == 2 + if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' + values = args[0] + expression = args[1] + args[0].send("#{mode}_by") do |entry| + value = expression.node.visit(entry) + if get_type(value) == 'number' + value + else + raise Errors::InvalidTypeError, "function #{mode}_by() expects values to be an numbers" + end + end + else + raise Errors::InvalidTypeError, "function #{mode}_by() expects an array and an expression" + end + else + raise Errors::InvalidArityError, "function #{mode}_by() expects two arguments" + end + end + + def function_abs(*args) + if args.count == 1 + value = args.first + else + raise Errors::InvalidArityError, "function abs() expects one argument" + end + if Numeric === value + value.abs + else + raise Errors::InvalidTypeError, "function abs() expects a number" + end + end + + def function_avg(*args) + if args.count == 1 + values = args.first + else + raise Errors::InvalidArityError, "function avg() expects one argument" + end + if Array === values + values.inject(0) do |total,n| + if Numeric === n + total + n + else + raise Errors::InvalidTypeError, "function avg() expects numeric values" + end + end / values.size.to_f + else + raise Errors::InvalidTypeError, "function avg() expects a number" + end + end + + def function_ceil(*args) + if args.count == 1 + value = args.first + else + raise Errors::InvalidArityError, "function ceil() expects one argument" + end + if Numeric === value + value.ceil + else + raise Errors::InvalidTypeError, "function ceil() expects a numeric value" + end + end + + def function_contains(*args) + if args.count == 2 + if String === args[0] || Array === args[0] + args[0].include?(args[1]) + else + raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list" + end + else + raise Errors::InvalidArityError, "function contains() expects 2 arguments" + end + end + + def function_floor(*args) + if args.count == 1 + value = args.first + else + raise Errors::InvalidArityError, "function floor() expects one argument" + end + if Numeric === value + value.floor + else + raise Errors::InvalidTypeError, "function floor() expects a numeric value" + end + end + + def function_length(*args) + if args.count == 1 + value = args.first + else + raise Errors::InvalidArityError, "function length() expects one argument" + end + case value + when Hash, Array, String then value.size + else raise Errors::InvalidTypeError, "function length() expects string, array or object" + end + end + + def function_max(*args) + if args.count == 1 + values = args.first + else + raise Errors::InvalidArityError, "function max() expects one argument" + end + if Array === values + values.inject(values.first) do |max, v| + if Numeric === v + v > max ? v : max + else + raise Errors::InvalidTypeError, "function max() expects numeric values" + end + end + else + raise Errors::InvalidTypeError, "function max() expects an array" + end + end + + def function_min(*args) + if args.count == 1 + values = args.first + else + raise Errors::InvalidArityError, "function min() expects one argument" + end + if Array === values + values.inject(values.first) do |min, v| + if Numeric === v + v < min ? v : min + else + raise Errors::InvalidTypeError, "function min() expects numeric values" + end + end + else + raise Errors::InvalidTypeError, "function min() expects an array" + end + end + + def function_type(*args) + if args.count == 1 + get_type(args.first) + else + raise Errors::InvalidArityError, "function type() expects one argument" + end + end + + def function_keys(*args) + if args.count == 1 + value = args.first + if hash_like?(value) + case value + when Hash then value.keys.map(&:to_s) + when Struct then value.members.map(&:to_s) + else raise NotImplementedError + end + else + raise Errors::InvalidTypeError, "function keys() expects a hash" + end + else + raise Errors::InvalidArityError, "function keys() expects one argument" + end + end + + def function_values(*args) + if args.count == 1 + value = args.first + if hash_like?(value) + value.values + elsif Array === value + value + else + raise Errors::InvalidTypeError, "function values() expects an array or a hash" + end + else + raise Errors::InvalidArityError, "function values() expects one argument" + end + end + + def function_join(*args) + if args.count == 2 + glue = args[0] + values = args[1] + if !(String === glue) + raise Errors::InvalidTypeError, "function join() expects the first argument to be a string" + elsif Array === values && values.all? { |v| String === v } + values.join(glue) + else + raise Errors::InvalidTypeError, "function join() expects values to be an array of strings" + end + else + raise Errors::InvalidArityError, "function join() expects an array of strings" + end + end + + def function_to_string(*args) + if args.count == 1 + value = args.first + String === value ? value : MultiJson.dump(value) + else + raise Errors::InvalidArityError, "function to_string() expects one argument" + end + end + + def function_to_number(*args) + if args.count == 1 + begin + value = Float(args.first) + Integer(value) === value ? value.to_i : value + rescue + nil + end + else + raise Errors::InvalidArityError, "function to_number() expects one argument" + end + end + + def function_sum(*args) + if args.count == 1 && Array === args.first + args.first.inject(0) do |sum,n| + if Numeric === n + sum + n + else + raise Errors::InvalidTypeError, "function sum() expects values to be numeric" + end + end + else + raise Errors::InvalidArityError, "function sum() expects one argument" + end + end + + def function_not_null(*args) + if args.count > 0 + args.find { |value| !value.nil? } + else + raise Errors::InvalidArityError, "function not_null() expects one or more arguments" + end + end + + def function_sort(*args) + if args.count == 1 + value = args.first + if Array === value + value.sort do |a, b| + a_type = get_type(a) + b_type = get_type(b) + if ['string', 'number'].include?(a_type) && a_type == b_type + a <=> b + else + raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" + end + end + else + raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" + end + else + raise Errors::InvalidArityError, "function sort() expects one argument" + end + end + + def function_sort_by(*args) + if args.count == 2 + if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' + values = args[0] + expression = args[1] + values.sort do |a,b| + a_value = expression.node.visit(a) + b_value = expression.node.visit(b) + a_type = get_type(a_value) + b_type = get_type(b_value) + if ['string', 'number'].include?(a_type) && a_type == b_type + a_value <=> b_value + else + raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" + end + end + else + raise Errors::InvalidTypeError, "function sort_by() expects an array and an expression" + end + else + raise Errors::InvalidArityError, "function sort_by() expects two arguments" + end + end + + def function_max_by(*args) + number_compare(:max, *args) + end + + def function_min_by(*args) + number_compare(:min, *args) + end + end + end +end diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb new file mode 100644 index 0000000..7811f0a --- /dev/null +++ b/lib/jmespath/nodes/index.rb @@ -0,0 +1,27 @@ +module JMESPath + # @api private + module Nodes + class Index < Node + attr_reader :index + + def initialize(index) + @index = index + end + + def visit(value) + if Array === value + value[@index] + else + nil + end + end + + def to_h + { + :type => :index, + :index => @index, + } + end + end + end +end diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb new file mode 100644 index 0000000..0b469d2 --- /dev/null +++ b/lib/jmespath/nodes/literal.rb @@ -0,0 +1,23 @@ +module JMESPath + # @api private + module Nodes + class Literal < Node + attr_reader :value + + def initialize(value) + @value = value + end + + def visit(value) + @value + end + + def to_h + { + :type => :literal, + :value => @value, + } + end + end + end +end diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb new file mode 100644 index 0000000..a90b3ef --- /dev/null +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -0,0 +1,46 @@ +module JMESPath + # @api private + module Nodes + class MultiSelectHash < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + if value.nil? + nil + else + @children.each.with_object({}) do |child, hash| + hash[child.key] = child.children[0].visit(value) + end + end + end + + def to_h + { + :type => :multi_select_hash, + :children => @children.map(&:to_h), + } + end + + class KeyValuePair + attr_reader :children, :key + + def initialize(children, key) + @children = children + @key = key + end + + def to_h + { + :type => :key_value_pair, + :children => @children.map(&:to_h), + :key => @key, + } + end + end + end + end +end diff --git a/lib/jmespath/nodes/multi_select_list.rb b/lib/jmespath/nodes/multi_select_list.rb new file mode 100644 index 0000000..e7948aa --- /dev/null +++ b/lib/jmespath/nodes/multi_select_list.rb @@ -0,0 +1,27 @@ +module JMESPath + # @api private + module Nodes + class MultiSelectList < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + if value.nil? + value + else + @children.map { |n| n.visit(value) } + end + end + + def to_h + { + :type => :multi_select_list, + :children => @children.map(&:to_h), + } + end + end + end +end diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb new file mode 100644 index 0000000..d8c7d1a --- /dev/null +++ b/lib/jmespath/nodes/or.rb @@ -0,0 +1,28 @@ +module JMESPath + # @api private + module Nodes + class Or < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + result = @children[0].visit(value) + if result.nil? or result.empty? + @children[1].visit(value) + else + result + end + end + + def to_h + { + :type => :or, + :children => @children.map(&:to_h), + } + end + end + end +end diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb new file mode 100644 index 0000000..9cfdda5 --- /dev/null +++ b/lib/jmespath/nodes/pipe.rb @@ -0,0 +1,23 @@ +module JMESPath + # @api private + module Nodes + class Pipe < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + @children[1].visit(@children[0].visit(value)) + end + + def to_h + { + :type => :pipe, + :children => @children.map(&:to_h), + } + end + end + end +end diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb new file mode 100644 index 0000000..c11aff4 --- /dev/null +++ b/lib/jmespath/nodes/projection.rb @@ -0,0 +1,45 @@ +module JMESPath + # @api private + module Nodes + class Projection < Node + attr_reader :children, :from + + def initialize(children, from) + @children = children + @from = from + end + + def visit(value) + # Interprets a projection node, passing the values of the left + # child through the values of the right child and aggregating + # the non-null results into the return value. + left = @children[0].visit(value) + if @from == :object && hash_like?(left) + projection(left.values) + elsif @from == :object && left == [] + projection(left) + elsif @from == :array && Array === left + projection(left) + else + nil + end + end + + def to_h + { + :type => :projection, + :children => @children.map(&:to_h), + :from => @from, + } + end + + private + + def projection(values) + values.inject([]) do |list, v| + list << @children[1].visit(v) + end.compact + end + end + end +end diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb new file mode 100644 index 0000000..2770429 --- /dev/null +++ b/lib/jmespath/nodes/slice.rb @@ -0,0 +1,87 @@ +module JMESPath + # @api private + module Nodes + class Slice < Node + attr_reader :args + + def initialize(args) + @args = args + end + + def visit(value) + function_slice(value, *@args) + end + + def to_h + { + :type => :slice, + :args => @args, + } + end + + private + + def function_slice(values, *args) + if String === values || Array === values + _slice(values, *args) + else + nil + end + end + + def _slice(values, start, stop, step) + start, stop, step = _adjust_slice(values.size, start, stop, step) + result = [] + if step > 0 + i = start + while i < stop + result << values[i] + i += step + end + else + i = start + while i > stop + result << values[i] + i += step + end + end + String === values ? result.join : result + end + + def _adjust_slice(length, start, stop, step) + if step.nil? + step = 1 + elsif step == 0 + raise Errors::RuntimeError, 'slice step cannot be 0' + end + + if start.nil? + start = step < 0 ? length - 1 : 0 + else + start = _adjust_endpoint(length, start, step) + end + + if stop.nil? + stop = step < 0 ? -1 : length + else + stop = _adjust_endpoint(length, stop, step) + end + + [start, stop, step] + end + + def _adjust_endpoint(length, endpoint, step) + if endpoint < 0 + endpoint += length + endpoint = 0 if endpoint < 0 + endpoint + elsif endpoint >= length + step < 0 ? length - 1 : length + else + endpoint + end + end + + end + end +end diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb new file mode 100644 index 0000000..da00187 --- /dev/null +++ b/lib/jmespath/nodes/subexpression.rb @@ -0,0 +1,23 @@ +module JMESPath + # @api private + module Nodes + class Subexpression < Node + attr_reader :children + + def initialize(children) + @children = children + end + + def visit(value) + @children[1].visit(@children[0].visit(value)) + end + + def to_h + { + :type => :subexpression, + :children => @children.map(&:to_h), + } + end + end + end +end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 832b9f0..8d52e9d 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -15,7 +15,7 @@ class Parser :filter, # foo.[?bar==10] ]) - CURRENT_NODE = { type: :current } + CURRENT_NODE = Nodes::Current.new # @option options [Lexer] :lexer def initialize(options = {}) @@ -61,10 +61,7 @@ def nud_current(stream) def nud_expref(stream) stream.next - { - type: :expression, - children: [expr(stream, 2)] - } + Nodes::Expression.new([expr(stream, 2)]) end def nud_filter(stream) @@ -78,7 +75,7 @@ def nud_flatten(stream) def nud_identifier(stream) token = stream.token stream.next - { type: :field, key: token.value } + Nodes::Field.new(token.value) end def nud_lbrace(stream) @@ -92,10 +89,7 @@ def nud_lbrace(stream) end end while stream.token.type != :rbrace stream.next - { - type: :multi_select_hash, - children: pairs - } + Nodes::MultiSelectHash.new(pairs) end def nud_lbracket(stream) @@ -113,10 +107,7 @@ def nud_lbracket(stream) def nud_literal(stream) value = stream.token.value stream.next - { - type: :literal, - value: value - } + Nodes::Literal.new(value) end def nud_quoted_identifier(stream) @@ -126,7 +117,7 @@ def nud_quoted_identifier(stream) msg = 'quoted identifiers are not allowed for function names' raise Errors::SyntaxError, msg else - { type: :field, key: token[:value] } + Nodes::Field.new(token[:value]) end end @@ -137,14 +128,8 @@ def nud_star(stream) def led_comparator(stream, left) token = stream.token stream.next - { - type: :comparator, - relation: token.value, - children: [ - left, - expr(stream), - ] - } + children = [left, expr(stream)] + Nodes::Comparator.new(children, token.value) end def led_dot(stream, left) @@ -152,13 +137,8 @@ def led_dot(stream, left) if stream.token.type == :star parse_wildcard_object(stream, left) else - { - type: :subexpression, - children: [ - left, - parse_dot(stream, Token::BINDING_POWER[:dot]) - ] - } + children = [left, parse_dot(stream, Token::BINDING_POWER[:dot])] + Nodes::Subexpression.new(children) end end @@ -170,42 +150,31 @@ def led_filter(stream, left) end stream.next rhs = parse_projection(stream, Token::BINDING_POWER[:filter]) - { - type: :projection, - from: :array, - children: [ - left ? left : CURRENT_NODE, - { - type: :condition, - children: [expression, rhs], - } - ] - } + children = [ + left ? left : CURRENT_NODE, + Nodes::Condition.new([expression, rhs]) + ] + Nodes::Projection.new(children, :array) end def led_flatten(stream, left) stream.next - { - type: :projection, - from: :array, - children: [ - { type: :flatten, children: [left] }, - parse_projection(stream, Token::BINDING_POWER[:flatten]) - ] - } + children = [ + Nodes::Flatten.new([left]), + parse_projection(stream, Token::BINDING_POWER[:flatten]) + ] + Nodes::Projection.new(children, :array) end def led_lbracket(stream, left) stream.next(match: Set.new([:number, :colon, :star])) type = stream.token.type if type == :number || type == :colon - { - type: :subexpression, - children: [ - left, - parse_array_index_expression(stream) - ] - } + children = [ + left, + parse_array_index_expression(stream) + ] + Nodes::Subexpression.new(children) else parse_wildcard_array(stream, left) end @@ -213,7 +182,7 @@ def led_lbracket(stream, left) def led_lparen(stream, left) args = [] - name = left[:key] + name = left.key stream.next while stream.token.type != :rparen args << expr(stream, 0) @@ -222,27 +191,19 @@ def led_lparen(stream, left) end end stream.next - { - type: :function, - fn: name, - children: args, - } + Nodes::Function.new(args, name) end def led_or(stream, left) stream.next - { - type: :or, - children: [left, expr(stream, Token::BINDING_POWER[:or])] - } + children = [left, expr(stream, Token::BINDING_POWER[:or])] + Nodes::Or.new(children) end def led_pipe(stream, left) stream.next - { - type: :pipe, - children: [left, expr(stream, Token::BINDING_POWER[:pipe])], - } + children = [left, expr(stream, Token::BINDING_POWER[:pipe])] + Nodes::Pipe.new(children) end def parse_array_index_expression(stream) @@ -258,11 +219,11 @@ def parse_array_index_expression(stream) end while stream.token.type != :rbracket stream.next if pos == 0 - { type: :index, index: parts[0] } + Nodes::Index.new(parts[0]) elsif pos > 2 raise Errors::SyntaxError, 'invalid array slice syntax: too many colons' else - { type: :slice, args: parts } + Nodes::Slice.new(parts) end end @@ -279,11 +240,7 @@ def parse_key_value_pair(stream) key = stream.token.value stream.next(match:Set.new([:colon])) stream.next - { - type: :key_value_pair, - key: key, - children: [expr(stream)] - } + Nodes::MultiSelectHash::KeyValuePair.new([expr(stream)], key) end def parse_multi_select_list(stream) @@ -298,10 +255,7 @@ def parse_multi_select_list(stream) end end while stream.token.type != :rbracket stream.next - { - type: :multi_select_list, - children: nodes - } + Nodes::MultiSelectList.new(nodes) end def parse_projection(stream, binding_power) @@ -321,26 +275,20 @@ def parse_projection(stream, binding_power) def parse_wildcard_array(stream, left = nil) stream.next(match:Set.new([:rbracket])) stream.next - { - type: :projection, - from: :array, - children: [ - left ? left : CURRENT_NODE, - parse_projection(stream, Token::BINDING_POWER[:star]) - ] - } + children = [ + left ? left : CURRENT_NODE, + parse_projection(stream, Token::BINDING_POWER[:star]) + ] + Nodes::Projection.new(children, :array) end def parse_wildcard_object(stream, left = nil) stream.next - { - type: :projection, - from: :object, - children: [ - left ? left : CURRENT_NODE, - parse_projection(stream, Token::BINDING_POWER[:star]) - ] - } + children = [ + left ? left : CURRENT_NODE, + parse_projection(stream, Token::BINDING_POWER[:star]) + ] + Nodes::Projection.new(children, :object) end end diff --git a/lib/jmespath/runtime.rb b/lib/jmespath/runtime.rb index 434bf07..4de3080 100644 --- a/lib/jmespath/runtime.rb +++ b/lib/jmespath/runtime.rb @@ -37,24 +37,18 @@ class Runtime # # @option options [Parser,CachingParser] :parser # - # @option options [Interpreter] :interpreter - # def initialize(options = {}) @parser = options[:parser] || default_parser(options) - @interpreter = options[:interpreter] || TreeInterpreter.new end # @return [Parser, CachingParser] attr_reader :parser - # @return [Interpreter] - attr_reader :interpreter - # @param [String] expression # @param [Hash] data # @return [Mixed,nil] def search(expression, data) - @interpreter.visit(@parser.parse(expression), data) + @parser.parse(expression).visit(data) end private diff --git a/lib/jmespath/tree_interpreter.rb b/lib/jmespath/tree_interpreter.rb deleted file mode 100644 index 9deced2..0000000 --- a/lib/jmespath/tree_interpreter.rb +++ /dev/null @@ -1,523 +0,0 @@ -module JMESPath - # @api private - class TreeInterpreter - - def visit(node, data) - dispatch(node, data) - end - - # @api private - def method_missing(method_name, *args) - if matches = method_name.to_s.match(/^function_(.*)/) - raise Errors::UnknownFunctionError, "unknown function #{matches[1]}()" - else - super - end - end - - private - - def dispatch(node, value) - case node[:type] - - when :field - # hash_like? - key = node[:key] - case value - when Hash then value.key?(key) ? value[key] : value[key.to_sym] - when Struct then value.respond_to?(key) ? value[key] : nil - else nil - end - - when :subexpression - dispatch(node[:children][1], dispatch(node[:children][0], value)) - - when :index - if Array === value - value[node[:index]] - else - nil - end - - when :projection - # Interprets a projection node, passing the values of the left - # child through the values of the right child and aggregating - # the non-null results into the return value. - left = dispatch(node[:children][0], value) - if node[:from] == :object && hash_like?(left) - projection(left.values, node) - elsif node[:from] == :object && left == [] - projection(left, node) - elsif node[:from] == :array && Array === left - projection(left, node) - else - nil - end - - when :flatten - value = dispatch(node[:children][0], value) - if Array === value - value.inject([]) do |values, v| - values + (Array === v ? v : [v]) - end - else - nil - end - - when :literal - node[:value] - - when :current - value - - when :or - result = dispatch(node[:children][0], value) - if result.nil? or result.empty? - dispatch(node[:children][1], value) - else - result - end - - when :pipe - dispatch(node[:children][1], dispatch(node[:children][0], value)) - - when :multi_select_list - if value.nil? - value - else - node[:children].map { |n| dispatch(n, value) } - end - - when :multi_select_hash - if value.nil? - nil - else - node[:children].each.with_object({}) do |child, hash| - hash[child[:key]] = dispatch(child[:children][0], value) - end - end - - - when :comparator - left = dispatch(node[:children][0], value) - right = dispatch(node[:children][1], value) - case node[:relation] - when '==' then compare_values(left, right) - when '!=' then !compare_values(left, right) - when '>' then is_int(left) && is_int(right) && left > right - when '>=' then is_int(left) && is_int(right) && left >= right - when '<' then is_int(left) && is_int(right) && left < right - when '<=' then is_int(left) && is_int(right) && left <= right - end - - when :condition - true == dispatch(node[:children][0], value) ? - dispatch(node[:children][1], value) : - nil - - when :function - args = node[:children].map { |child| dispatch(child, value) } - send("function_#{node[:fn]}", *args) - - when :slice - function_slice(value, *node[:args]) - - when :expression - ExprNode.new(self, node[:children][0]) - - else - raise NotImplementedError - end - end - - def hash_like?(value) - Hash === value || Struct === value - end - - def projection(values, node) - values.inject([]) do |list, v| - list << dispatch(node[:children][1], v) - end.compact - end - - def function_abs(*args) - if args.count == 1 - value = args.first - else - raise Errors::InvalidArityError, "function abs() expects one argument" - end - if Numeric === value - value.abs - else - raise Errors::InvalidTypeError, "function abs() expects a number" - end - end - - def function_avg(*args) - if args.count == 1 - values = args.first - else - raise Errors::InvalidArityError, "function avg() expects one argument" - end - if Array === values - values.inject(0) do |total,n| - if Numeric === n - total + n - else - raise Errors::InvalidTypeError, "function avg() expects numeric values" - end - end / values.size.to_f - else - raise Errors::InvalidTypeError, "function avg() expects a number" - end - end - - def function_ceil(*args) - if args.count == 1 - value = args.first - else - raise Errors::InvalidArityError, "function ceil() expects one argument" - end - if Numeric === value - value.ceil - else - raise Errors::InvalidTypeError, "function ceil() expects a numeric value" - end - end - - def function_contains(*args) - if args.count == 2 - if String === args[0] || Array === args[0] - args[0].include?(args[1]) - else - raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list" - end - else - raise Errors::InvalidArityError, "function contains() expects 2 arguments" - end - end - - def function_floor(*args) - if args.count == 1 - value = args.first - else - raise Errors::InvalidArityError, "function floor() expects one argument" - end - if Numeric === value - value.floor - else - raise Errors::InvalidTypeError, "function floor() expects a numeric value" - end - end - - def function_length(*args) - if args.count == 1 - value = args.first - else - raise Errors::InvalidArityError, "function length() expects one argument" - end - case value - when Hash, Array, String then value.size - else raise Errors::InvalidTypeError, "function length() expects string, array or object" - end - end - - def function_max(*args) - if args.count == 1 - values = args.first - else - raise Errors::InvalidArityError, "function max() expects one argument" - end - if Array === values - values.inject(values.first) do |max, v| - if Numeric === v - v > max ? v : max - else - raise Errors::InvalidTypeError, "function max() expects numeric values" - end - end - else - raise Errors::InvalidTypeError, "function max() expects an array" - end - end - - def function_min(*args) - if args.count == 1 - values = args.first - else - raise Errors::InvalidArityError, "function min() expects one argument" - end - if Array === values - values.inject(values.first) do |min, v| - if Numeric === v - v < min ? v : min - else - raise Errors::InvalidTypeError, "function min() expects numeric values" - end - end - else - raise Errors::InvalidTypeError, "function min() expects an array" - end - end - - def function_type(*args) - if args.count == 1 - get_type(args.first) - else - raise Errors::InvalidArityError, "function type() expects one argument" - end - end - - def function_keys(*args) - if args.count == 1 - value = args.first - if hash_like?(value) - case value - when Hash then value.keys.map(&:to_s) - when Struct then value.members.map(&:to_s) - else raise NotImplementedError - end - else - raise Errors::InvalidTypeError, "function keys() expects a hash" - end - else - raise Errors::InvalidArityError, "function keys() expects one argument" - end - end - - def function_values(*args) - if args.count == 1 - value = args.first - if hash_like?(value) - value.values - elsif Array === value - value - else - raise Errors::InvalidTypeError, "function values() expects an array or a hash" - end - else - raise Errors::InvalidArityError, "function values() expects one argument" - end - end - - def function_join(*args) - if args.count == 2 - glue = args[0] - values = args[1] - if !(String === glue) - raise Errors::InvalidTypeError, "function join() expects the first argument to be a string" - elsif Array === values && values.all? { |v| String === v } - values.join(glue) - else - raise Errors::InvalidTypeError, "function join() expects values to be an array of strings" - end - else - raise Errors::InvalidArityError, "function join() expects an array of strings" - end - end - - def function_to_string(*args) - if args.count == 1 - value = args.first - String === value ? value : MultiJson.dump(value) - else - raise Errors::InvalidArityError, "function to_string() expects one argument" - end - end - - def function_to_number(*args) - if args.count == 1 - begin - value = Float(args.first) - Integer(value) === value ? value.to_i : value - rescue - nil - end - else - raise Errors::InvalidArityError, "function to_number() expects one argument" - end - end - - def function_sum(*args) - if args.count == 1 && Array === args.first - args.first.inject(0) do |sum,n| - if Numeric === n - sum + n - else - raise Errors::InvalidTypeError, "function sum() expects values to be numeric" - end - end - else - raise Errors::InvalidArityError, "function sum() expects one argument" - end - end - - def function_not_null(*args) - if args.count > 0 - args.find { |value| !value.nil? } - else - raise Errors::InvalidArityError, "function not_null() expects one or more arguments" - end - end - - def function_sort(*args) - if args.count == 1 - value = args.first - if Array === value - value.sort do |a, b| - a_type = get_type(a) - b_type = get_type(b) - if ['string', 'number'].include?(a_type) && a_type == b_type - a <=> b - else - raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" - end - end - else - raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" - end - else - raise Errors::InvalidArityError, "function sort() expects one argument" - end - end - - def function_sort_by(*args) - if args.count == 2 - if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' - values = args[0] - expression = args[1] - values.sort do |a,b| - a_value = expression.interpreter.visit(expression.node, a) - b_value = expression.interpreter.visit(expression.node, b) - a_type = get_type(a_value) - b_type = get_type(b_value) - if ['string', 'number'].include?(a_type) && a_type == b_type - a_value <=> b_value - else - raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" - end - end - else - raise Errors::InvalidTypeError, "function sort_by() expects an array and an expression" - end - else - raise Errors::InvalidArityError, "function sort_by() expects two arguments" - end - end - - def function_max_by(*args) - number_compare(:max, *args) - end - - def function_min_by(*args) - number_compare(:min, *args) - end - - def number_compare(mode, *args) - if args.count == 2 - if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' - values = args[0] - expression = args[1] - args[0].send("#{mode}_by") do |entry| - value = expression.interpreter.visit(expression.node, entry) - if get_type(value) == 'number' - value - else - raise Errors::InvalidTypeError, "function #{mode}_by() expects values to be an numbers" - end - end - else - raise Errors::InvalidTypeError, "function #{mode}_by() expects an array and an expression" - end - else - raise Errors::InvalidArityError, "function #{mode}_by() expects two arguments" - end - end - - def function_slice(values, *args) - if String === values || Array === values - _slice(values, *args) - else - nil - end - end - - def _slice(values, start, stop, step) - start, stop, step = _adjust_slice(values.size, start, stop, step) - result = [] - if step > 0 - i = start - while i < stop - result << values[i] - i += step - end - else - i = start - while i > stop - result << values[i] - i += step - end - end - String === values ? result.join : result - end - - def _adjust_slice(length, start, stop, step) - if step.nil? - step = 1 - elsif step == 0 - raise Errors::RuntimeError, 'slice step cannot be 0' - end - - if start.nil? - start = step < 0 ? length - 1 : 0 - else - start = _adjust_endpoint(length, start, step) - end - - if stop.nil? - stop = step < 0 ? -1 : length - else - stop = _adjust_endpoint(length, stop, step) - end - - [start, stop, step] - end - - def _adjust_endpoint(length, endpoint, step) - if endpoint < 0 - endpoint += length - endpoint = 0 if endpoint < 0 - endpoint - elsif endpoint >= length - step < 0 ? length - 1 : length - else - endpoint - end - end - - def compare_values(a, b) - if a == b - true - else - false - end - end - - def is_int(value) - Integer === value - end - - def get_type(value) - case - when ExprNode === value then 'expression' - when String === value then 'string' - when hash_like?(value) then 'object' - when Array === value then 'array' - when [true, false].include?(value) then 'boolean' - when value.nil? then 'null' - when Numeric === value then 'number' - end - end - - end -end From 6820e75ca7f07531604e782c517301eb2fa3d5a0 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 08:42:30 +0100 Subject: [PATCH 02/49] Inline comparisons and type checks in Comparator #compare_values is exactly #==, and it's not much more code to write x.is_a?(Integer) than is_int(x). --- lib/jmespath/nodes/comparator.rb | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 6ed0afb..86500a9 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -13,12 +13,12 @@ def visit(value) left = @children[0].visit(value) right = @children[1].visit(value) case @relation - when '==' then compare_values(left, right) - when '!=' then !compare_values(left, right) - when '>' then is_int(left) && is_int(right) && left > right - when '>=' then is_int(left) && is_int(right) && left >= right - when '<' then is_int(left) && is_int(right) && left < right - when '<=' then is_int(left) && is_int(right) && left <= right + when '==' then left == right + when '!=' then left != right + when '>' then left.is_a?(Integer) && right.is_a?(Integer) && left > right + when '>=' then left.is_a?(Integer) && right.is_a?(Integer) && left >= right + when '<' then left.is_a?(Integer) && right.is_a?(Integer) && left < right + when '<=' then left.is_a?(Integer) && right.is_a?(Integer) && left <= right end end @@ -29,20 +29,6 @@ def to_h :relation => @relation, } end - - private - - def compare_values(a, b) - if a == b - true - else - false - end - end - - def is_int(value) - Integer === value - end end end end From d98bb11bddf59a81db9b5f9902e1a82221433bad Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 08:46:32 +0100 Subject: [PATCH 03/49] Rewrite Condition to use an if instead of the ternary operator if/else tends to be much easier to read. The semantics of Condition changes slightly with this change, the test is no longer strictly for true, but for truthiness, but it looks like we're guaranteed to only get true or false so it doesn't seem to matter. --- lib/jmespath/nodes/condition.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 559bd96..642b30c 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -9,9 +9,11 @@ def initialize(children) end def visit(value) - true == @children[0].visit(value) ? - @children[1].visit(value) : + if @children[0].visit(value) + @children[1].visit(value) + else nil + end end def to_h From a8b90022a97c7b06a45dabf53da83e2ec5397650 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 08:47:50 +0100 Subject: [PATCH 04/49] Simplify the interpretation of Expression Get rid of ExprNode completely. --- lib/jmespath/nodes/expression.rb | 16 ++++------------ lib/jmespath/nodes/function.rb | 2 +- lib/jmespath/parser.rb | 2 +- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index cdaccce..accc457 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -2,14 +2,14 @@ module JMESPath # @api private module Nodes class Expression < Node - attr_reader :children + attr_reader :node - def initialize(children) - @children = children + def initialize(node) + @node = node end def visit(value) - ExprNode.new(@children[0]) + self end def to_h @@ -18,14 +18,6 @@ def to_h :children => @children.map(&:to_h), } end - - class ExprNode - attr_reader :node - - def initialize(node) - @node = node - end - end end end end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 5ad0edf..123873b 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -34,7 +34,7 @@ def method_missing(method_name, *args) def get_type(value) case - when Expression::ExprNode === value then 'expression' + when Expression === value then 'expression' when String === value then 'string' when hash_like?(value) then 'object' when Array === value then 'array' diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 8d52e9d..8bafacb 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -61,7 +61,7 @@ def nud_current(stream) def nud_expref(stream) stream.next - Nodes::Expression.new([expr(stream, 2)]) + Nodes::Expression.new(expr(stream, 2)) end def nud_filter(stream) From 7cc27298e0401221088e118e04c5561be3b4373d Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 08:50:17 +0100 Subject: [PATCH 05/49] Optimize Flatten Instead of creating a new array for each iteration just accumulate. --- lib/jmespath/nodes/flatten.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index 6e386bb..a0d5c31 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -11,8 +11,12 @@ def initialize(children) def visit(value) value = @children[0].visit(value) if Array === value - value.inject([]) do |values, v| - values + (Array === v ? v : [v]) + value.each_with_object([]) do |v, values| + if Array === v + values.concat(v) + else + values.push(v) + end end else nil From 666347abd9f9302b1163e981be35cdee1e2667d8 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 08:59:49 +0100 Subject: [PATCH 06/49] Inline #projection into #visit in Projection --- lib/jmespath/nodes/projection.rb | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index c11aff4..e2e6d2c 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -15,13 +15,18 @@ def visit(value) # the non-null results into the return value. left = @children[0].visit(value) if @from == :object && hash_like?(left) - projection(left.values) - elsif @from == :object && left == [] - projection(left) - elsif @from == :array && Array === left - projection(left) - else - nil + left = left.values + elsif !(@from == :object && left == EMPTY_ARRAY) && !(@from == :array && Array === left) + left = nil + end + if left + list = [] + left.each do |v| + if (vv = @children[1].visit(v)) + list << vv + end + end + list end end @@ -33,13 +38,7 @@ def to_h } end - private - - def projection(values) - values.inject([]) do |list, v| - list << @children[1].visit(v) - end.compact - end + EMPTY_ARRAY = [].freeze end end end From f729e40ccfe24215e8ac3ad0a7de13570b06f25f Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:03:11 +0100 Subject: [PATCH 07/49] Inline slicing logic in Slice#visit --- lib/jmespath/nodes/slice.rb | 57 ++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 2770429..e9105d0 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -9,7 +9,26 @@ def initialize(args) end def visit(value) - function_slice(value, *@args) + if String === value || Array === value + start, stop, step = adjust_slice(value.size, *@args) + result = [] + if step > 0 + i = start + while i < stop + result << value[i] + i += step + end + else + i = start + while i > stop + result << value[i] + i += step + end + end + String === value ? result.join : result + else + nil + end end def to_h @@ -21,34 +40,7 @@ def to_h private - def function_slice(values, *args) - if String === values || Array === values - _slice(values, *args) - else - nil - end - end - - def _slice(values, start, stop, step) - start, stop, step = _adjust_slice(values.size, start, stop, step) - result = [] - if step > 0 - i = start - while i < stop - result << values[i] - i += step - end - else - i = start - while i > stop - result << values[i] - i += step - end - end - String === values ? result.join : result - end - - def _adjust_slice(length, start, stop, step) + def adjust_slice(length, start, stop, step) if step.nil? step = 1 elsif step == 0 @@ -58,19 +50,19 @@ def _adjust_slice(length, start, stop, step) if start.nil? start = step < 0 ? length - 1 : 0 else - start = _adjust_endpoint(length, start, step) + start = adjust_endpoint(length, start, step) end if stop.nil? stop = step < 0 ? -1 : length else - stop = _adjust_endpoint(length, stop, step) + stop = adjust_endpoint(length, stop, step) end [start, stop, step] end - def _adjust_endpoint(length, endpoint, step) + def adjust_endpoint(length, endpoint, step) if endpoint < 0 endpoint += length endpoint = 0 if endpoint < 0 @@ -81,7 +73,6 @@ def _adjust_endpoint(length, endpoint, step) endpoint end end - end end end From 9d6ce38e3426532cc185e52c200659f86ed1781c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:09:50 +0100 Subject: [PATCH 08/49] Divide the nodes into nodes with children and leaf nodes --- lib/jmespath/nodes.rb | 11 +++++++++++ lib/jmespath/nodes/comparator.rb | 4 ++-- lib/jmespath/nodes/condition.rb | 6 ------ lib/jmespath/nodes/current.rb | 2 +- lib/jmespath/nodes/expression.rb | 2 +- lib/jmespath/nodes/field.rb | 2 +- lib/jmespath/nodes/flatten.rb | 6 ------ lib/jmespath/nodes/function.rb | 4 ++-- lib/jmespath/nodes/index.rb | 2 +- lib/jmespath/nodes/literal.rb | 2 +- lib/jmespath/nodes/multi_select_hash.rb | 6 ------ lib/jmespath/nodes/multi_select_list.rb | 6 ------ lib/jmespath/nodes/or.rb | 6 ------ lib/jmespath/nodes/pipe.rb | 6 ------ lib/jmespath/nodes/projection.rb | 4 ++-- lib/jmespath/nodes/slice.rb | 2 +- lib/jmespath/nodes/subexpression.rb | 6 ------ 17 files changed, 23 insertions(+), 54 deletions(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index d337af7..bad6946 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -2,6 +2,12 @@ module JMESPath # @api private module Nodes class Node + attr_reader :children + + def initialize(children) + @children = children + end + def visit(value) end @@ -10,6 +16,11 @@ def hash_like?(value) end end + class Leaf + def visit(value) + end + end + autoload :Comparator, 'jmespath/nodes/comparator' autoload :Condition, 'jmespath/nodes/condition' autoload :Current, 'jmespath/nodes/current' diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 86500a9..42d986a 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -2,10 +2,10 @@ module JMESPath # @api private module Nodes class Comparator < Node - attr_reader :children, :relation + attr_reader :relation def initialize(children, relation) - @children = children + super(children) @relation = relation end diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 642b30c..6809276 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class Condition < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) if @children[0].visit(value) @children[1].visit(value) diff --git a/lib/jmespath/nodes/current.rb b/lib/jmespath/nodes/current.rb index b6dd8d3..fe8a945 100644 --- a/lib/jmespath/nodes/current.rb +++ b/lib/jmespath/nodes/current.rb @@ -1,9 +1,9 @@ module JMESPath # @api private module Nodes - class Current < Node attr_reader :value + class Current < Leaf def visit(value) value end diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index accc457..ede0c5d 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Expression < Node + class Expression < Leaf attr_reader :node def initialize(node) diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index a4b7404..20d9d28 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Field < Node + class Field < Leaf attr_reader :key def initialize(key) diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index a0d5c31..b7145cf 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class Flatten < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) value = @children[0].visit(value) if Array === value diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 123873b..e9c0899 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -2,10 +2,10 @@ module JMESPath # @api private module Nodes class Function < Node - attr_reader :children, :fn + attr_reader :fn def initialize(children, fn) - @children = children + super(children) @fn = fn end diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index 7811f0a..ebc4aa2 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -1,9 +1,9 @@ module JMESPath # @api private module Nodes - class Index < Node attr_reader :index + class Index < Leaf def initialize(index) @index = index end diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb index 0b469d2..e178d65 100644 --- a/lib/jmespath/nodes/literal.rb +++ b/lib/jmespath/nodes/literal.rb @@ -1,9 +1,9 @@ module JMESPath # @api private module Nodes - class Literal < Node attr_reader :value + class Literal < Leaf def initialize(value) @value = value end diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index a90b3ef..485e04d 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class MultiSelectHash < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) if value.nil? nil diff --git a/lib/jmespath/nodes/multi_select_list.rb b/lib/jmespath/nodes/multi_select_list.rb index e7948aa..7106d9f 100644 --- a/lib/jmespath/nodes/multi_select_list.rb +++ b/lib/jmespath/nodes/multi_select_list.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class MultiSelectList < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) if value.nil? value diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb index d8c7d1a..b171a59 100644 --- a/lib/jmespath/nodes/or.rb +++ b/lib/jmespath/nodes/or.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class Or < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) result = @children[0].visit(value) if result.nil? or result.empty? diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb index 9cfdda5..f34690f 100644 --- a/lib/jmespath/nodes/pipe.rb +++ b/lib/jmespath/nodes/pipe.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class Pipe < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) @children[1].visit(@children[0].visit(value)) end diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index e2e6d2c..7b3e73d 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -2,10 +2,10 @@ module JMESPath # @api private module Nodes class Projection < Node - attr_reader :children, :from + attr_reader :from def initialize(children, from) - @children = children + super(children) @from = from end diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index e9105d0..1f2df85 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -1,9 +1,9 @@ module JMESPath # @api private module Nodes - class Slice < Node attr_reader :args + class Slice < Leaf def initialize(args) @args = args end diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index da00187..aaf6bf1 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -2,12 +2,6 @@ module JMESPath # @api private module Nodes class Subexpression < Node - attr_reader :children - - def initialize(children) - @children = children - end - def visit(value) @children[1].visit(@children[0].visit(value)) end From c853e20a62c4202e4559f60a5153e34c313fe507 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:10:37 +0100 Subject: [PATCH 09/49] Remove all unnecessary Node/Leaf accessors They're not used so there is no need for them to be there. --- lib/jmespath/nodes.rb | 2 -- lib/jmespath/nodes/comparator.rb | 2 -- lib/jmespath/nodes/current.rb | 2 -- lib/jmespath/nodes/function.rb | 2 -- lib/jmespath/nodes/index.rb | 2 -- lib/jmespath/nodes/literal.rb | 2 -- lib/jmespath/nodes/projection.rb | 2 -- lib/jmespath/nodes/slice.rb | 2 -- 8 files changed, 16 deletions(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index bad6946..e3df3d1 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -2,8 +2,6 @@ module JMESPath # @api private module Nodes class Node - attr_reader :children - def initialize(children) @children = children end diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 42d986a..a406039 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -2,8 +2,6 @@ module JMESPath # @api private module Nodes class Comparator < Node - attr_reader :relation - def initialize(children, relation) super(children) @relation = relation diff --git a/lib/jmespath/nodes/current.rb b/lib/jmespath/nodes/current.rb index fe8a945..a9b4e89 100644 --- a/lib/jmespath/nodes/current.rb +++ b/lib/jmespath/nodes/current.rb @@ -1,8 +1,6 @@ module JMESPath # @api private module Nodes - attr_reader :value - class Current < Leaf def visit(value) value diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index e9c0899..91e663d 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -2,8 +2,6 @@ module JMESPath # @api private module Nodes class Function < Node - attr_reader :fn - def initialize(children, fn) super(children) @fn = fn diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index ebc4aa2..7012879 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -1,8 +1,6 @@ module JMESPath # @api private module Nodes - attr_reader :index - class Index < Leaf def initialize(index) @index = index diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb index e178d65..777e38c 100644 --- a/lib/jmespath/nodes/literal.rb +++ b/lib/jmespath/nodes/literal.rb @@ -1,8 +1,6 @@ module JMESPath # @api private module Nodes - attr_reader :value - class Literal < Leaf def initialize(value) @value = value diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index 7b3e73d..b5ee023 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -2,8 +2,6 @@ module JMESPath # @api private module Nodes class Projection < Node - attr_reader :from - def initialize(children, from) super(children) @from = from diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 1f2df85..181429f 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -1,8 +1,6 @@ module JMESPath # @api private module Nodes - attr_reader :args - class Slice < Leaf def initialize(args) @args = args From 54bd3cf309014c4476af88cd811f88e5a9e09008 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:24:51 +0100 Subject: [PATCH 10/49] Don't re-use Field for function names The only reason Field has to have an accessor for #key is that it is used to hold the function name while parsing. This introduces a temporary object that will hold the name. --- lib/jmespath/nodes/field.rb | 2 -- lib/jmespath/nodes/function.rb | 8 ++++++++ lib/jmespath/parser.rb | 10 +++++++--- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index 20d9d28..c4ef0e5 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -2,8 +2,6 @@ module JMESPath # @api private module Nodes class Field < Leaf - attr_reader :key - def initialize(key) @key = key end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 91e663d..d83c77b 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -20,6 +20,14 @@ def to_h } end + class FunctionName + attr_reader :name + + def initialize(name) + @name = name + end + end + private def method_missing(method_name, *args) diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 8bafacb..d624f2d 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -74,8 +74,12 @@ def nud_flatten(stream) def nud_identifier(stream) token = stream.token - stream.next - Nodes::Field.new(token.value) + n = stream.next + if n.type == :lparen + Nodes::Function::FunctionName.new(token.value) + else + Nodes::Field.new(token.value) + end end def nud_lbrace(stream) @@ -182,7 +186,7 @@ def led_lbracket(stream, left) def led_lparen(stream, left) args = [] - name = left.key + name = left.name stream.next while stream.token.type != :rparen args << expr(stream, 0) From 348010bf9c1f8d08714dacf6015fb062b9b5fa0c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:48:09 +0100 Subject: [PATCH 11/49] Use #each_with_object instead of #each + #with_object --- lib/jmespath/nodes/multi_select_hash.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index 485e04d..b29323f 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -6,7 +6,7 @@ def visit(value) if value.nil? nil else - @children.each.with_object({}) do |child, hash| + @children.each_with_object({}) do |child, hash| hash[child.key] = child.children[0].visit(value) end end From f004a6c9528a7861bd85c5962dc77c1c6d7dbd4c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:45:49 +0100 Subject: [PATCH 12/49] Change Comparator to take explicit arguments It always has two children, so it makes more sense for it to name them than taking in an array --- lib/jmespath/nodes/comparator.rb | 25 +++++++++++++------------ lib/jmespath/parser.rb | 4 ++-- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index a406039..b031d86 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -1,29 +1,30 @@ module JMESPath # @api private module Nodes - class Comparator < Node - def initialize(children, relation) - super(children) + class Comparator < Leaf + def initialize(left, right, relation) + @left = left + @right = right @relation = relation end def visit(value) - left = @children[0].visit(value) - right = @children[1].visit(value) + left_value = @left.visit(value) + right_value = @right.visit(value) case @relation - when '==' then left == right - when '!=' then left != right - when '>' then left.is_a?(Integer) && right.is_a?(Integer) && left > right - when '>=' then left.is_a?(Integer) && right.is_a?(Integer) && left >= right - when '<' then left.is_a?(Integer) && right.is_a?(Integer) && left < right - when '<=' then left.is_a?(Integer) && right.is_a?(Integer) && left <= right + when '==' then left_value == right_value + when '!=' then left_value != right_value + when '>' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value + when '>=' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value + when '<' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value + when '<=' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value end end def to_h { :type => :comparator, - :children => @children.map(&:to_h), + :children => [@left.to_h, @right.to_h], :relation => @relation, } end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index d624f2d..ac4c86f 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -132,8 +132,8 @@ def nud_star(stream) def led_comparator(stream, left) token = stream.token stream.next - children = [left, expr(stream)] - Nodes::Comparator.new(children, token.value) + right = expr(stream) + Nodes::Comparator.new(left, right, token.value) end def led_dot(stream, left) From 19c25354edb51e25cd3474253dd8d697f9912ed1 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:46:01 +0100 Subject: [PATCH 13/49] Change Condition to take explicit arguments It always has two children, so it makes more sense for it to name them than taking in an array --- lib/jmespath/nodes/condition.rb | 13 +++++++++---- lib/jmespath/parser.rb | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 6809276..23a2a79 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -1,10 +1,15 @@ module JMESPath # @api private module Nodes - class Condition < Node + class Condition < Leaf + def initialize(test, child) + @test = test + @child = child + end + def visit(value) - if @children[0].visit(value) - @children[1].visit(value) + if @test.visit(value) + @child.visit(value) else nil end @@ -13,7 +18,7 @@ def visit(value) def to_h { :type => :condition, - :children => @children.map(&:to_h), + :children => [@test.to_h, @child.to_h], } end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index ac4c86f..454e1bb 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -156,7 +156,7 @@ def led_filter(stream, left) rhs = parse_projection(stream, Token::BINDING_POWER[:filter]) children = [ left ? left : CURRENT_NODE, - Nodes::Condition.new([expression, rhs]) + Nodes::Condition.new(expression, rhs) ] Nodes::Projection.new(children, :array) end From 4c7f84e4a5ac0ec3775e770ad972a901bada701e Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:47:27 +0100 Subject: [PATCH 14/49] Change Flatten to take explicit arguments It always has just a single child, so it makes more sense for it to name it than taking in an array --- lib/jmespath/nodes/flatten.rb | 10 +++++++--- lib/jmespath/parser.rb | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index b7145cf..3dc453e 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -1,9 +1,13 @@ module JMESPath # @api private module Nodes - class Flatten < Node + class Flatten < Leaf + def initialize(child) + @child = child + end + def visit(value) - value = @children[0].visit(value) + value = @child.visit(value) if Array === value value.each_with_object([]) do |v, values| if Array === v @@ -20,7 +24,7 @@ def visit(value) def to_h { :type => :flatten, - :children => @children.map(&:to_h), + :children => [@child.to_h], } end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 454e1bb..5c548f4 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -164,7 +164,7 @@ def led_filter(stream, left) def led_flatten(stream, left) stream.next children = [ - Nodes::Flatten.new([left]), + Nodes::Flatten.new(left), parse_projection(stream, Token::BINDING_POWER[:flatten]) ] Nodes::Projection.new(children, :array) From 765bf108bb03f0d34681cfab43d514c3f2e94ca1 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:50:10 +0100 Subject: [PATCH 15/49] Change KeyValuePair to take explicit arguments It always has a value, so it makes more sense for it to name it than taking in an array --- lib/jmespath/nodes/multi_select_hash.rb | 12 ++++++------ lib/jmespath/parser.rb | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index b29323f..6587698 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -6,8 +6,8 @@ def visit(value) if value.nil? nil else - @children.each_with_object({}) do |child, hash| - hash[child.key] = child.children[0].visit(value) + @children.each_with_object({}) do |pair, hash| + hash[pair.key] = pair.value.visit(value) end end end @@ -20,17 +20,17 @@ def to_h end class KeyValuePair - attr_reader :children, :key + attr_reader :key, :value - def initialize(children, key) - @children = children + def initialize(key, value) @key = key + @value = value end def to_h { :type => :key_value_pair, - :children => @children.map(&:to_h), + :children => [@value.to_h], :key => @key, } end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 5c548f4..0e1ca4e 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -244,7 +244,7 @@ def parse_key_value_pair(stream) key = stream.token.value stream.next(match:Set.new([:colon])) stream.next - Nodes::MultiSelectHash::KeyValuePair.new([expr(stream)], key) + Nodes::MultiSelectHash::KeyValuePair.new(key, expr(stream)) end def parse_multi_select_list(stream) From 727d837ee8bcda58a7e00528d8a670bbc32c304e Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:51:41 +0100 Subject: [PATCH 16/49] Change Or to take explicit arguments It always has two children, so it makes more sense for it to name them than taking in an array --- lib/jmespath/nodes/or.rb | 13 +++++++++---- lib/jmespath/parser.rb | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb index b171a59..1b370cc 100644 --- a/lib/jmespath/nodes/or.rb +++ b/lib/jmespath/nodes/or.rb @@ -1,11 +1,16 @@ module JMESPath # @api private module Nodes - class Or < Node + class Or < Leaf + def initialize(left, right) + @left = left + @right = right + end + def visit(value) - result = @children[0].visit(value) + result = @left.visit(value) if result.nil? or result.empty? - @children[1].visit(value) + @right.visit(value) else result end @@ -14,7 +19,7 @@ def visit(value) def to_h { :type => :or, - :children => @children.map(&:to_h), + :children => [@left.to_h, @right.to_h], } end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 0e1ca4e..4a310a4 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -200,8 +200,8 @@ def led_lparen(stream, left) def led_or(stream, left) stream.next - children = [left, expr(stream, Token::BINDING_POWER[:or])] - Nodes::Or.new(children) + right = expr(stream, Token::BINDING_POWER[:or]) + Nodes::Or.new(left, right) end def led_pipe(stream, left) From cd38ec0fe8229bded90f4c59dd72a8d97891cad8 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 09:53:27 +0100 Subject: [PATCH 17/49] Change Pipe to take explicit arguments It always has two children, so it makes more sense for it to name them than taking in an array --- lib/jmespath/nodes/pipe.rb | 11 ++++++++--- lib/jmespath/parser.rb | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb index f34690f..40922e1 100644 --- a/lib/jmespath/nodes/pipe.rb +++ b/lib/jmespath/nodes/pipe.rb @@ -1,15 +1,20 @@ module JMESPath # @api private module Nodes - class Pipe < Node + class Pipe < Leaf + def initialize(left, right) + @left = left + @right = right + end + def visit(value) - @children[1].visit(@children[0].visit(value)) + @right.visit(@left.visit(value)) end def to_h { :type => :pipe, - :children => @children.map(&:to_h), + :children => [@left.to_h, @right.to_h], } end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 4a310a4..40d0cb0 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -206,8 +206,8 @@ def led_or(stream, left) def led_pipe(stream, left) stream.next - children = [left, expr(stream, Token::BINDING_POWER[:pipe])] - Nodes::Pipe.new(children) + right = expr(stream, Token::BINDING_POWER[:pipe]) + Nodes::Pipe.new(left, right) end def parse_array_index_expression(stream) From be89ffa7628fff5adfdbe6f25452a8fb930ac009 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 10:01:42 +0100 Subject: [PATCH 18/49] Change Projection to take explicit arguments It always has two children, so it makes more sense for it to name them than taking in an array This temporarily moves #hash_like? into Leaf, but the Node/Leaf thing will be refactored soon. --- lib/jmespath/nodes.rb | 4 ++++ lib/jmespath/nodes/projection.rb | 25 +++++++++++++------------ lib/jmespath/parser.rb | 32 ++++++++++++-------------------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index e3df3d1..22a0481 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -17,6 +17,10 @@ def hash_like?(value) class Leaf def visit(value) end + + def hash_like?(value) + Hash === value || Struct === value + end end autoload :Comparator, 'jmespath/nodes/comparator' diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index b5ee023..f71ccec 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -1,9 +1,10 @@ module JMESPath # @api private module Nodes - class Projection < Node - def initialize(children, from) - super(children) + class Projection < Leaf + def initialize(left, right, from) + @left = left + @right = right @from = from end @@ -11,16 +12,16 @@ def visit(value) # Interprets a projection node, passing the values of the left # child through the values of the right child and aggregating # the non-null results into the return value. - left = @children[0].visit(value) - if @from == :object && hash_like?(left) - left = left.values - elsif !(@from == :object && left == EMPTY_ARRAY) && !(@from == :array && Array === left) - left = nil + left_value = @left.visit(value) + if @from == :object && hash_like?(left_value) + left_value = left_value.values + elsif !(@from == :object && left_value == EMPTY_ARRAY) && !(@from == :array && Array === left_value) + left_value = nil end - if left + if left_value list = [] - left.each do |v| - if (vv = @children[1].visit(v)) + left_value.each do |v| + if (vv = @right.visit(v)) list << vv end end @@ -31,7 +32,7 @@ def visit(value) def to_h { :type => :projection, - :children => @children.map(&:to_h), + :children => [@left.to_h, @right.to_h], :from => @from, } end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 40d0cb0..25bf428 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -154,20 +154,16 @@ def led_filter(stream, left) end stream.next rhs = parse_projection(stream, Token::BINDING_POWER[:filter]) - children = [ - left ? left : CURRENT_NODE, - Nodes::Condition.new(expression, rhs) - ] - Nodes::Projection.new(children, :array) + left ||= CURRENT_NODE + right = Nodes::Condition.new(expression, rhs) + Nodes::Projection.new(left, right, :array) end def led_flatten(stream, left) stream.next - children = [ - Nodes::Flatten.new(left), - parse_projection(stream, Token::BINDING_POWER[:flatten]) - ] - Nodes::Projection.new(children, :array) + left = Nodes::Flatten.new(left) + right = parse_projection(stream, Token::BINDING_POWER[:flatten]) + Nodes::Projection.new(left, right, :array) end def led_lbracket(stream, left) @@ -279,20 +275,16 @@ def parse_projection(stream, binding_power) def parse_wildcard_array(stream, left = nil) stream.next(match:Set.new([:rbracket])) stream.next - children = [ - left ? left : CURRENT_NODE, - parse_projection(stream, Token::BINDING_POWER[:star]) - ] - Nodes::Projection.new(children, :array) + left ||= CURRENT_NODE + right = parse_projection(stream, Token::BINDING_POWER[:star]) + Nodes::Projection.new(left, right, :array) end def parse_wildcard_object(stream, left = nil) stream.next - children = [ - left ? left : CURRENT_NODE, - parse_projection(stream, Token::BINDING_POWER[:star]) - ] - Nodes::Projection.new(children, :object) + left ||= CURRENT_NODE + right = parse_projection(stream, Token::BINDING_POWER[:star]) + Nodes::Projection.new(left, right, :object) end end From 08be9986eff4999c1906014c44b7b836d63c78de Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 10:03:35 +0100 Subject: [PATCH 19/49] Change Subexpression to take explicit arguments It always has two children, so it makes more sense for it to name them than taking in an array --- lib/jmespath/nodes/subexpression.rb | 11 ++++++++--- lib/jmespath/parser.rb | 11 ++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index aaf6bf1..b4f235b 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -1,15 +1,20 @@ module JMESPath # @api private module Nodes - class Subexpression < Node + class Subexpression < Leaf + def initialize(left, right) + @left = left + @right = right + end + def visit(value) - @children[1].visit(@children[0].visit(value)) + @right.visit(@left.visit(value)) end def to_h { :type => :subexpression, - :children => @children.map(&:to_h), + :children => [@left.to_h, @right.to_h], } end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 25bf428..6095385 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -141,8 +141,8 @@ def led_dot(stream, left) if stream.token.type == :star parse_wildcard_object(stream, left) else - children = [left, parse_dot(stream, Token::BINDING_POWER[:dot])] - Nodes::Subexpression.new(children) + right = parse_dot(stream, Token::BINDING_POWER[:dot]) + Nodes::Subexpression.new(left, right) end end @@ -170,11 +170,8 @@ def led_lbracket(stream, left) stream.next(match: Set.new([:number, :colon, :star])) type = stream.token.type if type == :number || type == :colon - children = [ - left, - parse_array_index_expression(stream) - ] - Nodes::Subexpression.new(children) + right = parse_array_index_expression(stream) + Nodes::Subexpression.new(left, right) else parse_wildcard_array(stream, left) end From 5c223925831da65122aeb6e456a1a7ac961fb525 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 10:07:07 +0100 Subject: [PATCH 20/49] Get rid of Leaf, everything is a Node Node was introduced for things that had an arbitrary number of children, but most things actually don't, it only looked that way. --- lib/jmespath/nodes.rb | 13 ------------- lib/jmespath/nodes/comparator.rb | 2 +- lib/jmespath/nodes/condition.rb | 2 +- lib/jmespath/nodes/current.rb | 2 +- lib/jmespath/nodes/expression.rb | 2 +- lib/jmespath/nodes/field.rb | 2 +- lib/jmespath/nodes/flatten.rb | 2 +- lib/jmespath/nodes/function.rb | 2 +- lib/jmespath/nodes/index.rb | 2 +- lib/jmespath/nodes/literal.rb | 2 +- lib/jmespath/nodes/multi_select_hash.rb | 4 ++++ lib/jmespath/nodes/multi_select_list.rb | 4 ++++ lib/jmespath/nodes/or.rb | 2 +- lib/jmespath/nodes/pipe.rb | 2 +- lib/jmespath/nodes/projection.rb | 2 +- lib/jmespath/nodes/slice.rb | 2 +- lib/jmespath/nodes/subexpression.rb | 2 +- 17 files changed, 22 insertions(+), 27 deletions(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index 22a0481..d337af7 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -2,19 +2,6 @@ module JMESPath # @api private module Nodes class Node - def initialize(children) - @children = children - end - - def visit(value) - end - - def hash_like?(value) - Hash === value || Struct === value - end - end - - class Leaf def visit(value) end diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index b031d86..b5eb7bf 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Comparator < Leaf + class Comparator < Node def initialize(left, right, relation) @left = left @right = right diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 23a2a79..7f05767 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Condition < Leaf + class Condition < Node def initialize(test, child) @test = test @child = child diff --git a/lib/jmespath/nodes/current.rb b/lib/jmespath/nodes/current.rb index a9b4e89..36f026a 100644 --- a/lib/jmespath/nodes/current.rb +++ b/lib/jmespath/nodes/current.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Current < Leaf + class Current < Node def visit(value) value end diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index ede0c5d..accc457 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Expression < Leaf + class Expression < Node attr_reader :node def initialize(node) diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index c4ef0e5..5031e44 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Field < Leaf + class Field < Node def initialize(key) @key = key end diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index 3dc453e..b8512bc 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Flatten < Leaf + class Flatten < Node def initialize(child) @child = child end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index d83c77b..4ef7bbf 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -3,7 +3,7 @@ module JMESPath module Nodes class Function < Node def initialize(children, fn) - super(children) + @children = children @fn = fn end diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index 7012879..c9975ff 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Index < Leaf + class Index < Node def initialize(index) @index = index end diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb index 777e38c..f99c003 100644 --- a/lib/jmespath/nodes/literal.rb +++ b/lib/jmespath/nodes/literal.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Literal < Leaf + class Literal < Node def initialize(value) @value = value end diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index 6587698..1c43439 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -2,6 +2,10 @@ module JMESPath # @api private module Nodes class MultiSelectHash < Node + def initialize(children) + @children = children + end + def visit(value) if value.nil? nil diff --git a/lib/jmespath/nodes/multi_select_list.rb b/lib/jmespath/nodes/multi_select_list.rb index 7106d9f..fb80193 100644 --- a/lib/jmespath/nodes/multi_select_list.rb +++ b/lib/jmespath/nodes/multi_select_list.rb @@ -2,6 +2,10 @@ module JMESPath # @api private module Nodes class MultiSelectList < Node + def initialize(children) + @children = children + end + def visit(value) if value.nil? value diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb index 1b370cc..1a2b756 100644 --- a/lib/jmespath/nodes/or.rb +++ b/lib/jmespath/nodes/or.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Or < Leaf + class Or < Node def initialize(left, right) @left = left @right = right diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb index 40922e1..7035cbc 100644 --- a/lib/jmespath/nodes/pipe.rb +++ b/lib/jmespath/nodes/pipe.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Pipe < Leaf + class Pipe < Node def initialize(left, right) @left = left @right = right diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index f71ccec..f09fd13 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Projection < Leaf + class Projection < Node def initialize(left, right, from) @left = left @right = right diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 181429f..449462b 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Slice < Leaf + class Slice < Node def initialize(args) @args = args end diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index b4f235b..6731128 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -1,7 +1,7 @@ module JMESPath # @api private module Nodes - class Subexpression < Leaf + class Subexpression < Node def initialize(left, right) @left = left @right = right From 85302472c7e5e4a8d171c97e3d55d9cd016c3419 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 10:21:54 +0100 Subject: [PATCH 21/49] Break Projection apart into {Array,Object}Protection This avoids branching at runtime. --- lib/jmespath/nodes.rb | 2 ++ lib/jmespath/nodes/projection.rb | 41 ++++++++++++++++++++++++-------- lib/jmespath/parser.rb | 8 +++---- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index d337af7..b1d61bc 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -24,6 +24,8 @@ def hash_like?(value) autoload :Or, 'jmespath/nodes/or' autoload :Pipe, 'jmespath/nodes/pipe' autoload :Projection, 'jmespath/nodes/projection' + autoload :ArrayProjection, 'jmespath/nodes/projection' + autoload :ObjectProjection, 'jmespath/nodes/projection' autoload :Slice, 'jmespath/nodes/slice' autoload :Subexpression, 'jmespath/nodes/subexpression' end diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index f09fd13..0027949 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -2,25 +2,18 @@ module JMESPath # @api private module Nodes class Projection < Node - def initialize(left, right, from) + def initialize(left, right) @left = left @right = right - @from = from end def visit(value) # Interprets a projection node, passing the values of the left # child through the values of the right child and aggregating # the non-null results into the return value. - left_value = @left.visit(value) - if @from == :object && hash_like?(left_value) - left_value = left_value.values - elsif !(@from == :object && left_value == EMPTY_ARRAY) && !(@from == :array && Array === left_value) - left_value = nil - end - if left_value + if (projectees = extract_projectees(@left.visit(value))) list = [] - left_value.each do |v| + projectees.each do |v| if (vv = @right.visit(v)) list << vv end @@ -37,7 +30,35 @@ def to_h } end + private + + def extract_projectees(left_value) + nil + end + end + + class ArrayProjection < Projection + def extract_projectees(left_value) + if Array === left_value + left_value + else + nil + end + end + end + + class ObjectProjection < Projection EMPTY_ARRAY = [].freeze + + def extract_projectees(left_value) + if hash_like?(left_value) + left_value.values + elsif left_value == EMPTY_ARRAY + left_value + else + nil + end + end end end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 6095385..cff1c6b 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -156,14 +156,14 @@ def led_filter(stream, left) rhs = parse_projection(stream, Token::BINDING_POWER[:filter]) left ||= CURRENT_NODE right = Nodes::Condition.new(expression, rhs) - Nodes::Projection.new(left, right, :array) + Nodes::ArrayProjection.new(left, right) end def led_flatten(stream, left) stream.next left = Nodes::Flatten.new(left) right = parse_projection(stream, Token::BINDING_POWER[:flatten]) - Nodes::Projection.new(left, right, :array) + Nodes::ArrayProjection.new(left, right) end def led_lbracket(stream, left) @@ -274,14 +274,14 @@ def parse_wildcard_array(stream, left = nil) stream.next left ||= CURRENT_NODE right = parse_projection(stream, Token::BINDING_POWER[:star]) - Nodes::Projection.new(left, right, :array) + Nodes::ArrayProjection.new(left, right) end def parse_wildcard_object(stream, left = nil) stream.next left ||= CURRENT_NODE right = parse_projection(stream, Token::BINDING_POWER[:star]) - Nodes::Projection.new(left, right, :object) + Nodes::ObjectProjection.new(left, right) end end From 3fa270fbac9e2c9bd3e81bdbc6033702827a4bba Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 10:27:48 +0100 Subject: [PATCH 22/49] Break apart Comparator into {Eq,Neq,Gt,Gte,Lt,Lte}Comparator This avoids branching at runtime. --- lib/jmespath/nodes/comparator.rb | 70 ++++++++++++++++++++++++++------ lib/jmespath/parser.rb | 2 +- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index b5eb7bf..99518a1 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -2,23 +2,27 @@ module JMESPath # @api private module Nodes class Comparator < Node - def initialize(left, right, relation) + def initialize(left, right) @left = left @right = right - @relation = relation end - def visit(value) - left_value = @left.visit(value) - right_value = @right.visit(value) - case @relation - when '==' then left_value == right_value - when '!=' then left_value != right_value - when '>' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value - when '>=' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value - when '<' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value - when '<=' then left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value + def self.create(relation, left, right) + type = begin + case relation + when '==' then EqComparator + when '!=' then NeqComparator + when '>' then GtComparator + when '>=' then GteComparator + when '<' then LtComparator + when '<=' then LteComparator + end end + type.new(left, right) + end + + def visit(value) + check(@left.visit(value), @right.visit(value)) end def to_h @@ -28,6 +32,48 @@ def to_h :relation => @relation, } end + + private + + def check(left_value, right_value) + nil + end + end + + class EqComparator < Comparator + def check(left_value, right_value) + left_value == right_value + end + end + + class NeqComparator < Comparator + def check(left_value, right_value) + left_value != right_value + end + end + + class GtComparator < Comparator + def check(left_value, right_value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value + end + end + + class GteComparator < Comparator + def check(left_value, right_value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value + end + end + + class LtComparator < Comparator + def check(left_value, right_value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value + end + end + + class LteComparator < Comparator + def check(left_value, right_value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value + end end end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index cff1c6b..481b7c6 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -133,7 +133,7 @@ def led_comparator(stream, left) token = stream.token stream.next right = expr(stream) - Nodes::Comparator.new(left, right, token.value) + Nodes::Comparator.create(token.value, left, right) end def led_dot(stream, left) From c016b843eb8c9ffa003f39b871feff47c2e0fc74 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 12:27:28 +0100 Subject: [PATCH 23/49] Break apart Function into subclasses for each function This avoids using #send at runtime. --- lib/jmespath/nodes/function.rb | 144 ++++++++++++++++++++++++++------- lib/jmespath/parser.rb | 2 +- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 4ef7bbf..1006e18 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -2,14 +2,22 @@ module JMESPath # @api private module Nodes class Function < Node - def initialize(children, fn) + FUNCTIONS = {} + + def initialize(children) @children = children - @fn = fn + end + + def self.create(name, children) + if (type = FUNCTIONS[name]) + type.new(children) + else + raise Errors::UnknownFunctionError, "unknown function #{name}()" + end end def visit(value) - args = @children.map { |child| child.visit(value) } - send("function_#{@fn}", *args) + call(@children.map { |child| child.visit(value) }) end def to_h @@ -30,12 +38,8 @@ def initialize(name) private - def method_missing(method_name, *args) - if matches = method_name.to_s.match(/^function_(.*)/) - raise Errors::UnknownFunctionError, "unknown function #{matches[1]}()" - else - super - end + def call(args) + nil end def get_type(value) @@ -70,8 +74,12 @@ def number_compare(mode, *args) raise Errors::InvalidArityError, "function #{mode}_by() expects two arguments" end end + end + + class AbsFunction < Function + FUNCTIONS['abs'] = self - def function_abs(*args) + def call(args) if args.count == 1 value = args.first else @@ -83,8 +91,12 @@ def function_abs(*args) raise Errors::InvalidTypeError, "function abs() expects a number" end end + end + + class AvgFunction < Function + FUNCTIONS['avg'] = self - def function_avg(*args) + def call(args) if args.count == 1 values = args.first else @@ -102,8 +114,12 @@ def function_avg(*args) raise Errors::InvalidTypeError, "function avg() expects a number" end end + end - def function_ceil(*args) + class CeilFunction < Function + FUNCTIONS['ceil'] = self + + def call(args) if args.count == 1 value = args.first else @@ -115,8 +131,12 @@ def function_ceil(*args) raise Errors::InvalidTypeError, "function ceil() expects a numeric value" end end + end + + class ContainsFunction < Function + FUNCTIONS['contains'] = self - def function_contains(*args) + def call(args) if args.count == 2 if String === args[0] || Array === args[0] args[0].include?(args[1]) @@ -127,8 +147,12 @@ def function_contains(*args) raise Errors::InvalidArityError, "function contains() expects 2 arguments" end end + end + + class FloorFunction < Function + FUNCTIONS['floor'] = self - def function_floor(*args) + def call(args) if args.count == 1 value = args.first else @@ -140,8 +164,12 @@ def function_floor(*args) raise Errors::InvalidTypeError, "function floor() expects a numeric value" end end + end - def function_length(*args) + class LengthFunction < Function + FUNCTIONS['length'] = self + + def call(args) if args.count == 1 value = args.first else @@ -152,8 +180,12 @@ def function_length(*args) else raise Errors::InvalidTypeError, "function length() expects string, array or object" end end + end + + class MaxFunction < Function + FUNCTIONS['max'] = self - def function_max(*args) + def call(args) if args.count == 1 values = args.first else @@ -171,8 +203,12 @@ def function_max(*args) raise Errors::InvalidTypeError, "function max() expects an array" end end + end + + class MinFunction < Function + FUNCTIONS['min'] = self - def function_min(*args) + def call(args) if args.count == 1 values = args.first else @@ -190,16 +226,24 @@ def function_min(*args) raise Errors::InvalidTypeError, "function min() expects an array" end end + end - def function_type(*args) + class TypeFunction < Function + FUNCTIONS['type'] = self + + def call(args) if args.count == 1 get_type(args.first) else raise Errors::InvalidArityError, "function type() expects one argument" end end + end + + class KeysFunction < Function + FUNCTIONS['keys'] = self - def function_keys(*args) + def call(args) if args.count == 1 value = args.first if hash_like?(value) @@ -215,8 +259,12 @@ def function_keys(*args) raise Errors::InvalidArityError, "function keys() expects one argument" end end + end + + class ValuesFunction < Function + FUNCTIONS['values'] = self - def function_values(*args) + def call(args) if args.count == 1 value = args.first if hash_like?(value) @@ -230,8 +278,12 @@ def function_values(*args) raise Errors::InvalidArityError, "function values() expects one argument" end end + end - def function_join(*args) + class JoinFunction < Function + FUNCTIONS['join'] = self + + def call(args) if args.count == 2 glue = args[0] values = args[1] @@ -246,8 +298,12 @@ def function_join(*args) raise Errors::InvalidArityError, "function join() expects an array of strings" end end + end + + class ToStringFunction < Function + FUNCTIONS['to_string'] = self - def function_to_string(*args) + def call(args) if args.count == 1 value = args.first String === value ? value : MultiJson.dump(value) @@ -255,8 +311,12 @@ def function_to_string(*args) raise Errors::InvalidArityError, "function to_string() expects one argument" end end + end + + class ToNumberFunction < Function + FUNCTIONS['to_number'] = self - def function_to_number(*args) + def call(args) if args.count == 1 begin value = Float(args.first) @@ -268,8 +328,12 @@ def function_to_number(*args) raise Errors::InvalidArityError, "function to_number() expects one argument" end end + end - def function_sum(*args) + class SumFunction < Function + FUNCTIONS['sum'] = self + + def call(args) if args.count == 1 && Array === args.first args.first.inject(0) do |sum,n| if Numeric === n @@ -282,16 +346,24 @@ def function_sum(*args) raise Errors::InvalidArityError, "function sum() expects one argument" end end + end + + class NotNullFunction < Function + FUNCTIONS['not_null'] = self - def function_not_null(*args) + def call(args) if args.count > 0 args.find { |value| !value.nil? } else raise Errors::InvalidArityError, "function not_null() expects one or more arguments" end end + end + + class SortFunction < Function + FUNCTIONS['sort'] = self - def function_sort(*args) + def call(args) if args.count == 1 value = args.first if Array === value @@ -311,8 +383,12 @@ def function_sort(*args) raise Errors::InvalidArityError, "function sort() expects one argument" end end + end - def function_sort_by(*args) + class SortByFunction < Function + FUNCTIONS['sort_by'] = self + + def call(args) if args.count == 2 if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' values = args[0] @@ -335,12 +411,20 @@ def function_sort_by(*args) raise Errors::InvalidArityError, "function sort_by() expects two arguments" end end + end + + class MaxByFunction < Function + FUNCTIONS['max_by'] = self - def function_max_by(*args) + def call(args) number_compare(:max, *args) end + end + + class MinByFunction < Function + FUNCTIONS['min_by'] = self - def function_min_by(*args) + def call(args) number_compare(:min, *args) end end diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index 481b7c6..a0dd1ed 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -188,7 +188,7 @@ def led_lparen(stream, left) end end stream.next - Nodes::Function.new(args, name) + Nodes::Function.create(name, args) end def led_or(stream, left) From 36eb6e9b07276b79dea34d21e145d884bf5d6b5e Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 12:31:26 +0100 Subject: [PATCH 24/49] Optimize type detection in Function This inlines #hash_like?, which might be unfortunate, but it means that we can avoid creating arrays and check each branch twice, and a few other things. --- lib/jmespath/nodes/function.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 1006e18..9367086 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -43,14 +43,14 @@ def call(args) end def get_type(value) - case - when Expression === value then 'expression' - when String === value then 'string' - when hash_like?(value) then 'object' - when Array === value then 'array' - when [true, false].include?(value) then 'boolean' - when value.nil? then 'null' - when Numeric === value then 'number' + case value + when String then 'string' + when true, false then 'boolean' + when nil then 'null' + when Numeric then 'number' + when Hash, Struct then 'object' + when Array then 'array' + when Expression then 'expression' end end From da83fc4ee181c11913d026227e6b15753ce78b89 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 12:33:05 +0100 Subject: [PATCH 25/49] Remove #to_h from all AST nodes It was only used temporarily for checking compatibility with the old TreeInterpreter --- lib/jmespath/nodes/comparator.rb | 8 -------- lib/jmespath/nodes/condition.rb | 7 ------- lib/jmespath/nodes/current.rb | 6 ------ lib/jmespath/nodes/expression.rb | 7 ------- lib/jmespath/nodes/field.rb | 7 ------- lib/jmespath/nodes/flatten.rb | 7 ------- lib/jmespath/nodes/function.rb | 8 -------- lib/jmespath/nodes/index.rb | 7 ------- lib/jmespath/nodes/literal.rb | 7 ------- lib/jmespath/nodes/multi_select_hash.rb | 15 --------------- lib/jmespath/nodes/multi_select_list.rb | 7 ------- lib/jmespath/nodes/or.rb | 7 ------- lib/jmespath/nodes/pipe.rb | 7 ------- lib/jmespath/nodes/projection.rb | 8 -------- lib/jmespath/nodes/slice.rb | 7 ------- lib/jmespath/nodes/subexpression.rb | 7 ------- 16 files changed, 122 deletions(-) diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 99518a1..74389d9 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -25,14 +25,6 @@ def visit(value) check(@left.visit(value), @right.visit(value)) end - def to_h - { - :type => :comparator, - :children => [@left.to_h, @right.to_h], - :relation => @relation, - } - end - private def check(left_value, right_value) diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 7f05767..36b62a0 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -14,13 +14,6 @@ def visit(value) nil end end - - def to_h - { - :type => :condition, - :children => [@test.to_h, @child.to_h], - } - end end end end diff --git a/lib/jmespath/nodes/current.rb b/lib/jmespath/nodes/current.rb index 36f026a..8e37ac3 100644 --- a/lib/jmespath/nodes/current.rb +++ b/lib/jmespath/nodes/current.rb @@ -5,12 +5,6 @@ class Current < Node def visit(value) value end - - def to_h - { - :type => :current, - } - end end end end diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index accc457..1ff6841 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -11,13 +11,6 @@ def initialize(node) def visit(value) self end - - def to_h - { - :type => :expression, - :children => @children.map(&:to_h), - } - end end end end diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index 5031e44..e5a37fd 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -13,13 +13,6 @@ def visit(value) else nil end end - - def to_h - { - :type => :field, - :key => @key, - } - end end end end diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index b8512bc..4bf6ad4 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -20,13 +20,6 @@ def visit(value) nil end end - - def to_h - { - :type => :flatten, - :children => [@child.to_h], - } - end end end end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 9367086..57f5fb8 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -20,14 +20,6 @@ def visit(value) call(@children.map { |child| child.visit(value) }) end - def to_h - { - :type => :function, - :children => @children.map(&:to_h), - :fn => @fn, - } - end - class FunctionName attr_reader :name diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index c9975ff..b23e8ad 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -13,13 +13,6 @@ def visit(value) nil end end - - def to_h - { - :type => :index, - :index => @index, - } - end end end end diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb index f99c003..d8ecdd6 100644 --- a/lib/jmespath/nodes/literal.rb +++ b/lib/jmespath/nodes/literal.rb @@ -9,13 +9,6 @@ def initialize(value) def visit(value) @value end - - def to_h - { - :type => :literal, - :value => @value, - } - end end end end diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index 1c43439..1058e58 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -16,13 +16,6 @@ def visit(value) end end - def to_h - { - :type => :multi_select_hash, - :children => @children.map(&:to_h), - } - end - class KeyValuePair attr_reader :key, :value @@ -30,14 +23,6 @@ def initialize(key, value) @key = key @value = value end - - def to_h - { - :type => :key_value_pair, - :children => [@value.to_h], - :key => @key, - } - end end end end diff --git a/lib/jmespath/nodes/multi_select_list.rb b/lib/jmespath/nodes/multi_select_list.rb index fb80193..81c337c 100644 --- a/lib/jmespath/nodes/multi_select_list.rb +++ b/lib/jmespath/nodes/multi_select_list.rb @@ -13,13 +13,6 @@ def visit(value) @children.map { |n| n.visit(value) } end end - - def to_h - { - :type => :multi_select_list, - :children => @children.map(&:to_h), - } - end end end end diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb index 1a2b756..2dbf8c6 100644 --- a/lib/jmespath/nodes/or.rb +++ b/lib/jmespath/nodes/or.rb @@ -15,13 +15,6 @@ def visit(value) result end end - - def to_h - { - :type => :or, - :children => [@left.to_h, @right.to_h], - } - end end end end diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb index 7035cbc..3e2c9aa 100644 --- a/lib/jmespath/nodes/pipe.rb +++ b/lib/jmespath/nodes/pipe.rb @@ -10,13 +10,6 @@ def initialize(left, right) def visit(value) @right.visit(@left.visit(value)) end - - def to_h - { - :type => :pipe, - :children => [@left.to_h, @right.to_h], - } - end end end end diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index 0027949..8670156 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -22,14 +22,6 @@ def visit(value) end end - def to_h - { - :type => :projection, - :children => [@left.to_h, @right.to_h], - :from => @from, - } - end - private def extract_projectees(left_value) diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 449462b..1a8eecb 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -29,13 +29,6 @@ def visit(value) end end - def to_h - { - :type => :slice, - :args => @args, - } - end - private def adjust_slice(length, start, stop, step) diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index 6731128..5c32535 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -10,13 +10,6 @@ def initialize(left, right) def visit(value) @right.visit(@left.visit(value)) end - - def to_h - { - :type => :subexpression, - :children => [@left.to_h, @right.to_h], - } - end end end end From 235f32219e897d9caa34f25b02ac9bcdaf49b96e Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 14:03:17 +0100 Subject: [PATCH 26/49] Make Pipe an alias for Subexpression They are exactly the same thing --- lib/jmespath/nodes/pipe.rb | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/lib/jmespath/nodes/pipe.rb b/lib/jmespath/nodes/pipe.rb index 3e2c9aa..44c8023 100644 --- a/lib/jmespath/nodes/pipe.rb +++ b/lib/jmespath/nodes/pipe.rb @@ -1,15 +1,6 @@ module JMESPath # @api private module Nodes - class Pipe < Node - def initialize(left, right) - @left = left - @right = right - end - - def visit(value) - @right.visit(@left.visit(value)) - end - end + Pipe = Subexpression end end From a5077fc2b2ee585916a24f20186f68f752cc3b48 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 14:41:55 +0100 Subject: [PATCH 27/49] Rename things in Projection Also remove the comment, it doesn't explain anything the code doesn't. --- lib/jmespath/nodes/projection.rb | 33 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index 8670156..2907d5a 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -2,19 +2,16 @@ module JMESPath # @api private module Nodes class Projection < Node - def initialize(left, right) - @left = left - @right = right + def initialize(target, projection) + @target = target + @projection = projection end def visit(value) - # Interprets a projection node, passing the values of the left - # child through the values of the right child and aggregating - # the non-null results into the return value. - if (projectees = extract_projectees(@left.visit(value))) + if (targets = extract_targets(@target.visit(value))) list = [] - projectees.each do |v| - if (vv = @right.visit(v)) + targets.each do |v| + if (vv = @projection.visit(v)) list << vv end end @@ -24,15 +21,15 @@ def visit(value) private - def extract_projectees(left_value) + def extract_targets(left_value) nil end end class ArrayProjection < Projection - def extract_projectees(left_value) - if Array === left_value - left_value + def extract_targets(target) + if Array === target + target else nil end @@ -42,11 +39,11 @@ def extract_projectees(left_value) class ObjectProjection < Projection EMPTY_ARRAY = [].freeze - def extract_projectees(left_value) - if hash_like?(left_value) - left_value.values - elsif left_value == EMPTY_ARRAY - left_value + def extract_targets(target) + if hash_like?(target) + target.values + elsif target == EMPTY_ARRAY + EMPTY_ARRAY else nil end From a97e20aa72504921625efd180a1d7e53de9dbcbf Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 14:52:21 +0100 Subject: [PATCH 28/49] Change how Expression and Function interacts Better to let Expression encapsulate the visiting. --- lib/jmespath/nodes/expression.rb | 10 +++++++--- lib/jmespath/nodes/function.rb | 6 +++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index 1ff6841..afa974a 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -2,15 +2,19 @@ module JMESPath # @api private module Nodes class Expression < Node - attr_reader :node + attr_reader :expression - def initialize(node) - @node = node + def initialize(expression) + @expression = expression end def visit(value) self end + + def eval(value) + @expression.visit(value) + end end end end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 57f5fb8..f2f5f98 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -52,7 +52,7 @@ def number_compare(mode, *args) values = args[0] expression = args[1] args[0].send("#{mode}_by") do |entry| - value = expression.node.visit(entry) + value = expression.eval(entry) if get_type(value) == 'number' value else @@ -386,8 +386,8 @@ def call(args) values = args[0] expression = args[1] values.sort do |a,b| - a_value = expression.node.visit(a) - b_value = expression.node.visit(b) + a_value = expression.eval(a) + b_value = expression.eval(b) a_type = get_type(a_value) b_type = get_type(b_value) if ['string', 'number'].include?(a_type) && a_type == b_type From 2b422cda7960275d122572c27cbee75b3c6acaad Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 14:58:31 +0100 Subject: [PATCH 29/49] Avoid creating so many strings and arrays in Function Each literal string and array is an object allocation, every time the expression is run. --- lib/jmespath/nodes/function.rb | 44 ++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index f2f5f98..1656d33 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -36,24 +36,42 @@ def call(args) def get_type(value) case value - when String then 'string' - when true, false then 'boolean' - when nil then 'null' - when Numeric then 'number' - when Hash, Struct then 'object' - when Array then 'array' - when Expression then 'expression' + when String then STRING_TYPE + when true, false then BOOLEAN_TYPE + when nil then NULL_TYPE + when Numeric then NUMBER_TYPE + when Hash, Struct then OBJECT_TYPE + when Array then ARRAY_TYPE + when Expression then EXPRESSION_TYPE end end + ARRAY_TYPE = 0 + BOOLEAN_TYPE = 1 + EXPRESSION_TYPE = 2 + NULL_TYPE = 3 + NUMBER_TYPE = 4 + OBJECT_TYPE = 5 + STRING_TYPE = 6 + + TYPE_NAMES = { + ARRAY_TYPE => 'array', + BOOLEAN_TYPE => 'boolean', + EXPRESSION_TYPE => 'expression', + NULL_TYPE => 'null', + NUMBER_TYPE => 'number', + OBJECT_TYPE => 'object', + STRING_TYPE => 'string', + }.freeze + def number_compare(mode, *args) if args.count == 2 - if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' + if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE values = args[0] expression = args[1] args[0].send("#{mode}_by") do |entry| value = expression.eval(entry) - if get_type(value) == 'number' + if get_type(value) == NUMBER_TYPE value else raise Errors::InvalidTypeError, "function #{mode}_by() expects values to be an numbers" @@ -225,7 +243,7 @@ class TypeFunction < Function def call(args) if args.count == 1 - get_type(args.first) + TYPE_NAMES[get_type(args.first)] else raise Errors::InvalidArityError, "function type() expects one argument" end @@ -362,7 +380,7 @@ def call(args) value.sort do |a, b| a_type = get_type(a) b_type = get_type(b) - if ['string', 'number'].include?(a_type) && a_type == b_type + if (a_type == STRING_TYPE || a_type == NUMBER_TYPE) && a_type == b_type a <=> b else raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" @@ -382,7 +400,7 @@ class SortByFunction < Function def call(args) if args.count == 2 - if get_type(args[0]) == 'array' && get_type(args[1]) == 'expression' + if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE values = args[0] expression = args[1] values.sort do |a,b| @@ -390,7 +408,7 @@ def call(args) b_value = expression.eval(b) a_type = get_type(a_value) b_type = get_type(b_value) - if ['string', 'number'].include?(a_type) && a_type == b_type + if (a_type == STRING_TYPE || a_type == NUMBER_TYPE) && a_type == b_type a_value <=> b_value else raise Errors::InvalidTypeError, "function sort() expects values to be an array of numbers or integers" From 96659b03abbfe29095a0f1e6f8258bddd953c252 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 15:00:34 +0100 Subject: [PATCH 30/49] Dispatch max_by and min_by on a symbol instead of interpolating a string --- lib/jmespath/nodes/function.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 1656d33..9d38afd 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -69,19 +69,19 @@ def number_compare(mode, *args) if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE values = args[0] expression = args[1] - args[0].send("#{mode}_by") do |entry| + args[0].send(mode) do |entry| value = expression.eval(entry) if get_type(value) == NUMBER_TYPE value else - raise Errors::InvalidTypeError, "function #{mode}_by() expects values to be an numbers" + raise Errors::InvalidTypeError, "function #{mode}() expects values to be an numbers" end end else - raise Errors::InvalidTypeError, "function #{mode}_by() expects an array and an expression" + raise Errors::InvalidTypeError, "function #{mode}() expects an array and an expression" end else - raise Errors::InvalidArityError, "function #{mode}_by() expects two arguments" + raise Errors::InvalidArityError, "function #{mode}() expects two arguments" end end end @@ -427,7 +427,7 @@ class MaxByFunction < Function FUNCTIONS['max_by'] = self def call(args) - number_compare(:max, *args) + number_compare(:max_by, *args) end end @@ -435,7 +435,7 @@ class MinByFunction < Function FUNCTIONS['min_by'] = self def call(args) - number_compare(:min, *args) + number_compare(:min_by, *args) end end end From 00779d24fe81600d7d1ec36040ffb848cfdc5402 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 16:07:39 +0100 Subject: [PATCH 31/49] Name the parameters in Function#number_compare and ContainsFunction --- lib/jmespath/nodes/function.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 9d38afd..fdcb1c1 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -66,10 +66,10 @@ def get_type(value) def number_compare(mode, *args) if args.count == 2 - if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE - values = args[0] - expression = args[1] - args[0].send(mode) do |entry| + values = args[0] + expression = args[1] + if get_type(values) == ARRAY_TYPE && get_type(expression) == EXPRESSION_TYPE + values.send(mode) do |entry| value = expression.eval(entry) if get_type(value) == NUMBER_TYPE value @@ -148,8 +148,10 @@ class ContainsFunction < Function def call(args) if args.count == 2 - if String === args[0] || Array === args[0] - args[0].include?(args[1]) + haystack = args[0] + needle = args[1] + if String === haystack || Array === haystack + haystack.include?(needle) else raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list" end From 07eb98600c17a5ebe82a81841fc8560b97613b45 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 16:11:14 +0100 Subject: [PATCH 32/49] Move utility functions out of Function and into mixins --- lib/jmespath/nodes/function.rb | 58 ++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index fdcb1c1..dcc6cc2 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -33,7 +33,9 @@ def initialize(name) def call(args) nil end + end + module TypeChecker def get_type(value) case value when String then STRING_TYPE @@ -63,27 +65,6 @@ def get_type(value) OBJECT_TYPE => 'object', STRING_TYPE => 'string', }.freeze - - def number_compare(mode, *args) - if args.count == 2 - values = args[0] - expression = args[1] - if get_type(values) == ARRAY_TYPE && get_type(expression) == EXPRESSION_TYPE - values.send(mode) do |entry| - value = expression.eval(entry) - if get_type(value) == NUMBER_TYPE - value - else - raise Errors::InvalidTypeError, "function #{mode}() expects values to be an numbers" - end - end - else - raise Errors::InvalidTypeError, "function #{mode}() expects an array and an expression" - end - else - raise Errors::InvalidArityError, "function #{mode}() expects two arguments" - end - end end class AbsFunction < Function @@ -241,6 +222,8 @@ def call(args) end class TypeFunction < Function + include TypeChecker + FUNCTIONS['type'] = self def call(args) @@ -373,6 +356,8 @@ def call(args) end class SortFunction < Function + include TypeChecker + FUNCTIONS['sort'] = self def call(args) @@ -398,6 +383,8 @@ def call(args) end class SortByFunction < Function + include TypeChecker + FUNCTIONS['sort_by'] = self def call(args) @@ -425,7 +412,34 @@ def call(args) end end + module NumberComparator + include TypeChecker + + def number_compare(mode, *args) + if args.count == 2 + values = args[0] + expression = args[1] + if get_type(values) == ARRAY_TYPE && get_type(expression) == EXPRESSION_TYPE + values.send(mode) do |entry| + value = expression.eval(entry) + if get_type(value) == NUMBER_TYPE + value + else + raise Errors::InvalidTypeError, "function #{mode}() expects values to be an numbers" + end + end + else + raise Errors::InvalidTypeError, "function #{mode}() expects an array and an expression" + end + else + raise Errors::InvalidArityError, "function #{mode}() expects two arguments" + end + end + end + class MaxByFunction < Function + include NumberComparator + FUNCTIONS['max_by'] = self def call(args) @@ -434,6 +448,8 @@ def call(args) end class MinByFunction < Function + include NumberComparator + FUNCTIONS['min_by'] = self def call(args) From 4e20823476ae88adc016b6378ae84adc47432704 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 16:38:29 +0100 Subject: [PATCH 33/49] Attempt to optimize runs of Field to a single Node E.g. foo.bar.baz becomes one Node instead of several Subexpression:s with Field children. --- lib/jmespath/nodes.rb | 4 ++++ lib/jmespath/nodes/field.rb | 34 +++++++++++++++++++++++++++++ lib/jmespath/nodes/subexpression.rb | 14 ++++++++++++ lib/jmespath/runtime.rb | 3 ++- 4 files changed, 54 insertions(+), 1 deletion(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index b1d61bc..2ab76ee 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -8,6 +8,10 @@ def visit(value) def hash_like?(value) Hash === value || Struct === value end + + def optimize + self + end end autoload :Comparator, 'jmespath/nodes/comparator' diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index e5a37fd..e0bffda 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -13,6 +13,40 @@ def visit(value) else nil end end + + def chain(other) + ChainedField.new([@key, *other.keys]) + end + + protected + + def keys + [@key] + end + end + + class ChainedField < Field + def initialize(keys) + @keys = keys + end + + def visit(value) + @keys.reduce(value) do |value, key| + case value + when Hash then value.key?(key) ? value[key] : value[key.to_sym] + when Struct then value.respond_to?(key) ? value[key] : nil + else nil + end + end + end + + def chain(other) + ChainedField.new([*@keys, *other.keys]) + end + + private + + attr_reader :keys end end end diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index 5c32535..e4c3149 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -10,6 +10,20 @@ def initialize(left, right) def visit(value) @right.visit(@left.visit(value)) end + + def optimize + left = @left.optimize + right = @right.optimize + if left.is_a?(Field) && right.is_a?(Field) + left.chain(right) + else + self + end + end + + protected + + attr_reader :left, :right end end end diff --git a/lib/jmespath/runtime.rb b/lib/jmespath/runtime.rb index 4de3080..5103491 100644 --- a/lib/jmespath/runtime.rb +++ b/lib/jmespath/runtime.rb @@ -48,7 +48,8 @@ def initialize(options = {}) # @param [Hash] data # @return [Mixed,nil] def search(expression, data) - @parser.parse(expression).visit(data) + optimized_expression = @parser.parse(expression).optimize + optimized_expression.visit(data) end private From b585f1a9e4d44382000d57f03d49a851ab453b36 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 16:57:53 +0100 Subject: [PATCH 34/49] Make a slightly ugly optimization in Projection When the projection is Current there's no need to run it --- lib/jmespath/nodes/projection.rb | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index 2907d5a..18965a8 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -19,6 +19,14 @@ def visit(value) end end + def optimize + if @projection.is_a?(Current) + fast_instance + else + self + end + end + private def extract_targets(left_value) @@ -26,6 +34,14 @@ def extract_targets(left_value) end end + module FastProjector + def visit(value) + if (targets = extract_targets(@target.visit(value))) + targets.compact + end + end + end + class ArrayProjection < Projection def extract_targets(target) if Array === target @@ -34,6 +50,14 @@ def extract_targets(target) nil end end + + def fast_instance + FastArrayProjection.new(@target, @projection) + end + end + + class FastArrayProjection < ArrayProjection + include FastProjector end class ObjectProjection < Projection @@ -48,6 +72,14 @@ def extract_targets(target) nil end end + + def fast_instance + FastObjectProjection.new(@target, @projection) + end + end + + class FastObjectProjection < ObjectProjection + include FastProjector end end end From 283c88f22182d0af6cc1fa5afcdb547f0ed7710c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 17:32:33 +0100 Subject: [PATCH 35/49] Fix a bug in Projection It shouldn't skip false values, just nil values --- lib/jmespath/nodes/projection.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index 18965a8..a04fa6d 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -11,7 +11,8 @@ def visit(value) if (targets = extract_targets(@target.visit(value))) list = [] targets.each do |v| - if (vv = @projection.visit(v)) + vv = @projection.visit(v) + unless vv.nil? list << vv end end From 65f4dd2e95bfa875b07bdae67c3352d909150a69 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 19:52:55 +0100 Subject: [PATCH 36/49] Flatten runs of Subexpression Also combine runs of Field --- lib/jmespath/nodes/subexpression.rb | 48 ++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index e4c3149..6a6a288 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -12,18 +12,52 @@ def visit(value) end def optimize - left = @left.optimize - right = @right.optimize - if left.is_a?(Field) && right.is_a?(Field) - left.chain(right) - else - self - end + Chain.new(flatten).optimize end protected attr_reader :left, :right + + def flatten + nodes = [@left, @right] + until nodes.none? { |node| node.is_a?(Subexpression) } + nodes = nodes.flat_map do |node| + if node.is_a?(Subexpression) + [node.left, node.right] + else + [node] + end + end + end + nodes.map(&:optimize) + end + end + + class Chain + def initialize(children) + @children = children + end + + def visit(value) + @children.reduce(value) do |v, child| + child.visit(v) + end + end + + def optimize + children = @children.dup + index = 0 + while index < children.size - 1 + if children[index].is_a?(Field) && children[index + 1].is_a?(Field) + children[index] = children[index].chain(children[index + 1]) + children.delete_at(index + 1) + else + index += 1 + end + end + Chain.new(children) + end end end end From b1fe52213bb605cedab3a727885cf74096b88a81 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 19:53:35 +0100 Subject: [PATCH 37/49] Make sure all nodes with children propagate the #optimize call --- lib/jmespath/nodes/comparator.rb | 4 ++++ lib/jmespath/nodes/condition.rb | 4 ++++ lib/jmespath/nodes/expression.rb | 4 ++++ lib/jmespath/nodes/flatten.rb | 4 ++++ lib/jmespath/nodes/function.rb | 4 ++++ lib/jmespath/nodes/multi_select_hash.rb | 8 ++++++++ lib/jmespath/nodes/multi_select_list.rb | 4 ++++ lib/jmespath/nodes/or.rb | 4 ++++ lib/jmespath/nodes/projection.rb | 6 +++--- 9 files changed, 39 insertions(+), 3 deletions(-) diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 74389d9..528befb 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -25,6 +25,10 @@ def visit(value) check(@left.visit(value), @right.visit(value)) end + def optimize + self.class.new(@left.optimize, @right.optimize) + end + private def check(left_value, right_value) diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 36b62a0..7391c1a 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -14,6 +14,10 @@ def visit(value) nil end end + + def optimize + self.class.new(@test.optimize, @child.optimize) + end end end end diff --git a/lib/jmespath/nodes/expression.rb b/lib/jmespath/nodes/expression.rb index afa974a..218fc02 100644 --- a/lib/jmespath/nodes/expression.rb +++ b/lib/jmespath/nodes/expression.rb @@ -15,6 +15,10 @@ def visit(value) def eval(value) @expression.visit(value) end + + def optimize + self.class.new(@expression.optimize) + end end end end diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index 4bf6ad4..1009403 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -20,6 +20,10 @@ def visit(value) nil end end + + def optimize + self.class.new(@child.optimize) + end end end end diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index dcc6cc2..f2e3dbc 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -20,6 +20,10 @@ def visit(value) call(@children.map { |child| child.visit(value) }) end + def optimize + self.class.new(@children.map(&:optimize)) + end + class FunctionName attr_reader :name diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index 1058e58..32cbda4 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -16,6 +16,10 @@ def visit(value) end end + def optimize + self.class.new(@children.map(&:optimize)) + end + class KeyValuePair attr_reader :key, :value @@ -23,6 +27,10 @@ def initialize(key, value) @key = key @value = value end + + def optimize + self.class.new(@key, @value.optimize) + end end end end diff --git a/lib/jmespath/nodes/multi_select_list.rb b/lib/jmespath/nodes/multi_select_list.rb index 81c337c..66ffc7b 100644 --- a/lib/jmespath/nodes/multi_select_list.rb +++ b/lib/jmespath/nodes/multi_select_list.rb @@ -13,6 +13,10 @@ def visit(value) @children.map { |n| n.visit(value) } end end + + def optimize + self.class.new(@children.map(&:optimize)) + end end end end diff --git a/lib/jmespath/nodes/or.rb b/lib/jmespath/nodes/or.rb index 2dbf8c6..0b258bd 100644 --- a/lib/jmespath/nodes/or.rb +++ b/lib/jmespath/nodes/or.rb @@ -15,6 +15,10 @@ def visit(value) result end end + + def optimize + self.class.new(@left.optimize, @right.optimize) + end end end end diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index a04fa6d..a539a3a 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -24,7 +24,7 @@ def optimize if @projection.is_a?(Current) fast_instance else - self + self.class.new(@target.optimize, @projection.optimize) end end @@ -53,7 +53,7 @@ def extract_targets(target) end def fast_instance - FastArrayProjection.new(@target, @projection) + FastArrayProjection.new(@target.optimize, @projection) end end @@ -75,7 +75,7 @@ def extract_targets(target) end def fast_instance - FastObjectProjection.new(@target, @projection) + FastObjectProjection.new(@target.optimize, @projection) end end From 8b95d617560ceada6e245d146b22f1561f807690 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 20:41:06 +0100 Subject: [PATCH 38/49] Make sure projections get optimized --- lib/jmespath/nodes/projection.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index a539a3a..09cea56 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -53,7 +53,7 @@ def extract_targets(target) end def fast_instance - FastArrayProjection.new(@target.optimize, @projection) + FastArrayProjection.new(@target.optimize, @projection.optimize) end end @@ -75,7 +75,7 @@ def extract_targets(target) end def fast_instance - FastObjectProjection.new(@target.optimize, @projection) + FastObjectProjection.new(@target.optimize, @projection.optimize) end end From 925f9283786b2ebab3dbea2b7768f8ddb06c1ffb Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 20:41:27 +0100 Subject: [PATCH 39/49] Inline Comparator into Condition on #optimize --- lib/jmespath/nodes/comparator.rb | 2 + lib/jmespath/nodes/condition.rb | 77 +++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/jmespath/nodes/comparator.rb b/lib/jmespath/nodes/comparator.rb index 528befb..9a4cf30 100644 --- a/lib/jmespath/nodes/comparator.rb +++ b/lib/jmespath/nodes/comparator.rb @@ -2,6 +2,8 @@ module JMESPath # @api private module Nodes class Comparator < Node + attr_reader :left, :right + def initialize(left, right) @left = left @right = right diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index 7391c1a..a5c0d80 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -16,7 +16,82 @@ def visit(value) end def optimize - self.class.new(@test.optimize, @child.optimize) + test = @test.optimize + if (new_type = ComparatorCondition::COMPARATOR_TO_CONDITION[@test.class]) + new_type.new(test.left, test.right, @child).optimize + else + self.class.new(test, @child.optimize) + end + end + end + + class ComparatorCondition < Node + COMPARATOR_TO_CONDITION = {} + + def initialize(left, right, child) + @left = left + @right = right + @child = child + end + + def visit(value) + nil + end + end + + class EqCondition < ComparatorCondition + COMPARATOR_TO_CONDITION[EqComparator] = self + + def visit(value) + @left.visit(value) == @right.visit(value) ? @child.visit(value) : nil + end + end + + class NeqCondition < ComparatorCondition + COMPARATOR_TO_CONDITION[NeqComparator] = self + + def visit(value) + @left.visit(value) != @right.visit(value) ? @child.visit(value) : nil + end + end + + class GtCondition < ComparatorCondition + COMPARATOR_TO_CONDITION[GtComparator] = self + + def visit(value) + left_value = @left.visit(value) + right_value = @right.visit(value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value > right_value ? @child.visit(value) : nil + end + end + + class GteCondition < ComparatorCondition + COMPARATOR_TO_CONDITION[GteComparator] = self + + def visit(value) + left_value = @left.visit(value) + right_value = @right.visit(value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value >= right_value ? @child.visit(value) : nil + end + end + + class LtCondition < ComparatorCondition + COMPARATOR_TO_CONDITION[LtComparator] = self + + def visit(value) + left_value = @left.visit(value) + right_value = @right.visit(value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value < right_value ? @child.visit(value) : nil + end + end + + class LteCondition < ComparatorCondition + COMPARATOR_TO_CONDITION[LteComparator] = self + + def visit(value) + left_value = @left.visit(value) + right_value = @right.visit(value) + left_value.is_a?(Integer) && right_value.is_a?(Integer) && left_value <= right_value ? @child.visit(value) : nil end end end From 296582e2af290a20662824760d830c710c4f97ff Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 20:54:50 +0100 Subject: [PATCH 40/49] Optimize the case when the RHS of a comparison in a condition is a Literal --- lib/jmespath/nodes/condition.rb | 38 +++++++++++++++++++++++++++++++++ lib/jmespath/nodes/literal.rb | 2 ++ 2 files changed, 40 insertions(+) diff --git a/lib/jmespath/nodes/condition.rb b/lib/jmespath/nodes/condition.rb index a5c0d80..84e35c9 100644 --- a/lib/jmespath/nodes/condition.rb +++ b/lib/jmespath/nodes/condition.rb @@ -45,6 +45,25 @@ class EqCondition < ComparatorCondition def visit(value) @left.visit(value) == @right.visit(value) ? @child.visit(value) : nil end + + def optimize + if @right.is_a?(Literal) + LiteralRightEqCondition.new(@left, @right, @child) + else + self + end + end + end + + class LiteralRightEqCondition < EqCondition + def initialize(left, right, child) + super + @right = @right.value + end + + def visit(value) + @left.visit(value) == @right ? @child.visit(value) : nil + end end class NeqCondition < ComparatorCondition @@ -53,6 +72,25 @@ class NeqCondition < ComparatorCondition def visit(value) @left.visit(value) != @right.visit(value) ? @child.visit(value) : nil end + + def optimize + if @right.is_a?(Literal) + LiteralRightNeqCondition.new(@left, @right, @child) + else + self + end + end + end + + class LiteralRightNeqCondition < NeqCondition + def initialize(left, right, child) + super + @right = @right.value + end + + def visit(value) + @left.visit(value) != @right ? @child.visit(value) : nil + end end class GtCondition < ComparatorCondition diff --git a/lib/jmespath/nodes/literal.rb b/lib/jmespath/nodes/literal.rb index d8ecdd6..b73baa9 100644 --- a/lib/jmespath/nodes/literal.rb +++ b/lib/jmespath/nodes/literal.rb @@ -2,6 +2,8 @@ module JMESPath # @api private module Nodes class Literal < Node + attr_reader :value + def initialize(value) @value = value end From c48eced3f2cf0760e48ecdb55092addb5115c6df Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:01:58 +0100 Subject: [PATCH 41/49] Rename "children" in MultiSelectHash to "kv_pairs" --- lib/jmespath/nodes/multi_select_hash.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/jmespath/nodes/multi_select_hash.rb b/lib/jmespath/nodes/multi_select_hash.rb index 32cbda4..38bb295 100644 --- a/lib/jmespath/nodes/multi_select_hash.rb +++ b/lib/jmespath/nodes/multi_select_hash.rb @@ -2,22 +2,22 @@ module JMESPath # @api private module Nodes class MultiSelectHash < Node - def initialize(children) - @children = children + def initialize(kv_pairs) + @kv_pairs = kv_pairs end def visit(value) if value.nil? nil else - @children.each_with_object({}) do |pair, hash| + @kv_pairs.each_with_object({}) do |pair, hash| hash[pair.key] = pair.value.visit(value) end end end def optimize - self.class.new(@children.map(&:optimize)) + self.class.new(@kv_pairs.map(&:optimize)) end class KeyValuePair From fd00e14cdc859d314c889a01a7d6fb6ef26a4bb0 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:06:55 +0100 Subject: [PATCH 42/49] Make the arguments to Slice explicit --- lib/jmespath/nodes/slice.rb | 8 +++++--- lib/jmespath/parser.rb | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 1a8eecb..2959724 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -2,13 +2,15 @@ module JMESPath # @api private module Nodes class Slice < Node - def initialize(args) - @args = args + def initialize(start, stop, step) + @start = start + @stop = stop + @step = step end def visit(value) if String === value || Array === value - start, stop, step = adjust_slice(value.size, *@args) + start, stop, step = adjust_slice(value.size, @start, @stop, @step) result = [] if step > 0 i = start diff --git a/lib/jmespath/parser.rb b/lib/jmespath/parser.rb index a0dd1ed..97bdf67 100644 --- a/lib/jmespath/parser.rb +++ b/lib/jmespath/parser.rb @@ -220,7 +220,7 @@ def parse_array_index_expression(stream) elsif pos > 2 raise Errors::SyntaxError, 'invalid array slice syntax: too many colons' else - Nodes::Slice.new(parts) + Nodes::Slice.new(*parts) end end From be4ac612f821d4603218d752ba773943efdf5b46 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:19:10 +0100 Subject: [PATCH 43/49] Optimze single step slices with positive start and stop --- lib/jmespath/nodes/slice.rb | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 2959724..56e0835 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -31,6 +31,14 @@ def visit(value) end end + def optimize + if (@step.nil? || @step == 1) && @start && @stop && @start > 0 && @stop > @start + SimpleSlice.new(@start, @stop) + else + self + end + end + private def adjust_slice(length, start, stop, step) @@ -67,5 +75,19 @@ def adjust_endpoint(length, endpoint, step) end end end + + class SimpleSlice < Slice + def initialize(start, stop) + super(start, stop, 1) + end + + def visit(value) + if String === value || Array === value + value[@start, @stop - @start] + else + nil + end + end + end end end From 407f2367a223cef6eed3f20b8f34116694a3dbc2 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:21:22 +0100 Subject: [PATCH 44/49] Optimize an extra time in Chain Right now the children of a Chain are guaranteed to be optimized, but if it were used from somewhere else that would not hold. --- lib/jmespath/nodes/subexpression.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index 6a6a288..dcbed5c 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -46,7 +46,7 @@ def visit(value) end def optimize - children = @children.dup + children = @children.map(&:optimize) index = 0 while index < children.size - 1 if children[index].is_a?(Field) && children[index + 1].is_a?(Field) From a24423832d5e99cf4e726d7210f2f8d276a82074 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:24:14 +0100 Subject: [PATCH 45/49] Make the chaining done in Chain more generic --- lib/jmespath/nodes.rb | 4 ++++ lib/jmespath/nodes/field.rb | 4 ++++ lib/jmespath/nodes/subexpression.rb | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index 2ab76ee..c51d36b 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -12,6 +12,10 @@ def hash_like?(value) def optimize self end + + def chains_with?(other) + false + end end autoload :Comparator, 'jmespath/nodes/comparator' diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index e0bffda..21e463a 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -14,6 +14,10 @@ def visit(value) end end + def chains_with?(other) + other.is_a?(Field) + end + def chain(other) ChainedField.new([@key, *other.keys]) end diff --git a/lib/jmespath/nodes/subexpression.rb b/lib/jmespath/nodes/subexpression.rb index dcbed5c..81e23dd 100644 --- a/lib/jmespath/nodes/subexpression.rb +++ b/lib/jmespath/nodes/subexpression.rb @@ -49,7 +49,7 @@ def optimize children = @children.map(&:optimize) index = 0 while index < children.size - 1 - if children[index].is_a?(Field) && children[index + 1].is_a?(Field) + if children[index].chains_with?(children[index + 1]) children[index] = children[index].chain(children[index + 1]) children.delete_at(index + 1) else From bd9edca94f8f5254380168d81444c2384c81f37c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:32:40 +0100 Subject: [PATCH 46/49] Optimize runs of Index to ChainIndex --- lib/jmespath/nodes/index.rb | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index b23e8ad..0a8eff3 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -13,6 +13,40 @@ def visit(value) nil end end + + def chains_with?(other) + other.is_a?(Index) + end + + def chain(other) + ChainedIndex.new([*indexes, *other.indexes]) + end + + protected + + def indexes + [@index] + end + end + + class ChainedIndex < Index + def initialize(indexes) + @indexes = indexes + end + + def visit(value) + @indexes.reduce(value) do |v, index| + if Array === v + v[index] + else + nil + end + end + end + + private + + attr_reader :indexes end end end From e6e7b9b2c603c1cf9f9803b94ad68a6843c45031 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:42:11 +0100 Subject: [PATCH 47/49] Make Index an alias for Field This is maybe a bit of an aggressive optimization, but Field and Index are probably common in longer runs, so this avoids a lot of method calls, potentially. --- lib/jmespath/nodes/field.rb | 34 +++++++++++++++++++++----- lib/jmespath/nodes/index.rb | 48 +------------------------------------ 2 files changed, 29 insertions(+), 53 deletions(-) diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index 21e463a..784960d 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -8,9 +8,20 @@ def initialize(key) def visit(value) case value - when Hash then value.key?(@key) ? value[@key] : value[@key.to_sym] - when Struct then value.respond_to?(@key) ? value[@key] : nil - else nil + when Array + if @key.is_a?(Integer) + value[@key] + end + when Hash + if value.key?(@key) + value[@key] + elsif @key.is_a?(String) + value[@key.to_sym] + end + when Struct + if value.respond_to?(@key) + value[@key] + end end end @@ -37,9 +48,20 @@ def initialize(keys) def visit(value) @keys.reduce(value) do |value, key| case value - when Hash then value.key?(key) ? value[key] : value[key.to_sym] - when Struct then value.respond_to?(key) ? value[key] : nil - else nil + when Array + if key.is_a?(Integer) + value[key] + end + when Hash + if value.key?(key) + value[key] + elsif key.is_a?(String) + value[key.to_sym] + end + when Struct + if value.respond_to?(key) + value[key] + end end end end diff --git a/lib/jmespath/nodes/index.rb b/lib/jmespath/nodes/index.rb index 0a8eff3..5cf4120 100644 --- a/lib/jmespath/nodes/index.rb +++ b/lib/jmespath/nodes/index.rb @@ -1,52 +1,6 @@ module JMESPath # @api private module Nodes - class Index < Node - def initialize(index) - @index = index - end - - def visit(value) - if Array === value - value[@index] - else - nil - end - end - - def chains_with?(other) - other.is_a?(Index) - end - - def chain(other) - ChainedIndex.new([*indexes, *other.indexes]) - end - - protected - - def indexes - [@index] - end - end - - class ChainedIndex < Index - def initialize(indexes) - @indexes = indexes - end - - def visit(value) - @indexes.reduce(value) do |v, index| - if Array === v - v[index] - else - nil - end - end - end - - private - - attr_reader :indexes - end + Index = Field end end From 033afd271d9cd3888cb01acb6f6d9b852257a8c6 Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:45:12 +0100 Subject: [PATCH 48/49] Cache the symbolized key in Key --- lib/jmespath/nodes/field.rb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index 784960d..cafb930 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -4,6 +4,7 @@ module Nodes class Field < Node def initialize(key) @key = key + @key_sym = key.respond_to?(:to_sym) ? key.to_sym : nil end def visit(value) @@ -15,8 +16,8 @@ def visit(value) when Hash if value.key?(@key) value[@key] - elsif @key.is_a?(String) - value[@key.to_sym] + elsif @key_sym + value[@key_sym] end when Struct if value.respond_to?(@key) @@ -43,6 +44,11 @@ def keys class ChainedField < Field def initialize(keys) @keys = keys + @key_syms = keys.each_with_object({}) do |k, syms| + if k.respond_to?(:to_sym) + syms[k] = k.to_sym + end + end end def visit(value) @@ -55,8 +61,8 @@ def visit(value) when Hash if value.key?(key) value[key] - elsif key.is_a?(String) - value[key.to_sym] + elsif (sym = @key_syms[key]) + value[sym] end when Struct if value.respond_to?(key) From de5e54d9af374d7bd07c887d861184099c6db52c Mon Sep 17 00:00:00 2001 From: Theo Date: Wed, 21 Jan 2015 21:54:15 +0100 Subject: [PATCH 49/49] Rewrite the case/whens in Field to if/else Every clause really has a subclause, so if/else makes more sense. The change also avoids using #key? when #[] + !#nil? will avoid another hash lookup. --- lib/jmespath/nodes/field.rb | 46 +++++++++++++++---------------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index cafb930..80d6d7e 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -8,21 +8,16 @@ def initialize(key) end def visit(value) - case value - when Array - if @key.is_a?(Integer) - value[@key] - end - when Hash - if value.key?(@key) - value[@key] - elsif @key_sym - value[@key_sym] - end - when Struct - if value.respond_to?(@key) - value[@key] + if value.is_a?(Array) && @key.is_a?(Integer) + value[@key] + elsif value.is_a?(Hash) + if !(v = value[@key]).nil? + v + elsif @key_sym && !(v = value[@key_sym]).nil? + v end + elsif value.is_a?(Struct) && value.respond_to?(@key) + value[@key] end end @@ -53,21 +48,16 @@ def initialize(keys) def visit(value) @keys.reduce(value) do |value, key| - case value - when Array - if key.is_a?(Integer) - value[key] - end - when Hash - if value.key?(key) - value[key] - elsif (sym = @key_syms[key]) - value[sym] - end - when Struct - if value.respond_to?(key) - value[key] + if value.is_a?(Array) && key.is_a?(Integer) + value[key] + elsif value.is_a?(Hash) + if !(v = value[key]).nil? + v + elsif (sym = @key_syms[key]) && !(v = value[sym]).nil? + v end + elsif value.is_a?(Struct) && value.respond_to?(key) + value[key] end end end