diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index bbfbc6132..90d2dc15c 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -367,7 +367,10 @@ def join(input, glue = ' ') # Sorts the items in an array in case-sensitive alphabetical, or numerical, order. # @liquid_syntax array | sort # @liquid_return [array[untyped]] - def sort(input, property = nil) + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def sort(input, property = nil, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) ary = InputIterator.new(input, context) return [] if ary.empty? @@ -378,7 +381,13 @@ 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 do |a, b| + if deep[:enable] + nil_safe_compare(a.dig(*deep[:properties]), b.dig(*deep[:properties])) + else + nil_safe_compare(a[property], b[property]) + end + end rescue TypeError raise_property_error(property) end @@ -396,7 +405,10 @@ def sort(input, property = nil) # > string, so sorting on numerical values can lead to unexpected results. # @liquid_syntax array | sort_natural # @liquid_return [array[untyped]] - def sort_natural(input, property = nil) + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def sort_natural(input, property = nil, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) ary = InputIterator.new(input, context) return [] if ary.empty? @@ -407,7 +419,13 @@ 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 do |a, b| + if deep[:enable] + nil_safe_casecmp(a.dig(*deep[:properties]), b.dig(*deep[:properties])) + else + nil_safe_casecmp(a[property], b[property]) + end + end rescue TypeError raise_property_error(property) end @@ -423,7 +441,10 @@ def sort_natural(input, property = nil) # This requires you to provide both the property name and the associated value. # @liquid_syntax array | where: string, string # @liquid_return [array[untyped]] - def where(input, property, target_value = nil) + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def where(input, property, target_value = nil, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) ary = InputIterator.new(input, context) if ary.empty? @@ -439,7 +460,8 @@ def where(input, property, target_value = nil) end else ary.select do |item| - item[property] == target_value + item_value = deep[:enable] ? item.dig(*deep[:properties]) : item[property] + item_value == target_value rescue TypeError raise_property_error(property) rescue NoMethodError @@ -457,6 +479,7 @@ def where(input, property, target_value = nil) # Removes any duplicate items in an array. # @liquid_syntax array | uniq # @liquid_return [array[untyped]] +<<<<<<< HEAD ======= # Reject the elements of an array to those with a certain property value. # By default the target is any falsy value. @@ -485,6 +508,12 @@ def reject(input, properties, target_value = nil) # provide optional property with which to determine uniqueness >>>>>>> bfdcfcea (Add reject filter) def uniq(input, property = nil) +======= + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def uniq(input, property = nil, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) +>>>>>>> 58d7d1a8 (Add deep search for suitable filters) ary = InputIterator.new(input, context) if property.nil? @@ -493,7 +522,7 @@ def uniq(input, property = nil) [] else ary.uniq do |item| - item[property] + deep[:enable] ? item.dig(*deep[:properties]) : item[property] rescue TypeError raise_property_error(property) rescue NoMethodError @@ -522,15 +551,19 @@ def reverse(input) # Creates an array of values from a specific property of the items in an array. # @liquid_syntax array | map: string # @liquid_return [array[untyped]] - def map(input, property) - InputIterator.new(input, context).map do |e| - e = e.call if e.is_a?(Proc) + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def map(input, property, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) + + InputIterator.new(input, context).map do |item| + item = item.call if item.is_a?(Proc) if property == "to_liquid" - e - elsif e.respond_to?(:[]) - r = e[property] - r.is_a?(Proc) ? r.call : r + item + elsif item.respond_to?(:[]) + result = deep[:enable] ? item.dig(*deep[:properties]) : item[property] + result.is_a?(Proc) ? result.call : result end end rescue TypeError @@ -544,7 +577,10 @@ def map(input, property) # Removes any `nil` items from an array. # @liquid_syntax array | compact # @liquid_return [array[untyped]] - def compact(input, property = nil) + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def compact(input, property = nil, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) ary = InputIterator.new(input, context) if property.nil? @@ -553,7 +589,7 @@ def compact(input, property = nil) [] else ary.reject do |item| - item[property].nil? + deep[:enable] ? item.dig(*deep[:properties]).nil? : item[property].nil? rescue TypeError raise_property_error(property) rescue NoMethodError @@ -963,7 +999,11 @@ def default(input, default_value = '', options = {}) # Returns the sum of all elements in an array. # @liquid_syntax array | sum # @liquid_return [number] - def sum(input, property = nil) + # @liquid_optional_param deep [boolean | string] Whether to use dot notation to perform a deep search. A string can be passed to change separator. + def sum(input, property = nil, options = {}) + options = {} unless options.is_a?(Hash) + deep = deep_search_properties(property, options) + ary = InputIterator.new(input, context) return 0 if ary.empty? @@ -971,7 +1011,7 @@ def sum(input, property = nil) if property.nil? item elsif item.respond_to?(:[]) - item[property] + deep[:enable] ? item.dig(*deep[:properties]) : item[property] else 0 end @@ -1023,6 +1063,20 @@ def nil_safe_casecmp(a, b) end end + def deep_search_properties(key, options = {}) + options = {} unless options.is_a?(Hash) + + enable = options['deep'] ? true : false + separator = options['deep'].is_a?(String) ? options['deep'] : '.' if enable + properties = key.to_s.split(separator) if enable + + { + enable: enable, + separator: separator, + properties: properties, + } + end + class InputIterator include Enumerable diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index 6ac753711..221254904 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -286,6 +286,60 @@ def test_sort assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")) end + def test_sort_deep_default_separator + input = [ + { "foo" => { "price" => 4, "handle" => "alpha" } }, + { "foo" => { "handle" => "beta" } }, + { "foo" => { "price" => 1, "handle" => "gamma" } }, + { "foo" => { "handle" => "delta" } }, + { "foo" => { "price" => 2, "handle" => "epsilon" } }, + ] + expectation = [ + { "foo" => { "price" => 1, "handle" => "gamma" } }, + { "foo" => { "price" => 2, "handle" => "epsilon" } }, + { "foo" => { "price" => 4, "handle" => "alpha" } }, + { "foo" => { "handle" => "beta" } }, + { "foo" => { "handle" => "delta" } }, + ] + assert_equal(expectation, @filters.sort(input, "foo.price", { "deep" => true })) + end + + def test_sort_deep_custom_separator + input = [ + { "foo" => { "price" => 4, "handle" => "alpha" } }, + { "foo" => { "handle" => "beta" } }, + { "foo" => { "price" => 1, "handle" => "gamma" } }, + { "foo" => { "handle" => "delta" } }, + { "foo" => { "price" => 2, "handle" => "epsilon" } }, + ] + expectation = [ + { "foo" => { "price" => 1, "handle" => "gamma" } }, + { "foo" => { "price" => 2, "handle" => "epsilon" } }, + { "foo" => { "price" => 4, "handle" => "alpha" } }, + { "foo" => { "handle" => "beta" } }, + { "foo" => { "handle" => "delta" } }, + ] + assert_equal(expectation, @filters.sort(input, "foo_price", { "deep" => "_" })) + end + + def test_sort_deep_off_by_default + input = [ + { "foo.price" => 4, "handle" => "alpha" }, + { "handle" => "beta" }, + { "foo.price" => 1, "handle" => "gamma" }, + { "handle" => "delta" }, + { "foo.price" => 2, "handle" => "epsilon" }, + ] + expectation = [ + { "foo.price" => 1, "handle" => "gamma" }, + { "foo.price" => 2, "handle" => "epsilon" }, + { "foo.price" => 4, "handle" => "alpha" }, + { "handle" => "beta" }, + { "handle" => "delta" }, + ] + assert_equal(expectation, @filters.sort(input, "foo.price")) + end + def test_sort_with_nils assert_equal([1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])) assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")) @@ -360,6 +414,72 @@ def test_sort_natural_case_check assert_equal(["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])) end + def test_sort_natural_deep_default_separator + input = [ + { "foo" => { "key" => "X" } }, + { "foo" => { "key" => "Y" } }, + { "foo" => { "key" => "Z" } }, + { "foo" => { "fake" => "t" } }, + { "foo" => { "key" => "a" } }, + { "foo" => { "key" => "b" } }, + { "foo" => { "key" => "c" } }, + ] + expectation = [ + { "foo" => { "key" => "a" } }, + { "foo" => { "key" => "b" } }, + { "foo" => { "key" => "c" } }, + { "foo" => { "key" => "X" } }, + { "foo" => { "key" => "Y" } }, + { "foo" => { "key" => "Z" } }, + { "foo" => { "fake" => "t" } }, + ] + assert_equal(expectation, @filters.sort_natural(input, "foo.key", { "deep" => true })) + end + + def test_sort_natural_deep_custom_separator + input = [ + { "foo" => { "key" => "X" } }, + { "foo" => { "key" => "Y" } }, + { "foo" => { "key" => "Z" } }, + { "foo" => { "fake" => "t" } }, + { "foo" => { "key" => "a" } }, + { "foo" => { "key" => "b" } }, + { "foo" => { "key" => "c" } }, + ] + expectation = [ + { "foo" => { "key" => "a" } }, + { "foo" => { "key" => "b" } }, + { "foo" => { "key" => "c" } }, + { "foo" => { "key" => "X" } }, + { "foo" => { "key" => "Y" } }, + { "foo" => { "key" => "Z" } }, + { "foo" => { "fake" => "t" } }, + ] + assert_equal(expectation, @filters.sort_natural(input, "foo_key", { "deep" => "_" })) + end + + def test_sort_natural_deep_off_by_default + input = [ + { "foo.key" => "X" }, + { "foo.key" => "Y" }, + { "foo.key" => "Z" }, + { "foo.fake" => "t" }, + { "foo.key" => "a" }, + { "foo.key" => "b" }, + { "foo.key" => "c" }, + ] + expectation = [ + { "foo.key" => "a" }, + { "foo.key" => "b" }, + { "foo.key" => "c" }, + { "foo.key" => "X" }, + { "foo.key" => "Y" }, + { "foo.key" => "Z" }, + { "foo.fake" => "t" }, + ] + assert_equal(expectation, @filters.sort_natural(input, "foo.key")) + end + def test_sort_empty_array assert_equal([], @filters.sort([], "a")) end @@ -428,6 +548,45 @@ def test_uniq_invalid_property end end + def test_uniq_deep_default_separator + input = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "baz", "handle" => "beta" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + expectation = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + assert_equal(expectation, @filters.uniq(input, "foo.bar", { "deep" => true })) + end + + def test_uniq_deep_custom_separator + input = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "baz", "handle" => "beta" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + expectation = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + assert_equal(expectation, @filters.uniq(input, "foo_bar", { "deep" => "_" })) + end + + def test_uniq_deep_off_by_default + input = [ + { "foo.bar" => "baz", "handle" => "alpha" }, + { "foo.bar" => "baz", "handle" => "beta" }, + { "foo.bar" => "qux", "handle" => "charlie" }, + ] + expectation = [ + { "foo.bar" => "baz", "handle" => "alpha" }, + { "foo.bar" => "qux", "handle" => "charlie" }, + ] + assert_equal(expectation, @filters.uniq(input, "foo.bar")) + end + def test_compact_empty_array assert_equal([], @filters.compact([], "a")) end @@ -444,6 +603,45 @@ def test_compact_invalid_property end end + def test_compact_deep_default_separator + input = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "handle" => "beta" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + expectation = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + assert_equal(expectation, @filters.compact(input, "foo.bar", { "deep" => true })) + end + + def test_compact_deep_custom_separator + input = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "handle" => "beta" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + expectation = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + assert_equal(expectation, @filters.compact(input, "foo_bar", { "deep" => "_" })) + end + + def test_compact_deep_off_by_default + input = [ + { "foo.bar" => "baz", "handle" => "alpha" }, + { "foo.handle" => "beta" }, + { "foo.bar" => "qux", "handle" => "charlie" }, + ] + expectation = [ + { "foo.bar" => "baz", "handle" => "alpha" }, + { "foo.bar" => "qux", "handle" => "charlie" }, + ] + assert_equal(expectation, @filters.compact(input, "foo.bar")) + end + def test_reverse assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])) end @@ -461,6 +659,30 @@ def test_map ) end + def test_map_deep_default_separator + assert_template_result( + 'abc', + "{{ ary | map: 'foo.bar', deep: true }}", + { 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] }, + ) + end + + def test_map_deep_custom_separator + assert_template_result( + 'abc', + "{{ ary | map: 'foo_bar', deep: '_' }}", + { 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] }, + ) + end + + def test_map_deep_off_by_default + assert_template_result( + 'abc', + "{{ ary | map: 'foo.bar' }}", + { 'ary' => [{ 'foo.bar' => 'a' }, { 'foo.bar' => 'b' }, { 'foo.bar' => 'c' }] }, + ) + end + def test_map_doesnt_call_arbitrary_stuff assert_template_result("", '{{ "foo" | map: "__id__" }}') assert_template_result("", '{{ "foo" | map: "inspect" }}') @@ -972,6 +1194,7 @@ def test_where_array_of_only_unindexable_values assert_nil(@filters.where([nil], "ok")) end +<<<<<<< HEAD def test_reject input = [ { "handle" => "alpha", "ok" => true }, @@ -1045,6 +1268,45 @@ def test_reject_deep assert_equal(expectation, @filters.reject(input, "item.ok", false)) assert_equal(expectation, @filters.reject(input, "item.ok")) +======= + def test_where_deep_default_separator + input = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "baz", "handle" => "beta" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + expectation = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "baz", "handle" => "beta" } }, + ] + assert_equal(expectation, @filters.where(input, "foo.bar", "baz", { "deep" => true })) + end + + def test_where_deep_custom_separator + input = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "baz", "handle" => "beta" } }, + { "foo" => { "bar" => "qux", "handle" => "charlie" } }, + ] + expectation = [ + { "foo" => { "bar" => "baz", "handle" => "alpha" } }, + { "foo" => { "bar" => "baz", "handle" => "beta" } }, + ] + assert_equal(expectation, @filters.where(input, "foo_bar", "baz", { "deep" => "_" })) + end + + def test_where_deep_off_by_default + input = [ + { "foo.bar" => "baz", "handle" => "alpha" }, + { "foo.bar" => "baz", "handle" => "beta" }, + { "foo.bar" => "qux", "handle" => "charlie" }, + ] + expectation = [ + { "foo.bar" => "baz", "handle" => "alpha" }, + { "foo.bar" => "baz", "handle" => "beta" }, + ] + assert_equal(expectation, @filters.where(input, "foo.bar", "baz")) +>>>>>>> 2800d8dc (Add missing tests) end def test_all_filters_never_raise_non_liquid_exception @@ -1182,6 +1444,7 @@ def test_sum_with_property_calls_to_liquid_on_property_values assert(t.foo > 0) end +<<<<<<< HEAD def test_sum_of_floats input = [0.1, 0.2, 0.3] assert_equal(0.6, @filters.sum(input)) @@ -1216,6 +1479,36 @@ def test_sum_with_floats_and_indexable_map_values assert_template_result("1.2", "{{ input | sum: 'quantity' }}", { "input" => input }) assert_template_result("0.1", "{{ input | sum: 'weight' }}", { "input" => input }) assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input }) +======= + def test_sum_deep_default_separator + input = [ + { "foo" => { "quantity" => 1 } }, + { "foo" => { "quantity" => 2 } }, + { "foo" => { "quantity" => 3 } }, + { "foo" => { "quantity" => 4 } }, + ] + assert_equal(10, @filters.sum(input, "foo.quantity", { "deep" => true })) + end + + def test_sum_deep_custom_separator + input = [ + { "foo" => { "quantity" => 1 } }, + { "foo" => { "quantity" => 2 } }, + { "foo" => { "quantity" => 3 } }, + { "foo" => { "quantity" => 4 } }, + ] + assert_equal(10, @filters.sum(input, "foo_quantity", { "deep" => "_" })) + end + + def test_sum_deep_off_by_default + input = [ + { "foo.quantity" => 1 }, + { "foo.quantity" => 2 }, + { "foo.quantity" => 3 }, + { "foo.quantity" => 4 }, + ] + assert_equal(10, @filters.sum(input, "foo.quantity")) +>>>>>>> 858428f2 (Initial new tests) end private