Skip to content

Commit

Permalink
duck-type values responding to #to_hash or #to_ary (#51)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Woods <alexwoo@amazon.com>
  • Loading branch information
notEthan and alextwoods committed Jan 10, 2022
1 parent a1db2bb commit b9167e5
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 69 deletions.
4 changes: 0 additions & 4 deletions lib/jmespath/nodes.rb
Expand Up @@ -5,10 +5,6 @@ class Node
def visit(value)
end

def hash_like?(value)
Hash === value || Struct === value
end

def optimize
self
end
Expand Down
4 changes: 2 additions & 2 deletions lib/jmespath/nodes/comparator.rb
Expand Up @@ -42,13 +42,13 @@ module Comparators

class Eq < Comparator
def check(left_value, right_value)
left_value == right_value
Util.as_json(left_value) == Util.as_json(right_value)
end
end

class Neq < Comparator
def check(left_value, right_value)
left_value != right_value
Util.as_json(left_value) != Util.as_json(right_value)
end
end

Expand Down
8 changes: 4 additions & 4 deletions lib/jmespath/nodes/condition.rb
Expand Up @@ -43,7 +43,7 @@ class EqCondition < ComparatorCondition
COMPARATOR_TO_CONDITION[Comparators::Eq] = self

def visit(value)
@left.visit(value) == @right.visit(value) ? @child.visit(value) : nil
Util.as_json(@left.visit(value)) == Util.as_json(@right.visit(value)) ? @child.visit(value) : nil
end

def optimize
Expand All @@ -62,15 +62,15 @@ def initialize(left, right, child)
end

def visit(value)
@left.visit(value) == @right ? @child.visit(value) : nil
Util.as_json(@left.visit(value)) == @right ? @child.visit(value) : nil
end
end

class NeqCondition < ComparatorCondition
COMPARATOR_TO_CONDITION[Comparators::Neq] = self

def visit(value)
@left.visit(value) != @right.visit(value) ? @child.visit(value) : nil
Util.as_json(@left.visit(value)) != Util.as_json(@right.visit(value)) ? @child.visit(value) : nil
end

def optimize
Expand All @@ -89,7 +89,7 @@ def initialize(left, right, child)
end

def visit(value)
@left.visit(value) != @right ? @child.visit(value) : nil
Util.as_json(@left.visit(value)) != @right ? @child.visit(value) : nil
end
end

Expand Down
14 changes: 8 additions & 6 deletions lib/jmespath/nodes/field.rb
Expand Up @@ -8,9 +8,10 @@ def initialize(key)
end

def visit(value)
if value.is_a?(Array) && @key.is_a?(Integer)
value[@key]
elsif value.is_a?(Hash)
if value.respond_to?(:to_ary) && @key.is_a?(Integer)
value.to_ary[@key]
elsif value.respond_to?(:to_hash)
value = value.to_hash
if !(v = value[@key]).nil?
v
elsif @key_sym && !(v = value[@key_sym]).nil?
Expand Down Expand Up @@ -48,9 +49,10 @@ def initialize(keys)

def visit(obj)
@keys.reduce(obj) do |value, key|
if value.is_a?(Array) && key.is_a?(Integer)
value[key]
elsif value.is_a?(Hash)
if value.respond_to?(:to_ary) && key.is_a?(Integer)
value.to_ary[key]
elsif value.respond_to?(:to_hash)
value = value.to_hash
if !(v = value[key]).nil?
v
elsif (sym = @key_syms[key]) && !(v = value[sym]).nil?
Expand Down
8 changes: 4 additions & 4 deletions lib/jmespath/nodes/flatten.rb
Expand Up @@ -8,10 +8,10 @@ def initialize(child)

def visit(value)
value = @child.visit(value)
if Array === value
value.each_with_object([]) do |v, values|
if Array === v
values.concat(v)
if value.respond_to?(:to_ary)
value.to_ary.each_with_object([]) do |v, values|
if v.respond_to?(:to_ary)
values.concat(v.to_ary)
else
values.push(v)
end
Expand Down
98 changes: 59 additions & 39 deletions lib/jmespath/nodes/function.rb
Expand Up @@ -50,14 +50,20 @@ def call(args)

