mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 08:50:45 -07:00
Add ExpressionParser and ExpressionConsumer
This commit is contained in:
committed by
Guilherme Carreiro
parent
6a9e46dd19
commit
1bff382ebc
@@ -0,0 +1,429 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ExpressionConsumerTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_consume_string_literal_with_double_quotes
|
||||
parser = parse_context.new_parser('"hello"')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('hello', result)
|
||||
end
|
||||
|
||||
def test_consume_string_literal_with_single_quotes
|
||||
parser = parse_context.new_parser("'world'")
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('world', result)
|
||||
end
|
||||
|
||||
def test_consume_string_with_empty_content
|
||||
parser = parse_context.new_parser('""')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('', result)
|
||||
end
|
||||
|
||||
def test_consume_single_quote_empty_string
|
||||
parser = parse_context.new_parser("''")
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('', result)
|
||||
end
|
||||
|
||||
def test_consume_integer_literal
|
||||
parser = parse_context.new_parser('42')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(42, result)
|
||||
assert_kind_of(Integer, result)
|
||||
end
|
||||
|
||||
def test_consume_negative_integer_literal
|
||||
parser = parse_context.new_parser('-42')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(-42, result)
|
||||
end
|
||||
|
||||
def test_consume_float_literal
|
||||
parser = parse_context.new_parser('3.14')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(3.14, result)
|
||||
assert_kind_of(Float, result)
|
||||
end
|
||||
|
||||
def test_consume_negative_float_literal
|
||||
parser = parse_context.new_parser('-3.14')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(-3.14, result)
|
||||
end
|
||||
|
||||
def test_consume_zero_as_integer
|
||||
parser = parse_context.new_parser('0')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(0, result)
|
||||
assert_kind_of(Integer, result)
|
||||
end
|
||||
|
||||
def test_consume_zero_as_float
|
||||
parser = parse_context.new_parser('0.0')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(0.0, result)
|
||||
assert_kind_of(Float, result)
|
||||
end
|
||||
|
||||
def test_consume_nil_literal
|
||||
parser = parse_context.new_parser('nil')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
def test_consume_null_literal
|
||||
parser = parse_context.new_parser('null')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
def test_consume_true_literal
|
||||
parser = parse_context.new_parser('true')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(true, result)
|
||||
end
|
||||
|
||||
def test_consume_false_literal
|
||||
parser = parse_context.new_parser('false')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(false, result)
|
||||
end
|
||||
|
||||
def test_consume_blank_literal
|
||||
parser = parse_context.new_parser('blank')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('', result)
|
||||
end
|
||||
|
||||
def test_consume_empty_literal
|
||||
parser = parse_context.new_parser('empty')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('', result)
|
||||
end
|
||||
|
||||
def test_consume_nil_literal_with_lookups
|
||||
parser = parse_context.new_parser('nil.size')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('nil', result.name)
|
||||
assert_equal(['size'], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_true_literal_with_lookups
|
||||
parser = parse_context.new_parser('true.size')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('true', result.name)
|
||||
assert_equal(['size'], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_negative_number_parses_as_number
|
||||
parser = parse_context.new_parser('-5')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(-5, result)
|
||||
end
|
||||
|
||||
def test_consume_simple_variable
|
||||
parser = parse_context.new_parser('product')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal([], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_variable_with_dot_lookup
|
||||
parser = parse_context.new_parser('product.title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_variable_with_multiple_dot_lookups
|
||||
parser = parse_context.new_parser('product.variants.first')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['variants', 'first'], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_variable_with_bracket_lookup
|
||||
parser = parse_context.new_parser('items[0]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('items', result.name)
|
||||
assert_equal(0, result.lookups[0])
|
||||
end
|
||||
|
||||
def test_consume_variable_with_bracket_string_lookup
|
||||
parser = parse_context.new_parser('items["key"]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('items', result.name)
|
||||
assert_equal('key', result.lookups[0])
|
||||
end
|
||||
|
||||
def test_consume_variable_with_bracket_variable_lookup
|
||||
parser = parse_context.new_parser('items[index]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('index', result.lookups[0].name)
|
||||
end
|
||||
|
||||
def test_consume_variable_with_mixed_lookups
|
||||
parser = parse_context.new_parser('product.variants[0].title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(3, result.lookups.length)
|
||||
assert_equal('variants', result.lookups[0])
|
||||
assert_equal(0, result.lookups[1])
|
||||
assert_equal('title', result.lookups[2])
|
||||
end
|
||||
|
||||
def test_consume_bracket_notation_without_variable
|
||||
parser = parse_context.new_parser('[0]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal(0, result.name)
|
||||
end
|
||||
|
||||
def test_consume_bracket_notation_with_lookups
|
||||
parser = parse_context.new_parser('[0].title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal(0, result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_bracket_notation_with_bracket_lookups
|
||||
parser = parse_context.new_parser('[0][1]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal(0, result.name)
|
||||
assert_equal(1, result.lookups[0])
|
||||
end
|
||||
|
||||
def test_consume_range_with_integer_literals
|
||||
parser = parse_context.new_parser('(1..5)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(Range, result)
|
||||
assert_equal(1..5, result)
|
||||
end
|
||||
|
||||
def test_consume_range_with_negative_integers
|
||||
parser = parse_context.new_parser('(-5..-1)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(Range, result)
|
||||
assert_equal(-5..-1, result)
|
||||
end
|
||||
|
||||
def test_consume_range_with_variable_start
|
||||
parser = parse_context.new_parser('(start..10)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(RangeLookup, result)
|
||||
assert_kind_of(VariableLookup, result.start_obj)
|
||||
assert_equal(10, result.end_obj)
|
||||
end
|
||||
|
||||
def test_consume_range_with_variable_end
|
||||
parser = parse_context.new_parser('(1..end)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(RangeLookup, result)
|
||||
assert_equal(1, result.start_obj)
|
||||
assert_kind_of(VariableLookup, result.end_obj)
|
||||
end
|
||||
|
||||
def test_consume_range_with_both_variables
|
||||
parser = parse_context.new_parser('(start..end)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(RangeLookup, result)
|
||||
assert_kind_of(VariableLookup, result.start_obj)
|
||||
assert_kind_of(VariableLookup, result.end_obj)
|
||||
end
|
||||
|
||||
def test_consume_range_with_variable_lookups
|
||||
parser = parse_context.new_parser('(start.value..end.value)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(RangeLookup, result)
|
||||
assert_equal(['value'], result.start_obj.lookups)
|
||||
end
|
||||
|
||||
def test_consume_command_method_size_sets_flag
|
||||
parser = parse_context.new_parser('items.size')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert(result.lookup_command?(0))
|
||||
end
|
||||
|
||||
def test_consume_command_method_first_sets_flag
|
||||
parser = parse_context.new_parser('items.first')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert(result.lookup_command?(0))
|
||||
end
|
||||
|
||||
def test_consume_command_method_last_sets_flag
|
||||
parser = parse_context.new_parser('items.last')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert(result.lookup_command?(0))
|
||||
end
|
||||
|
||||
def test_consume_non_command_method_does_not_set_flag
|
||||
parser = parse_context.new_parser('items.title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
refute(result.lookup_command?(0))
|
||||
end
|
||||
|
||||
def test_consume_advances_parser_position
|
||||
parser = parse_context.new_parser('foo.bar')
|
||||
ExpressionConsumer.consume(parser, parse_context)
|
||||
assert(parser.look(:end_of_string))
|
||||
end
|
||||
|
||||
def test_consume_stops_before_extra_tokens
|
||||
parser = parse_context.new_parser('foo bar')
|
||||
ExpressionConsumer.consume(parser, parse_context)
|
||||
refute(parser.look(:end_of_string))
|
||||
end
|
||||
|
||||
def test_consume_with_nested_brackets
|
||||
parser = parse_context.new_parser('items[items[0]]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_kind_of(VariableLookup, result.lookups[0])
|
||||
end
|
||||
|
||||
def test_consume_bracket_with_range
|
||||
parser = parse_context.new_parser('items[(1..3)]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(Range, result.lookups[0])
|
||||
assert_equal(1..3, result.lookups[0])
|
||||
end
|
||||
|
||||
def test_consume_raises_on_invalid_token_type
|
||||
parser = parse_context.new_parser('|')
|
||||
error = assert_raises(SyntaxError) do
|
||||
ExpressionConsumer.consume(parser, parse_context)
|
||||
end
|
||||
assert_match(/is not a valid expression/, error.message)
|
||||
end
|
||||
|
||||
def test_consume_range_with_string_literals
|
||||
parser = parse_context.new_parser('("a".."z")')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(Range, result)
|
||||
assert_equal(0..0, result)
|
||||
end
|
||||
|
||||
def test_consume_multiple_command_methods
|
||||
parser = parse_context.new_parser('items.first.size.last')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert(result.lookup_command?(0))
|
||||
assert(result.lookup_command?(1))
|
||||
assert(result.lookup_command?(2))
|
||||
end
|
||||
|
||||
def test_consume_dot_after_bracket
|
||||
parser = parse_context.new_parser('items[0].title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(0, result.lookups[0])
|
||||
assert_equal('title', result.lookups[1])
|
||||
end
|
||||
|
||||
def test_consume_bracket_after_dot
|
||||
parser = parse_context.new_parser('product.variants[0]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal('variants', result.lookups[0])
|
||||
assert_equal(0, result.lookups[1])
|
||||
end
|
||||
|
||||
def test_consume_multiple_brackets_with_different_types
|
||||
parser = parse_context.new_parser('a[0]["key"][var]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(0, result.lookups[0])
|
||||
assert_equal('key', result.lookups[1])
|
||||
assert_kind_of(VariableLookup, result.lookups[2])
|
||||
end
|
||||
|
||||
def test_consume_deep_nested_brackets
|
||||
parser = parse_context.new_parser('a[b[c[d]]]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
inner1 = result.lookups[0]
|
||||
assert_kind_of(VariableLookup, inner1)
|
||||
inner2 = inner1.lookups[0]
|
||||
assert_kind_of(VariableLookup, inner2)
|
||||
end
|
||||
|
||||
def test_consume_with_spaces_in_range
|
||||
parser = parse_context.new_parser('( 1 .. 10 )')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(1..10, result)
|
||||
end
|
||||
|
||||
def test_consume_starting_with_bracket_then_dots
|
||||
parser = parse_context.new_parser('[0].first.last')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(0, result.name)
|
||||
assert(result.lookup_command?(0))
|
||||
assert(result.lookup_command?(1))
|
||||
end
|
||||
|
||||
def test_consume_only_dot_lookups
|
||||
parser = parse_context.new_parser('a.b.c.d')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(['b', 'c', 'd'], result.lookups)
|
||||
end
|
||||
|
||||
def test_consume_only_bracket_lookups
|
||||
parser = parse_context.new_parser('a[0][1][2]')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_equal(3, result.lookups.length)
|
||||
assert_equal(0, result.lookups[0])
|
||||
assert_equal(1, result.lookups[1])
|
||||
assert_equal(2, result.lookups[2])
|
||||
end
|
||||
|
||||
def test_consume_complex_nested_expression
|
||||
parser = parse_context.new_parser('product.variants[index].title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(VariableLookup, result.lookups[1])
|
||||
assert_equal('index', result.lookups[1].name)
|
||||
end
|
||||
|
||||
def test_consume_range_with_bracketed_variables
|
||||
parser = parse_context.new_parser('(items[0]..items[1])')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
assert_kind_of(RangeLookup, result)
|
||||
assert_kind_of(VariableLookup, result.start_obj)
|
||||
end
|
||||
|
||||
def test_consume_evaluates_correctly
|
||||
parser = parse_context.new_parser('product')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
context = Context.new({ 'product' => 'Test Product' })
|
||||
assert_equal('Test Product', context.evaluate(result))
|
||||
end
|
||||
|
||||
def test_consume_with_lookups_evaluates_correctly
|
||||
parser = parse_context.new_parser('product.title')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
context = Context.new({ 'product' => { 'title' => 'My Title' } })
|
||||
assert_equal('My Title', context.evaluate(result))
|
||||
end
|
||||
|
||||
def test_consume_range_evaluates_correctly
|
||||
parser = parse_context.new_parser('(start..end)')
|
||||
result = ExpressionConsumer.consume(parser, parse_context)
|
||||
context = Context.new({ 'start' => 1, 'end' => 5 })
|
||||
assert_equal(1..5, context.evaluate(result))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_context
|
||||
@parse_context ||= ParseContext.new(environment: Environment.build(error_mode: :rigid))
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,102 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ExpressionParserTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_parse_returns_nil_for_empty_string
|
||||
result = ExpressionParser.parse('', parse_context)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
def test_parse_returns_nil_for_whitespace_only
|
||||
result = ExpressionParser.parse(' ', parse_context)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
def test_parse_raises_on_extra_tokens_after_expression
|
||||
error = assert_raises(SyntaxError) do
|
||||
ExpressionParser.parse('foo bar', parse_context)
|
||||
end
|
||||
assert_match(/Expected end_of_string but found id/, error.message)
|
||||
end
|
||||
|
||||
def test_parse_string_literal_with_double_quotes
|
||||
result = ExpressionParser.parse('"hello"', parse_context)
|
||||
assert_equal('hello', result)
|
||||
end
|
||||
|
||||
def test_parse_string_literal_with_single_quotes
|
||||
result = ExpressionParser.parse("'world'", parse_context)
|
||||
assert_equal('world', result)
|
||||
end
|
||||
|
||||
def test_parse_integer_literal
|
||||
result = ExpressionParser.parse('42', parse_context)
|
||||
assert_equal(42, result)
|
||||
end
|
||||
|
||||
def test_parse_float_literal
|
||||
result = ExpressionParser.parse('3.14', parse_context)
|
||||
assert_equal(3.14, result)
|
||||
end
|
||||
|
||||
def test_parse_nil_literal
|
||||
result = ExpressionParser.parse('nil', parse_context)
|
||||
assert_nil(result)
|
||||
end
|
||||
|
||||
def test_parse_true_literal
|
||||
result = ExpressionParser.parse('true', parse_context)
|
||||
assert_equal(true, result)
|
||||
end
|
||||
|
||||
def test_parse_simple_variable
|
||||
result = ExpressionParser.parse('product', parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
end
|
||||
|
||||
def test_parse_variable_with_dot_lookup
|
||||
result = ExpressionParser.parse('product.title', parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('product', result.name)
|
||||
assert_equal(['title'], result.lookups)
|
||||
end
|
||||
|
||||
def test_parse_variable_with_bracket_lookup
|
||||
result = ExpressionParser.parse('items[0]', parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
assert_equal('items', result.name)
|
||||
assert_equal(0, result.lookups[0])
|
||||
end
|
||||
|
||||
def test_parse_range_with_integer_literals
|
||||
result = ExpressionParser.parse('(1..5)', parse_context)
|
||||
assert_kind_of(Range, result)
|
||||
assert_equal(1..5, result)
|
||||
end
|
||||
|
||||
def test_parse_range_with_variables
|
||||
result = ExpressionParser.parse('(start..end)', parse_context)
|
||||
assert_kind_of(RangeLookup, result)
|
||||
end
|
||||
|
||||
def test_parse_validates_end_of_string
|
||||
result = ExpressionParser.parse('foo', parse_context)
|
||||
assert_kind_of(VariableLookup, result)
|
||||
end
|
||||
|
||||
def test_parse_evaluates_correctly
|
||||
result = ExpressionParser.parse('product.title', parse_context)
|
||||
context = Context.new({ 'product' => { 'title' => 'My Title' } })
|
||||
assert_equal('My Title', context.evaluate(result))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_context
|
||||
@parse_context ||= ParseContext.new(environment: Environment.build(error_mode: :rigid))
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user