Skip to content

Commit

Permalink
Add find, find_index, has, and reject filters to arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
karreiro committed Dec 3, 2024
1 parent 3d1e2d4 commit 41a77c6
Show file tree
Hide file tree
Showing 2 changed files with 382 additions and 32 deletions.
130 changes: 102 additions & 28 deletions lib/liquid/standardfilters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ def sort(input, property = nil)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
rescue TypeError
raise_property_error(property)
end
Expand Down Expand Up @@ -407,7 +407,7 @@ def sort_natural(input, property = nil)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
ary.sort { |a, b| nil_safe_casecmp(fetch_property(a, property), fetch_property(b, property)) }
rescue TypeError
raise_property_error(property)
end
Expand All @@ -424,29 +424,59 @@ def sort_natural(input, property = nil)
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
def where(input, property, target_value = nil)
ary = InputIterator.new(input, context)
filter_array(input, property, target_value, :select)
end

if ary.empty?
[]
elsif target_value.nil?
ary.select do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
else
ary.select do |item|
item[property] == target_value
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Filters an array to exclude items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | reject: string, string
# @liquid_return [array[untyped]]
def reject(input, property, target_value = nil)
filter_array(input, property, target_value, :reject)
end

# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Tests if any item in an array has a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | some: string, string
# @liquid_return [boolean]
def has(input, property, target_value = nil)
filter_array(input, property, target_value, :any?)
end

# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find: string, string
# @liquid_return [untyped]
def find(input, property, target_value = nil)
filter_array(input, property, target_value, :find)
end

# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the index of the first item in an array with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | find_index: string, string
# @liquid_return [number]
def find_index(input, property, target_value = nil)
filter_array(input, property, target_value, :find_index)
end

# @liquid_public_docs
Expand All @@ -465,7 +495,7 @@ def uniq(input, property = nil)
[]
else
ary.uniq do |item|
item[property]
fetch_property(item, property)
rescue TypeError
raise_property_error(property)
rescue NoMethodError
Expand Down Expand Up @@ -501,7 +531,7 @@ def map(input, property)
if property == "to_liquid"
e
elsif e.respond_to?(:[])
r = e[property]
r = fetch_property(e, property)
r.is_a?(Proc) ? r.call : r
end
end
Expand All @@ -525,7 +555,7 @@ def compact(input, property = nil)
[]
else
ary.reject do |item|
item[property].nil?
fetch_property(item, property).nil?
rescue TypeError
raise_property_error(property)
rescue NoMethodError
Expand Down Expand Up @@ -899,7 +929,7 @@ def sum(input, property = nil)
if property.nil?
item
elsif item.respond_to?(:[])
item[property]
fetch_property(item, property)
else
0
end
Expand All @@ -918,6 +948,50 @@ def sum(input, property = nil)

attr_reader :context

def filter_array(input, property, target_value, method)
ary = InputIterator.new(input, context)

return [] if ary.empty?

ary.public_send(method) do |item|
if target_value.nil?
fetch_property(item, property)
else
fetch_property(item, property) == target_value
end
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end

def fetch_property(drop, property_or_keys)
##
# This keeps backward compatibility by supporting properties containing
# dots. This is valid in Liquid syntax and used in some runtimes, such as
# Shopify with metafields.
#
# Using this approach, properties like 'price.value' can be accessed in
# both of the following examples:
#
# ```
# [
# { 'name' => 'Item 1', 'price.price' => 40000 },
# { 'name' => 'Item 2', 'price' => { 'value' => 39900 } }
# ]
# ```
value = drop[property_or_keys]

return value if !value.nil? || !property_or_keys.is_a?(String)

keys = property_or_keys.split('.')
keys.reduce(drop) do |drop, key|
drop.respond_to?(:[]) ? drop[key] : drop
end
end

def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
Expand Down
Loading

0 comments on commit 41a77c6

Please sign in to comment.