module TypeChecker
def get_type(value)
case value
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
if value.respond_to?(:to_str)
STRING_TYPE
elsif value == true || value == false
BOOLEAN_TYPE
elsif value == nil
NULL_TYPE
elsif value.is_a?(Numeric)
NUMBER_TYPE
elsif value.respond_to?(:to_hash) || value.is_a?(Struct)
OBJECT_TYPE
elsif value.respond_to?(:to_ary)
ARRAY_TYPE
elsif value.is_a?(Expression)
EXPRESSION_TYPE
end
end

Expand Down Expand Up @@ -106,7 +112,8 @@ def call(args)
else
return maybe_raise Errors::InvalidArityError, "function avg() expects one argument"
end
if Array === values
if values.respond_to?(:to_ary)
values = values.to_ary
return nil if values.empty?
values.inject(0) do |total,n|
if Numeric === n
Expand Down Expand Up @@ -144,9 +151,11 @@ class ContainsFunction < Function
def call(args)
if args.count == 2
haystack = args[0]
needle = args[1]
if String === haystack || Array === haystack
haystack.include?(needle)
needle = Util.as_json(args[1])
if haystack.respond_to?(:to_str)
haystack.to_str.include?(needle)
elsif haystack.respond_to?(:to_ary)
haystack.to_ary.any? { |e| Util.as_json(e) == needle }
else
return maybe_raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list"
end
Expand Down Expand Up @@ -182,9 +191,14 @@ def call(args)
else
return maybe_raise Errors::InvalidArityError, "function length() expects one argument"
end
case value
when Hash, Array, String then value.size
else return maybe_raise Errors::InvalidTypeError, "function length() expects string, array or object"
if value.respond_to?(:to_hash)
value.to_hash.size
elsif value.respond_to?(:to_ary)
value.to_ary.size
elsif value.respond_to?(:to_str)
value.to_str.size
else
return maybe_raise Errors::InvalidTypeError, "function length() expects string, array or object"
end
end
end
Expand All @@ -202,8 +216,8 @@ def call(args)
else
return maybe_raise Errors::InvalidTypeError, "function map() expects the first argument to be an expression"
end
if Array === args[1]
list = args[1]
if args[1].respond_to?(:to_ary)
list = args[1].to_ary
else
return maybe_raise Errors::InvalidTypeError, "function map() expects the second argument to be an list"
end
Expand All @@ -223,7 +237,8 @@ def call(args)
else
return maybe_raise Errors::InvalidArityError, "function max() expects one argument"
end
if Array === values
if values.respond_to?(:to_ary)
values = values.to_ary
return nil if values.empty?
first = values.first
first_type = get_type(first)
Expand Down Expand Up @@ -258,7 +273,8 @@ def call(args)
else
return maybe_raise Errors::InvalidArityError, "function min() expects one argument"
end
if Array === values
if values.respond_to?(:to_ary)
values = values.to_ary
return nil if values.empty?
first = values.first
first_type = get_type(first)
Expand Down Expand Up @@ -302,12 +318,10 @@ class KeysFunction < Function
def call(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
if value.respond_to?(:to_hash)
value.to_hash.keys.map(&:to_s)
elsif value.is_a?(Struct)
value.members.map(&:to_s)
else
return maybe_raise Errors::InvalidTypeError, "function keys() expects a hash"
end
Expand All @@ -323,10 +337,12 @@ class ValuesFunction < Function
def call(args)
if args.count == 1
value = args.first
if hash_like?(value)
if value.respond_to?(:to_hash)
value.to_hash.values
elsif value.is_a?(Struct)
value.values
elsif Array === value
value
elsif value.respond_to?(:to_ary)
value.to_ary
else
return maybe_raise Errors::InvalidTypeError, "function values() expects an array or a hash"
end
Expand All @@ -343,10 +359,10 @@ def call(args)
if args.count == 2
glue = args[0]
values = args[1]
if !(String === glue)
if !glue.respond_to?(:to_str)
return maybe_raise Errors::InvalidTypeError, "function join() expects the first argument to be a string"
elsif Array === values && values.all? { |v| String === v }
values.join(glue)
elsif values.respond_to?(:to_ary) && values.to_ary.all? { |v| v.respond_to?(:to_str) }
values.to_ary.join(glue)
else
return maybe_raise Errors::InvalidTypeError, "function join() expects values to be an array of strings"
end
Expand All @@ -362,7 +378,7 @@ class ToStringFunction < Function
def call(args)
if args.count == 1
value = args.first
String === value ? value : value.to_json
value.respond_to?(:to_str) ? value.to_str : value.to_json
else
return maybe_raise Errors::InvalidArityError, "function to_string() expects one argument"
end
Expand Down Expand Up @@ -390,8 +406,8 @@ class SumFunction < Function
FUNCTIONS['sum'] = self

def call(args)
if args.count == 1 && Array === args.first
args.first.inject(0) do |sum,n|
if args.count == 1 && args.first.respond_to?(:to_ary)
args.first.to_ary.inject(0) do |sum,n|
if Numeric === n
sum + n
else
Expand Down Expand Up @@ -424,7 +440,8 @@ class SortFunction < Function
def call(args)
if args.count == 1
value = args.first
if Array === value
if value.respond_to?(:to_ary)
value = value.to_ary
# every element in the list must be of the same type
array_type = get_type(value[0])
if array_type == STRING_TYPE || array_type == NUMBER_TYPE || value.size == 0
Expand Down Expand Up @@ -459,7 +476,7 @@ class SortByFunction < Function
def call(args)
if args.count == 2
if get_type(args[0]) == ARRAY_TYPE && get_type(args[1]) == EXPRESSION_TYPE
values = args[0]
values = args[0].to_ary
expression = args[1]
array_type = get_type(expression.eval(values[0]))
if array_type == STRING_TYPE || array_type == NUMBER_TYPE || values.size == 0
Expand Down Expand Up @@ -495,6 +512,7 @@ def compare_by(mode, *args)
values = args[0]
expression = args[1]
if get_type(values) == ARRAY_TYPE && get_type(expression) == EXPRESSION_TYPE
values = values.to_ary
type = get_type(expression.eval(values.first))
if type != NUMBER_TYPE && type != STRING_TYPE
msg = "function #{mode}() expects values to be strings or numbers"
Expand Down Expand Up @@ -616,8 +634,10 @@ def call(args)
return maybe_raise Errors::InvalidArityError, msg
end
value = args.first
if Array === value || String === value
value.reverse
if value.respond_to?(:to_ary)
value.to_ary.reverse
elsif value.respond_to?(:to_str)
value.to_str.reverse
else
msg = "function reverse() expects an array or string"
return maybe_raise Errors::InvalidTypeError, msg
Expand All @@ -630,7 +650,7 @@ class ToArrayFunction < Function

def call(args)
value = args.first
Array === value ? value : [value]
value.respond_to?(:to_ary) ? value.to_ary : [value]
end
end
end
Expand Down
8 changes: 5 additions & 3 deletions lib/jmespath/nodes/projection.rb
Expand Up @@ -45,8 +45,8 @@ def visit(value)

class ArrayProjection < Projection
def extract_targets(target)
if Array === target
target
if target.respond_to?(:to_ary)
target.to_ary
else
nil
end
Expand All @@ -63,7 +63,9 @@ class FastArrayProjection < ArrayProjection

class ObjectProjection < Projection
def extract_targets(target)
if hash_like?(target)
if target.respond_to?(:to_hash)
target.to_hash.values
elsif target.is_a?(Struct)
target.values
else
nil
Expand Down
6 changes: 3 additions & 3 deletions lib/jmespath/nodes/slice.rb
Expand Up @@ -10,7 +10,7 @@ def initialize(start, stop, step)
end

def visit(value)
if String === value || Array === value
if (value = value.respond_to?(:to_str) ? value.to_str : value.respond_to?(:to_ary) ? value.to_ary : nil)
start, stop, step = adjust_slice(value.size, @start, @stop, @step)
result = []
if step > 0
Expand All @@ -26,7 +26,7 @@ def visit(value)
i += step
end
end
String === value ? result.join : result
value.respond_to?(:to_str) ? result.join : result
else
nil
end
Expand Down Expand Up @@ -80,7 +80,7 @@ def initialize(start, stop)
end

def visit(value)
if String === value || Array === value
if (value = value.respond_to?(:to_str) ? value.to_str : value.respond_to?(:to_ary) ? value.to_ary : nil)
value[@start, @stop - @start]
else
nil
Expand Down

0 comments on commit b9167e5

Please sign in to comment.