From 2e059c551ea59d24be8fcf5641d9831ae834f079 Mon Sep 17 00:00:00 2001 From: Ethan Date: Sun, 2 Jan 2022 12:38:14 -0800 Subject: [PATCH] duck-type values responding to #to_hash or #to_ary rather than checking class --- lib/jmespath/nodes.rb | 4 -- lib/jmespath/nodes/field.rb | 14 +++--- lib/jmespath/nodes/flatten.rb | 8 +-- lib/jmespath/nodes/function.rb | 85 +++++++++++++++++++------------- lib/jmespath/nodes/projection.rb | 8 +-- lib/jmespath/nodes/slice.rb | 2 +- 6 files changed, 68 insertions(+), 53 deletions(-) diff --git a/lib/jmespath/nodes.rb b/lib/jmespath/nodes.rb index e67cc0d..e9ba80e 100644 --- a/lib/jmespath/nodes.rb +++ b/lib/jmespath/nodes.rb @@ -5,10 +5,6 @@ class Node def visit(value) end - def hash_like?(value) - Hash === value || Struct === value - end - def optimize self end diff --git a/lib/jmespath/nodes/field.rb b/lib/jmespath/nodes/field.rb index 3aaee9d..0f2df46 100644 --- a/lib/jmespath/nodes/field.rb +++ b/lib/jmespath/nodes/field.rb @@ -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? @@ -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? diff --git a/lib/jmespath/nodes/flatten.rb b/lib/jmespath/nodes/flatten.rb index 1009403..7d46ece 100644 --- a/lib/jmespath/nodes/flatten.rb +++ b/lib/jmespath/nodes/flatten.rb @@ -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 diff --git a/lib/jmespath/nodes/function.rb b/lib/jmespath/nodes/function.rb index 6a6ab52..b260357 100644 --- a/lib/jmespath/nodes/function.rb +++ b/lib/jmespath/nodes/function.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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" @@ -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 diff --git a/lib/jmespath/nodes/projection.rb b/lib/jmespath/nodes/projection.rb index ef4e737..bed2090 100644 --- a/lib/jmespath/nodes/projection.rb +++ b/lib/jmespath/nodes/projection.rb @@ -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 @@ -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 diff --git a/lib/jmespath/nodes/slice.rb b/lib/jmespath/nodes/slice.rb index 97f045f..1303723 100644 --- a/lib/jmespath/nodes/slice.rb +++ b/lib/jmespath/nodes/slice.rb @@ -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