Skip to content

Commit bfe29e1

Browse files
authoredJan 31, 2025
Merge pull request #1907 from Shopify/nested-properties
Fix array filters to not support nested properties
2 parents 8dd9279 + f9454d8 commit bfe29e1

File tree

4 files changed

+13
-151
lines changed

4 files changed

+13
-151
lines changed
 

‎History.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## 5.8.0 (unreleased)
44

5+
## 5.7.2 2025-01-31
6+
7+
* Fix array filters to not support nested properties
8+
59
## 5.7.1 2025-01-24
610

711
* Fix the `find` and `find_index`filters to return `nil` when filtering empty arrays

‎lib/liquid/standardfilters.rb

+8-33
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def sort(input, property = nil)
387387
end
388388
elsif ary.all? { |el| el.respond_to?(:[]) }
389389
begin
390-
ary.sort { |a, b| nil_safe_compare(fetch_property(a, property), fetch_property(b, property)) }
390+
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
391391
rescue TypeError
392392
raise_property_error(property)
393393
end
@@ -416,7 +416,7 @@ def sort_natural(input, property = nil)
416416
end
417417
elsif ary.all? { |el| el.respond_to?(:[]) }
418418
begin
419-
ary.sort { |a, b| nil_safe_casecmp(fetch_property(a, property), fetch_property(b, property)) }
419+
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
420420
rescue TypeError
421421
raise_property_error(property)
422422
end
@@ -504,7 +504,7 @@ def uniq(input, property = nil)
504504
[]
505505
else
506506
ary.uniq do |item|
507-
fetch_property(item, property)
507+
item[property]
508508
rescue TypeError
509509
raise_property_error(property)
510510
rescue NoMethodError
@@ -540,7 +540,7 @@ def map(input, property)
540540
if property == "to_liquid"
541541
e
542542
elsif e.respond_to?(:[])
543-
r = fetch_property(e, property)
543+
r = e[property]
544544
r.is_a?(Proc) ? r.call : r
545545
end
546546
end
@@ -564,7 +564,7 @@ def compact(input, property = nil)
564564
[]
565565
else
566566
ary.reject do |item|
567-
fetch_property(item, property).nil?
567+
item[property].nil?
568568
rescue TypeError
569569
raise_property_error(property)
570570
rescue NoMethodError
@@ -950,7 +950,7 @@ def sum(input, property = nil)
950950
if property.nil?
951951
item
952952
elsif item.respond_to?(:[])
953-
fetch_property(item, property)
953+
item[property]
954954
else
955955
0
956956
end
@@ -976,9 +976,9 @@ def filter_array(input, property, target_value, default_value = [], &block)
976976

977977
block.call(ary) do |item|
978978
if target_value.nil?
979-
fetch_property(item, property)
979+
item[property]
980980
else
981-
fetch_property(item, property) == target_value
981+
item[property] == target_value
982982
end
983983
rescue TypeError
984984
raise_property_error(property)
@@ -988,31 +988,6 @@ def filter_array(input, property, target_value, default_value = [], &block)
988988
end
989989
end
990990

991-
def fetch_property(drop, property_or_keys)
992-
##
993-
# This keeps backward compatibility by supporting properties containing
994-
# dots. This is valid in Liquid syntax and used in some runtimes, such as
995-
# Shopify with metafields.
996-
#
997-
# Using this approach, properties like 'price.value' can be accessed in
998-
# both of the following examples:
999-
#
1000-
# ```
1001-
# [
1002-
# { 'name' => 'Item 1', 'price.price' => 40000 },
1003-
# { 'name' => 'Item 2', 'price' => { 'value' => 39900 } }
1004-
# ]
1005-
# ```
1006-
value = drop[property_or_keys]
1007-
1008-
return value if !value.nil? || !property_or_keys.is_a?(String)
1009-
1010-
keys = property_or_keys.split('.')
1011-
keys.reduce(drop) do |drop, key|
1012-
drop.respond_to?(:[]) ? drop[key] : drop
1013-
end
1014-
end
1015-
1016991
def raise_property_error(property)
1017992
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
1018993
end

‎lib/liquid/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
# frozen_string_literal: true
33

44
module Liquid
5-
VERSION = "5.7.1"
5+
VERSION = "5.7.2"
66
end

‎test/integration/standard_filter_test.rb

-117
Original file line numberDiff line numberDiff line change
@@ -54,30 +54,6 @@ def each(&block)
5454
end
5555
end
5656

57-
class TestDeepEnumerable < Liquid::Drop
58-
include Enumerable
59-
60-
class Product < Liquid::Drop
61-
attr_reader :title, :price, :premium
62-
63-
def initialize(title:, price:, premium: nil)
64-
@title = { "content" => title, "language" => "en" }
65-
@price = { "value" => price, "unit" => "USD" }
66-
@premium = { "category" => premium } if premium
67-
end
68-
end
69-
70-
def each(&block)
71-
[
72-
Product.new(title: "Pro goggles", price: 1299),
73-
Product.new(title: "Thermal gloves", price: 1299),
74-
Product.new(title: "Alpine jacket", price: 3999, premium: 'Basic'),
75-
Product.new(title: "Mountain boots", price: 3899, premium: 'Pro'),
76-
Product.new(title: "Safety helmet", price: 1999)
77-
].each(&block)
78-
end
79-
end
80-
8157
class NumberLikeThing < Liquid::Drop
8258
def initialize(amount)
8359
@amount = amount
@@ -438,15 +414,6 @@ def test_sort_natural_invalid_property
438414
end
439415
end
440416

