Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

duck-type values responding to #to_hash or #to_ary #51

Merged
merged 6 commits into from Jan 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
notEthan marked this conversation as resolved.
Show resolved Hide resolved
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)
notEthan marked this conversation as resolved.
Show resolved Hide resolved
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