Skip to content

Commit

Permalink
Fix array filters to not support nested properties
Browse files Browse the repository at this point in the history
  • Loading branch information
karreiro committed Jan 31, 2025
1 parent 8dd9279 commit f9454d8
Show file tree
Hide file tree
Showing 4 changed files with 13 additions and 151 deletions.
4 changes: 4 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## 5.8.0 (unreleased)

## 5.7.2 2025-01-31

* Fix array filters to not support nested properties

## 5.7.1 2025-01-24

* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays
Expand Down
41 changes: 8 additions & 33 deletions lib/liquid/standardfilters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ def sort(input, property = nil)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
Expand Down Expand Up @@ -416,7 +416,7 @@ def sort_natural(input, property = nil)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(fetch_property(a, property), fetch_property(b, property)) }
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
Expand Down Expand Up @@ -504,7 +504,7 @@ def uniq(input, property = nil)
[]
else
ary.uniq do |item|
fetch_property(item, property)
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
Expand Down Expand Up @@ -540,7 +540,7 @@ def map(input, property)
if property == "to_liquid"
e
elsif e.respond_to?(:[])
r = fetch_property(e, property)
r = e[property]
r.is_a?(Proc) ? r.call : r
end
end
Expand All @@ -564,7 +564,7 @@ def compact(input, property = nil)
[]
else
ary.reject do |item|
fetch_property(item, property).nil?
item[property].nil?
rescue TypeError
raise_property_error(property)
rescue NoMethodError
Expand Down Expand Up @@ -950,7 +950,7 @@ def sum(input, property = nil)
if property.nil?
item
elsif item.respond_to?(:[])
fetch_property(item, property)
item[property]
else
0
end
Expand All @@ -976,9 +976,9 @@ def filter_array(input, property, target_value, default_value = [], &block)

block.call(ary) do |item|
if target_value.nil?
fetch_property(item, property)
item[property]
else
fetch_property(item, property) == target_value
item[property] == target_value
end
rescue TypeError
raise_property_error(property)
Expand All @@ -988,31 +988,6 @@ def filter_array(input, property, target_value, default_value = [], &block)
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
2 changes: 1 addition & 1 deletion lib/liquid/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
# frozen_string_literal: true

module Liquid
VERSION = "5.7.1"
VERSION = "5.7.2"
end
117 changes: 0 additions & 117 deletions test/integration/standard_filter_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,30 +54,6 @@ def each(&block)
end
end

class TestDeepEnumerable < Liquid::Drop
include Enumerable

class Product < Liquid::Drop
attr_reader :title, :price, :premium

def initialize(title:, price:, premium: nil)
@title = { "content" => title, "language" => "en" }
@price = { "value" => price, "unit" => "USD" }
@premium = { "category" => premium } if premium
end
end

def each(&block)
[
Product.new(title: "Pro goggles", price: 1299),
Product.new(title: "Thermal gloves", price: 1299),
Product.new(title: "Alpine jacket", price: 3999, premium: 'Basic'),
Product.new(title: "Mountain boots", price: 3899, premium: 'Pro'),
Product.new(title: "Safety helmet", price: 1999)
].each(&block)
end
end

class NumberLikeThing < Liquid::Drop
def initialize(amount)
@amount = amount
Expand Down Expand Up @@ -438,15 +414,6 @@ def test_sort_natural_invalid_property
end
end

def test_sort_natural_with_deep_enumerables
template = <<~LIQUID
{{- products | sort_natural: 'title.content' | map: 'title.content' | join: ', ' -}}
LIQUID
expected_output = "Alpine jacket, Mountain boots, Pro goggles, Safety helmet, Thermal gloves"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_legacy_sort_hash
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
end
Expand Down Expand Up @@ -483,15 +450,6 @@ def test_uniq_invalid_property
end
end

def test_uniq_with_deep_enumerables
template = <<~LIQUID
{{- products | uniq: 'price.value' | map: "title.content" | join: ', ' -}}
LIQUID
expected_output = "Pro goggles, Alpine jacket, Mountain boots, Safety helmet"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_compact_empty_array
assert_equal([], @filters.compact([], "a"))
end
Expand All @@ -508,15 +466,6 @@ def test_compact_invalid_property
end
end

def test_compact_with_deep_enumerables
template = <<~LIQUID
{{- products | compact: 'premium.category' | map: 'title.content' | join: ', ' -}}
LIQUID
expected_output = "Alpine jacket, Mountain boots"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_reverse
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
end
Expand Down Expand Up @@ -626,15 +575,6 @@ def test_sort_works_on_enumerables
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
end

def test_sort_with_deep_enumerables
template = <<~LIQUID
{{- products | sort: 'price.value' | map: 'title.content' | join: ', ' -}}
LIQUID
expected_output = "Pro goggles, Thermal gloves, Safety helmet, Mountain boots, Alpine jacket"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_first_and_last_call_to_liquid
assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] })
assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] })
Expand Down Expand Up @@ -951,15 +891,6 @@ def test_reject_with_false_value
assert_template_result(expected_output, template, { "array" => array })
end

def test_reject_with_deep_enumerables
template = <<~LIQUID
{{- products | reject: 'title.content', 'Pro goggles' | map: 'price.value' | join: ', ' -}}
LIQUID
expected_output = "1299, 3999, 3899, 1999"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_has
array = [
{ "handle" => "alpha", "ok" => true },
Expand Down Expand Up @@ -1028,16 +959,6 @@ def test_has_with_false_value_when_does_not_have_it
assert_template_result(expected_output, template, { "array" => array })
end

def test_has_with_deep_enumerables
template = <<~LIQUID
{{- products | has: 'title.content', 'Pro goggles' -}},
{{- products | has: 'title.content', 'foo' -}}
LIQUID
expected_output = "true,false"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_find_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
Expand All @@ -1056,16 +977,6 @@ def test_find_with_value
assert_template_result(expected_output, template, { "products" => products })
end

def test_find_with_deep_enumerables
template = <<~LIQUID
{%- assign product = products | find: 'title.content', 'Pro goggles' -%}
{{- product.title.content -}}
LIQUID
expected_output = "Pro goggles"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_find_with_empty_arrays
template = <<~LIQUID
{%- assign product = products | find: 'title.content', 'Not found' -%}
Expand Down Expand Up @@ -1096,16 +1007,6 @@ def test_find_index_with_value
assert_template_result(expected_output, template, { "products" => products })
end

def test_find_index_with_deep_enumerables
template = <<~LIQUID
{%- assign index = products | find_index: 'title.content', 'Alpine jacket' -%}
{{- index -}}
LIQUID
expected_output = "2"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_find_index_with_empty_arrays
template = <<~LIQUID
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
Expand Down Expand Up @@ -1216,15 +1117,6 @@ def test_where_array_of_only_unindexable_values
assert_nil(@filters.where([nil], "ok"))
end

def test_where_with_deep_enumerables
template = <<~LIQUID
{{- products | where: 'title.content', 'Pro goggles' | map: 'price.value' -}}
LIQUID
expected_output = "1299"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new
Expand Down Expand Up @@ -1376,15 +1268,6 @@ def test_sum_with_floats_and_indexable_map_values
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
end

def test_sum_with_deep_enumerables
template = <<~LIQUID
{{- products | sum: 'price.value' -}}
LIQUID
expected_output = "12495"

assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end

private

def with_timezone(tz)
Expand Down

0 comments on commit f9454d8

Please sign in to comment.