441-
def test_sort_natural_with_deep_enumerables
442-
template = <<~LIQUID
443-
{{- products | sort_natural: 'title.content' | map: 'title.content' | join: ', ' -}}
444-
LIQUID
445-
expected_output = "Alpine jacket, Mountain boots, Pro goggles, Safety helmet, Thermal gloves"
446-
447-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
448-
end
449-
450417
def test_legacy_sort_hash
451418
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
452419
end
@@ -483,15 +450,6 @@ def test_uniq_invalid_property
483450
end
484451
end
485452

486-
def test_uniq_with_deep_enumerables
487-
template = <<~LIQUID
488-
{{- products | uniq: 'price.value' | map: "title.content" | join: ', ' -}}
489-
LIQUID
490-
expected_output = "Pro goggles, Alpine jacket, Mountain boots, Safety helmet"
491-
492-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
493-
end
494-
495453
def test_compact_empty_array
496454
assert_equal([], @filters.compact([], "a"))
497455
end
@@ -508,15 +466,6 @@ def test_compact_invalid_property
508466
end
509467
end
510468

511-
def test_compact_with_deep_enumerables
512-
template = <<~LIQUID
513-
{{- products | compact: 'premium.category' | map: 'title.content' | join: ', ' -}}
514-
LIQUID
515-
expected_output = "Alpine jacket, Mountain boots"
516-
517-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
518-
end
519-
520469
def test_reverse
521470
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
522471
end
@@ -626,15 +575,6 @@ def test_sort_works_on_enumerables
626575
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
627576
end
628577

629-
def test_sort_with_deep_enumerables
630-
template = <<~LIQUID
631-
{{- products | sort: 'price.value' | map: 'title.content' | join: ', ' -}}
632-
LIQUID
633-
expected_output = "Pro goggles, Thermal gloves, Safety helmet, Mountain boots, Alpine jacket"
634-
635-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
636-
end
637-
638578
def test_first_and_last_call_to_liquid
639579
assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] })
640580
assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] })
@@ -951,15 +891,6 @@ def test_reject_with_false_value
951891
assert_template_result(expected_output, template, { "array" => array })
952892
end
953893

954-
def test_reject_with_deep_enumerables
955-
template = <<~LIQUID
956-
{{- products | reject: 'title.content', 'Pro goggles' | map: 'price.value' | join: ', ' -}}
957-
LIQUID
958-
expected_output = "1299, 3999, 3899, 1999"
959-
960-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
961-
end
962-
963894
def test_has
964895
array = [
965896
{ "handle" => "alpha", "ok" => true },
@@ -1028,16 +959,6 @@ def test_has_with_false_value_when_does_not_have_it
1028959
assert_template_result(expected_output, template, { "array" => array })
1029960
end
1030961

1031-
def test_has_with_deep_enumerables
1032-
template = <<~LIQUID
1033-
{{- products | has: 'title.content', 'Pro goggles' -}},
1034-
{{- products | has: 'title.content', 'foo' -}}
1035-
LIQUID
1036-
expected_output = "true,false"
1037-
1038-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
1039-
end
1040-
1041962
def test_find_with_value
1042963
products = [
1043964
{ "title" => "Pro goggles", "price" => 1299 },
@@ -1056,16 +977,6 @@ def test_find_with_value
1056977
assert_template_result(expected_output, template, { "products" => products })
1057978
end
1058979

1059-
def test_find_with_deep_enumerables
1060-
template = <<~LIQUID
1061-
{%- assign product = products | find: 'title.content', 'Pro goggles' -%}
1062-
{{- product.title.content -}}
1063-
LIQUID
1064-
expected_output = "Pro goggles"
1065-
1066-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
1067-
end
1068-
1069980
def test_find_with_empty_arrays
1070981
template = <<~LIQUID
1071982
{%- assign product = products | find: 'title.content', 'Not found' -%}
@@ -1096,16 +1007,6 @@ def test_find_index_with_value
10961007
assert_template_result(expected_output, template, { "products" => products })
10971008
end
10981009

1099-
def test_find_index_with_deep_enumerables
1100-
template = <<~LIQUID
1101-
{%- assign index = products | find_index: 'title.content', 'Alpine jacket' -%}
1102-
{{- index -}}
1103-
LIQUID
1104-
expected_output = "2"
1105-
1106-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
1107-
end
1108-
11091010
def test_find_index_with_empty_arrays
11101011
template = <<~LIQUID
11111012
{%- assign index = products | find_index: 'title.content', 'Not found' -%}
@@ -1216,15 +1117,6 @@ def test_where_array_of_only_unindexable_values
12161117
assert_nil(@filters.where([nil], "ok"))
12171118
end
12181119

1219-
def test_where_with_deep_enumerables
1220-
template = <<~LIQUID
1221-
{{- products | where: 'title.content', 'Pro goggles' | map: 'price.value' -}}
1222-
LIQUID
1223-
expected_output = "1299"
1224-
1225-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
1226-
end
1227-
12281120
def test_all_filters_never_raise_non_liquid_exception
12291121
test_drop = TestDrop.new(value: "test")
12301122
test_drop.context = Context.new
@@ -1376,15 +1268,6 @@ def test_sum_with_floats_and_indexable_map_values
13761268
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
13771269
end
13781270

1379-
def test_sum_with_deep_enumerables
1380-
template = <<~LIQUID
1381-
{{- products | sum: 'price.value' -}}
1382-
LIQUID
1383-
expected_output = "12495"
1384-
1385-
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
1386-
end
1387-
13881271
private
13891272

13901273
def with_timezone(tz)

0 commit comments

Comments
 (0)
Failed to load comments.