Skip to content

Commit

Permalink
duck-type values responding to #to_hash or #to_ary rather than checki…
Browse files Browse the repository at this point in the history
…ng class
  • Loading branch information
notEthan committed Jan 2, 2022
1 parent 1844ca4 commit c6d38cf
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 54 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
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
85 changes: 50 additions & 35 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 @@ -145,8 +152,10 @@ def call(args)
if args.count == 2
haystack = args[0]
needle = args[1]
if String === haystack || Array === haystack
haystack.include?(needle)
if haystack.respond_to?(:to_str)
haystack.to_str.include?(needle)
elsif haystack.respond_to?(:to_ary)
haystack.to_ary.include?(needle)
else
return maybe_raise Errors::InvalidTypeError, "contains expects 2nd arg to be a list"
end
Expand Down Expand Up @@ -182,9 +191,10 @@ 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.respond_to?(:to_ary) || value.respond_to?(:to_str)
value.size
else
return maybe_raise Errors::InvalidTypeError, "function length() expects string, array or object"
end
end
end
Expand All @@ -202,8 +212,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 +233,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 +269,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 +314,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 +333,12 @@ class ValuesFunction < Function
def call(args)
if args.count == 1
value = args.first
if hash_like?(value)
value.values
elsif Array === value
value
if value.respond_to?(:to_hash)
value.to_hash.values
elsif value.is_a?(Struct)
value.to_h.values
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 @@ -345,8 +357,8 @@ def call(args)
values = args[1]
if !(String === glue)
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| String === v }
values.to_ary.join(glue)
else
return maybe_raise Errors::InvalidTypeError, "function join() expects values to be an array of strings"
end
Expand Down Expand Up @@ -390,8 +402,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 +436,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 @@ -616,7 +629,9 @@ def call(args)
return maybe_raise Errors::InvalidArityError, msg
end
value = args.first
if Array === value || String === value
if value.respond_to?(:to_ary)
value.to_ary.reverse
elsif String === value
value.reverse
else
msg = "function reverse() expects an array or string"
Expand All @@ -630,7 +645,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
10 changes: 6 additions & 4 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,8 +63,10 @@ class FastArrayProjection < ArrayProjection

class ObjectProjection < Projection
def extract_targets(target)
if hash_like?(target)
target.values
if target.respond_to?(:to_hash)
target.to_hash.values
elsif target.is_a?(Struct)
target.to_h.values
else
nil
end
Expand Down
2 changes: 1 addition & 1 deletion 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 Down

0 comments on commit c6d38cf

Please sign in to comment.