From 85a70d7cae85b9c3a38b21b052c75f3afd6660d7 Mon Sep 17 00:00:00 2001 From: Guilherme Carreiro Date: Fri, 15 Nov 2024 16:33:27 +0100 Subject: [PATCH] Prototype --- lib/liquid/standardfilters.rb | 127 +++++-- test/integration/standard_filter_test.rb | 421 ++++++++++++++++++++++- 2 files changed, 511 insertions(+), 37 deletions(-) diff --git a/lib/liquid/standardfilters.rb b/lib/liquid/standardfilters.rb index ffaceb32..0d3bc08b 100644 --- a/lib/liquid/standardfilters.rb +++ b/lib/liquid/standardfilters.rb @@ -207,12 +207,18 @@ module Liquid def slice(input, offset, length = nil) offset = Utils.to_integer(offset) length = length ? Utils.to_integer(length) : 1 + default_value = [] begin - if input.is_a?(Array) - input.slice(offset, length) || [] + unless input.is_a?(Array) + default_value = '' + input = input.to_s + end + + if length.negative? + input[offset...length] || default_value else - input.to_s.slice(offset, length) || '' + input.slice(offset, length) || default_value end rescue RangeError if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset) @@ -424,29 +430,59 @@ module Liquid # @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 @@ -918,6 +954,47 @@ module Liquid 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| + case target_value + when nil + item[property] + when Hash + compare_with_operator(item[property], target_value) + else + item[property] == target_value + end + rescue TypeError + raise_property_error(property) + rescue NoMethodError + return nil unless item.respond_to?(:[]) + raise + end + end + + def compare_with_operator(value, comparison) + operation = comparison.keys.first + compare_value = comparison.values.first + operators = { + 'greater' => ->(a, b) { a > b }, + 'less' => ->(a, b) { a < b }, + 'greater_or_equal' => ->(a, b) { a >= b }, + 'less_or_equal' => ->(a, b) { a <= b }, + 'contains' => ->(a, b) { a.to_s.include?(b.to_s) }, + } + + operator = operators[operation] + operator && operator.call(value, compare_value) + rescue NoMethodError, TypeError, ArgumentError + false + rescue StandardError + false + end + def raise_property_error(property) raise Liquid::ArgumentError, "cannot select the property '#{property}'" end diff --git a/test/integration/standard_filter_test.rb b/test/integration/standard_filter_test.rb index eae4a1c9..bc5a0234 100644 --- a/test/integration/standard_filter_test.rb +++ b/test/integration/standard_filter_test.rb @@ -131,6 +131,9 @@ class StandardFiltersTest < Minitest::Test assert_equal(input, @filters.slice(input, 0, 1 << 63)) assert_equal([], @filters.slice(input, 1 << 63, 6)) assert_equal([], @filters.slice(input, -(1 << 63), 6)) + assert_equal(%w(b a), @filters.slice(input, 3, -1)) + assert_equal(%w(o b), @filters.slice(input, 2, -2)) + assert_equal(%w(f o o), @filters.slice(input, 0, -3)) end def test_truncate @@ -778,14 +781,69 @@ class StandardFiltersTest < Minitest::Test assert_template_result('bcd', "{{ a | append: b}}", assigns) end - def test_concat - assert_equal([1, 2, 3, 4], @filters.concat([1, 2], [3, 4])) - assert_equal([1, 2, 'a'], @filters.concat([1, 2], ['a'])) - assert_equal([1, 2, 10], @filters.concat([1, 2], [10])) + def test_append_with_arrays + products = [ + "Snowdevil pro goggles", + "Snowdevil alpine jacket", + ] - assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do - @filters.concat([1, 2], 10) - end + template = <<~LIQUID + {{ + products + | append: 'Snowdevil bonus gift' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil pro goggles, Snowdevil alpine jacket, Snowdevil bonus gift" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_concat + array1 = [1, 2] + array2 = [3, 4] + + template = <<~LIQUID + {{ + array1 + | concat: array2 + | join: ', ' + -}} + LIQUID + expected_output = "1, 2, 3, 4" + + assert_template_result(expected_output, template, { "array1" => array1, "array2" => array2 }) + end + + def test_concat_with_string + array1 = [1, 2] + array2 = ['a'] + + template = <<~LIQUID + {{ + array1 + | concat: array2 + | join: ', ' + -}} + LIQUID + expected_output = "1, 2, a" + + assert_template_result(expected_output, template, { "array1" => array1, "array2" => array2 }) + end + + def test_concat_with_number + array = [1, 2] + + template = <<~LIQUID + {{ + array + | concat: 10 + | join: ', ' + -}} + LIQUID + expected_output = "1, 2, 10" + + assert_template_result(expected_output, template, { "array" => array }) end def test_prepend @@ -794,6 +852,24 @@ class StandardFiltersTest < Minitest::Test assert_template_result('abc', "{{ a | prepend: b}}", assigns) end + def test_prepend_with_arrays + products = [ + "Snowdevil pro goggles", + "Snowdevil alpine jacket", + ] + + template = <<~LIQUID + {{ + products + | prepend: 'Snowdevil bonus gift' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil bonus gift, Snowdevil pro goggles, Snowdevil alpine jacket" + + assert_template_result(expected_output, template, { "products" => products }) + end + def test_default assert_equal("foo", @filters.default("foo", "bar")) assert_equal("bar", @filters.default(nil, "bar")) @@ -827,21 +903,306 @@ class StandardFiltersTest < Minitest::Test assert_template_result('abc', "{{ 'abc' | date: '%D' }}") end - def test_where - input = [ + def test_where_with_value + array = [ { "handle" => "alpha", "ok" => true }, { "handle" => "beta", "ok" => false }, { "handle" => "gamma", "ok" => false }, { "handle" => "delta", "ok" => true }, ] - expectation = [ + template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}" + expected_output = "alpha delta" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_where_without_value + array = [ { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, { "handle" => "delta", "ok" => true }, ] - assert_equal(expectation, @filters.where(input, "ok", true)) - assert_equal(expectation, @filters.where(input, "ok")) + template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}" + expected_output = "alpha delta" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_where_without_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}" + expected_output = "beta gamma" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_where_with_greater_than + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {{ + products + | where: 'price', greater: 300 + | map: 'title' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil alpine jacket, Snowdevil mountain boots" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_where_with_less_than + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {{ + products + | where: 'price', less: 150 + | map: 'title' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil pro goggles, Snowdevil thermal gloves" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_where_with_greater_or_equal + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {{ + products + | where: 'price', greater_or_equal: 389.99 + | map: 'title' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil alpine jacket, Snowdevil mountain boots" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_where_with_less_or_equal + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {{ + products + | where: 'price', less_or_equal: 149.99 + | map: 'title' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil pro goggles, Snowdevil thermal gloves" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_where_with_contains + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {{ + products + | where: 'title', contains: 'es' + | map: 'title' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil pro goggles, Snowdevil thermal gloves" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_reject_with_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | reject: 'ok', true | map: 'handle' | join: ' ' }}" + expected_output = "beta gamma" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def xtest_reject_with_value2 + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | add:array.first }}" + expected_output = "beta gamma" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_reject_with_greater_operator + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {{ + products + | reject: 'price', greater: 190 + | map: 'title' + | join: ', ' + -}} + LIQUID + expected_output = "Snowdevil pro goggles, Snowdevil thermal gloves" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_reject_without_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | reject: 'ok' | map: 'handle' | join: ' ' }}" + expected_output = "beta gamma" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_reject_with_false_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | reject: 'ok', false | map: 'handle' | join: ' ' }}" + expected_output = "alpha delta" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_some_with_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | some: 'ok', true }}" + expected_output = "true" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_some_without_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | some: 'ok' }}" + expected_output = "true" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_some_with_false_value + array = [ + { "handle" => "alpha", "ok" => true }, + { "handle" => "beta", "ok" => false }, + { "handle" => "gamma", "ok" => false }, + { "handle" => "delta", "ok" => true }, + ] + + template = "{{ array | some: 'ok', false }}" + expected_output = "true" + + assert_template_result(expected_output, template, { "array" => array }) + end + + def test_some_with_contains + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = "{{ products | some: 'title', contains: 'safety' }}" + expected_output = "true" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_some_with_contains + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = "{{ products | some: 'title', contains: 'game boy' }}" + expected_output = "false" + + assert_template_result(expected_output, template, { "products" => products }) end def test_where_string_keys @@ -873,6 +1234,42 @@ class StandardFiltersTest < Minitest::Test assert_equal(expectation, @filters.where(input, "ok")) end + def test_find_with_value + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {%- assign product = products | find: 'price', greater: 150 -%} + {{- product.title -}} + LIQUID + expected_output = "Snowdevil alpine jacket" + + assert_template_result(expected_output, template, { "products" => products }) + end + + def test_find_index_with_value + products = [ + { "title" => "Snowdevil pro goggles", "price" => 129.99 }, + { "title" => "Snowdevil thermal gloves", "price" => 149.99 }, + { "title" => "Snowdevil alpine jacket", "price" => 399.99 }, + { "title" => "Snowdevil mountain boots", "price" => 389.99 }, + { "title" => "Snowdevil safety helmet", "price" => 199.99 } + ] + + template = <<~LIQUID + {%- assign index = products | find_index: 'price', greater: 150 -%} + {{- index -}} + LIQUID + expected_output = "2" + + assert_template_result(expected_output, template, { "products" => products }) + end + def test_where_non_array_map_input assert_equal([{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")) assert_equal([], @filters.where({ "a" => "not ok" }, "a", "ok"))